diff --git a/.github/localflows/github-action-locally.sh b/.github/localflows/github-action-locally.sh index 77ca6fb80..d28cff034 100755 --- a/.github/localflows/github-action-locally.sh +++ b/.github/localflows/github-action-locally.sh @@ -6,4 +6,4 @@ if ! [ -x "$(which act)" ]; then fi # run act -act --reuse --platform self-hosted=jashbook/golang-lint:1.19-latest --workflows .github/localflows/cicd-local.yml +act --reuse --platform self-hosted=jashbook/golang-lint:1.20-latest --workflows .github/localflows/cicd-local.yml diff --git a/.github/utils/bug_stats.sh b/.github/utils/bug_stats.sh new file mode 100755 index 000000000..317bd2798 --- /dev/null +++ b/.github/utils/bug_stats.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +set -o errexit +set -o nounset + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +bug_report_md_file=${1} + +crit_cnt=$(cat ${bug_report_md_file} | grep crit | wc -l) +major_cnt=$(cat ${bug_report_md_file} | grep major | wc -l) +minor_cnt=$(cat ${bug_report_md_file} | grep minor | wc -l) +total_cnt=$(cat ${bug_report_md_file} | wc -l) +total_cnt=$((total_cnt-2)) + +printf "bug stats\ntotal open: %s\ncritial: %s\nmajor: %s\nminor: %s\n" ${total_cnt} ${crit_cnt} ${major_cnt} ${minor_cnt} \ No newline at end of file diff --git a/.github/utils/bugs_not_in_current_milestone.sh b/.github/utils/bugs_not_in_current_milestone.sh new file mode 100755 index 000000000..1c0176fd8 --- /dev/null +++ b/.github/utils/bugs_not_in_current_milestone.sh @@ -0,0 +1,37 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +process_issue_rows() { + for ((i = 0; i < ${item_count}; i++)) + do + local issue_body=$(echo ${last_issue_list} | jq -r ".[${i}]") + local issue_id=$(echo ${issue_body} | jq -r ".number") + local url=$(echo ${issue_body} | jq -r '.html_url') + local title=$(echo ${issue_body} | jq -r '.title') + local assignees=$(echo ${issue_body} | jq -r '.assignees[]?.login') + local state=$(echo ${issue_body}| jq -r '.state') + printf "[%s](%s) #%s | %s | %s | %s \n" "${title}" "${url}" "${issue_id}" "$(join_by , ${assignees})" "${state}" + gh_update_issue_milestone ${issue_id} + done +} + +item_count=100 +page=1 +printf "%s | %s | %s \n" "Issue Title" "Assignees" "Issue State" +echo "---|---|---" +while [ "${item_count}" == "100" ] +do + gh_get_issues "none" "kind/bug" "open" ${page} + item_count=$(echo ${last_issue_list} | jq -r '. | length') + process_issue_rows + page=$((page+1)) +done \ No newline at end of file diff --git a/.github/utils/create_releasing_pr.sh b/.github/utils/create_releasing_pr.sh new file mode 100755 index 000000000..bc7c1a9f6 --- /dev/null +++ b/.github/utils/create_releasing_pr.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +set -x + +git stash +git switch ${HEAD_BRANCH} +git pull +git rebase origin/${BASE_BRANCH} +git pull +git push + +echo "Creating ${PR_TITLE}" +gh pr create --head ${HEAD_BRANCH} --base ${BASE_BRANCH} --title "${PR_TITLE}" --body "" --label "releasing-task" \ No newline at end of file diff --git a/.github/utils/current_milestone_bugs.sh b/.github/utils/current_milestone_bugs.sh new file mode 100755 index 000000000..ff1a294d2 --- /dev/null +++ b/.github/utils/current_milestone_bugs.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +LABELS=${LABELS:-'kind/bug,bug'} #"severity/critical,severity/major,severity/minor,severity/normal" + +print_issue_rows() { + for ((i = 0; i < ${item_count}; i++)) + do + local issue_body=$(echo ${last_issue_list} | jq -r ".[${i}]") + local issue_id=$(echo ${issue_body} | jq -r ".number") + local url=$(echo ${issue_body} | jq -r '.html_url') + local title=$(echo ${issue_body} | jq -r '.title') + local assignees=$(echo ${issue_body} | jq -r '.assignees[]?.login') + local state=$(echo ${issue_body}| jq -r '.state') + local labels=$(echo ${issue_body} | jq -r '.labels[]?.name') + printf "[%s](%s) #%s | %s | %s | %s \n" "${title}" "${url}" "${issue_id}" "$(join_by , ${assignees})" "${state}" "$(join_by , ${labels})" + done +} + +count_total=0 +item_count=100 +page=1 +echo "" +printf "%s | %s | %s | %s \n" "Issue Title" "Assignees" "Issue State" "Labels" +echo "---|---|---|---" +while [ "${item_count}" == "100" ] +do + gh_get_issues ${MILESTONE_ID} "${LABELS}" "open" ${page} + item_count=$(echo ${last_issue_list} | jq -r '. | length') + print_issue_rows + page=$((page+1)) + count_total=$((count_total + item_count)) +done + +if [ -n "$DEBUG" ]; then +echo "" +echo "total items: ${count_total}" +fi diff --git a/.github/utils/feature_triage.sh b/.github/utils/feature_triage.sh index 41044eebd..997ec7189 100755 --- a/.github/utils/feature_triage.sh +++ b/.github/utils/feature_triage.sh @@ -4,44 +4,44 @@ set -o errexit set -o nounset set -o pipefail -REMOTE_URL=$(git config --get remote.origin.url) -OWNER=$(dirname ${REMOTE_URL} | awk -F ":" '{print $2}') -REPO=$(basename -s .git ${REMOTE_URL}) -MILESTONE_ID=${MILESTONE_ID:-5} +# requires `git`, `gh`, and `jq` commands, ref. https://cli.github.com/manual/installation for installation guides. -# GH list issues API ref: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues -ISSUE_LIST=$(gh api \ - --header 'Accept: application/vnd.github+json' \ - --method GET \ - /repos/${OWNER}/${REPO}/issues \ - -F per_page=100 \ - -f milestone=${MILESTONE_ID} \ - -f labels=kind/feature \ - -f state=all) - -ROWS=$(echo ${ISSUE_LIST}| jq -r '. | sort_by(.state,.number)| .[].number') +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash +print_issue_rows() { + for ((i = 0; i < ${item_count}; i++)) + do + local issue_body=$(echo ${last_issue_list} | jq -r ".[${i}]") + local issue_id=$(echo ${issue_body} | jq -r ".number") + local url=$(echo ${issue_body} | jq -r '.html_url') + local title=$(echo ${issue_body} | jq -r '.title') + local assignees=$(echo ${issue_body} | jq -r '.assignees[]?.login') + local state=$(echo ${issue_body}| jq -r '.state') + local labels=$(echo ${issue_body} | jq -r '.labels[]?.name') + pr_url=$(echo ${issue_body} | jq -r '.pull_request?.url') + if [ "$pr_url" == "null" ]; then + pr_url="N/A" + fi + printf "[%s](%s) #%s | %s | %s | %s| | \n" "${title}" "${url}" "${issue_id}" "$(join_by , ${assignees})" "${state}" "${pr_url}" + done +} +count_total=0 +item_count=100 +page=1 +echo "" printf "%s | %s | %s | %s | %s | %s\n" "Feature Title" "Assignees" "Issue State" "Code PR Merge Status" "Feature Doc. Status" "Extra Notes" echo "---|---|---|---|---|---" -for ROW in $ROWS -do - ISSUE_ID=$(echo $ROW | awk -F "," '{print $1}') - # GH get issue API ref: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#get-an-issue - ISSUE_BODY=$(gh api \ - --header 'Accept: application/vnd.github+json' \ - --method GET \ - /repos/${OWNER}/${REPO}/issues/${ISSUE_ID}) - URL=$(echo $ISSUE_BODY| jq -r '.url') - TITLE=$(echo $ISSUE_BODY| jq -r '.title') - ASSIGNEES=$(echo $ISSUE_BODY| jq -r '.assignees[]?.login') - ASSIGNEES_PRINTABLE= - for ASSIGNEE in $ASSIGNEES - do - ASSIGNEES_PRINTABLE="${ASSIGNEES_PRINTABLE},${ASSIGNEE}" - done - ASSIGNEES_PRINTABLE=${ASSIGNEES_PRINTABLE#,} - STATE=$(echo $ISSUE_BODY| jq -r '.state') - PR=$(echo $ISSUE_BODY| jq -r '.pull_request?.url') - printf "[%s](%s) #%s | %s | %s | | | \n" "$TITLE" $URL $ISSUE_ID "$ASSIGNEES_PRINTABLE" "$STATE" -done \ No newline at end of file +while [ "${item_count}" == "100" ] +do + gh_get_issues ${MILESTONE_ID} "kind/feature" "all" ${page} + item_count=$(echo ${last_issue_list} | jq -r '. | length') + print_issue_rows + page=$((page+1)) + count_total=$((count_total + item_count)) +done + +echo "" +echo "total items: ${count_total}" diff --git a/.github/utils/functions.bash b/.github/utils/functions.bash new file mode 100644 index 000000000..de1e22c24 --- /dev/null +++ b/.github/utils/functions.bash @@ -0,0 +1,112 @@ +# bash functions + +DEBUG=${DEBUG:-} +# requires `gh` command, ref. https://cli.github.com/manual/installation for installation guides. + +gh_get_issues () { + # @arg milestone - Milestone ID, if the string none is passed, issues without milestones are returned. + # @arg labels - A list of comma separated label names, processed as OR query. + # @arg state - Can be one of: open, closed, all; Default: open. + # @arg page - Cardinal value; Default: 1 + # @result $last_issue_list - contains JSON result + declare milestone="$1" labels="$2" state="${3:-open}" page="${4:-1}" + local label_filter="" + IFS=',' read -ra label_items <<< "${labels}" + for i in "${label_items[@]}"; do + label_filter="${label_filter} -f labels=${i}" + done + _gh_get_issues ${milestone} "${label_filter}" ${state} ${page} +} + +gh_get_issues_with_and_labels () { + # @arg milestone - Milestone ID, if the string none is passed, issues without milestones are returned. + # @arg labels - A list of comma separated label names, processed as AND query. + # @arg state - Can be one of: open, closed, all; Default: open. + # @arg page - Cardinal value; Default: 1 + # @result $last_issue_list - contains JSON result + declare milestone="$1" labels="$2" state="${3:-open}" page="${4:-1}" + _gh_get_issues ${milestone} "-f labels=${labels}" ${state} ${page} +} + +_gh_get_issues () { + # @arg milestone - Milestone ID, if the string none is passed, issues without milestones are returned. + # @arg label_filter - Label fileter query params. + # @arg state - Can be one of: open, closed, all; Default: open. + # @arg page - Cardinal value; Default: 1 + # @result $last_issue_list - contains JSON result + declare milestone="$1" label_filter="$2" state="${3:-open}" page="${4:-1}" + + # GH list issues API ref: https://docs.github.com/en/rest/issues/issues?apiVersion=2022-11-28#list-repository-issues + local cmd="gh api \ + --method GET \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + /repos/${OWNER}/${REPO}/issues \ + -F per_page=100 \ + -F page=${page} \ + -f milestone=${milestone} \ + ${label_filter} \ + -f state=${state}" + if [ -n "$DEBUG" ]; then echo $cmd; fi + last_issue_list=`eval ${cmd} 2> /dev/null` +} + + +gh_get_issue_body() { + # @arg issue_id - Github issue ID + # @result last_issue_body + # @result last_issue_url + # @result last_issue_title + # @result last_issue_state + # @result last_issue_assignees - multi-lines items + declare issue_id="$1" + + local issue_body=$(gh api \ + --method GET \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + /repos/${OWNER}/${REPO}/issues/${issue_id}) + local url=$(echo ${issue_body} | jq -r '.url') + local title=$(echo ${issue_body} | jq -r '.title') + local assignees=$(echo ${issue_body} | jq -r '.assignees[]?.login') + local state=$(echo ${issue_body}| jq -r '.state') + last_issue_body="${issue_body}" + last_issue_url="${url}" + last_issue_title="${title}" + last_issue_state="${state}" + last_issue_assignees=${assignees} +} + +gh_update_issue_milestone() { + # @arg issue_id - Github issue ID + # @arg milestone - Milestone ID, if the string none is passed, issues without milestones are returned. + # @result last_issue_resp + declare issue_id="$1" milestone_id="${2:-}" + + if [ -z "$milestone_id" ]; then + milestone_id=${MILESTONE_ID} + fi + + local req_data="{\"milestone\":$milestone_id}" + + if [ -n "$DEBUG" ]; then echo "req_data=$req_data"; fi + + local gh_token=$(gh auth token) + local resp=$(curl \ + --location \ + --request PATCH \ + --header 'Accept: application/vnd.github+json' \ + --header 'X-GitHub-Api-Version: 2022-11-28' \ + --header "Authorization: Bearer ${gh_token}" \ + --data "${req_data}" \ + https://api.github.com/repos/${OWNER}/${REPO}/issues/${issue_id}) + + last_issue_resp=${resp} +} + +function join_by { + local d=${1-} f=${2-} + if shift 2; then + printf %s "$f" "${@/#/$d}" + fi +} diff --git a/.github/utils/generate_kbcli_sha256.py b/.github/utils/generate_kbcli_sha256.py new file mode 100644 index 000000000..4c1c96a41 --- /dev/null +++ b/.github/utils/generate_kbcli_sha256.py @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# generate kbcli sha256 notes +# 1. open each *.sha256.txt in target direct +# 2. get the contain of the file +# 3. render the template + +import os +import sys +from datetime import date +from string import Template + +release_note_template_path = "docs/release_notes/template.md" + +def main(argv: list[str]) -> None: + """ + :param: the kbcli version + :param: the sha256 files direct + :return None + """ + kbcli_version = argv[1] + sha256_direct = argv[2] + release_note_template_path = "docs/release_notes/kbcli_template.md" + release_note_path = f"docs/release_notes/{kbcli_version}/kbcli.md" + + template = "" + try: + with open(release_note_template_path, "r") as file: + template = file.read() + except FileNotFoundError as e: + print(f"template {release_note_template_path} not found, IGNORED") + + with open(release_note_path,'a') as f_dest: + f_dest.write(Template(template).safe_substitute( + kbcli_version = kbcli_version[1:], + today = date.today().strftime("%Y-%m-%d"), + )) + for file in os.listdir(sha256_direct): + with open(os.path.join(sha256_direct, file),"r") as f: + f_dest.write(f.read()) + f_dest.write('\n') + print("Done") + +if __name__ == "__main__": + main(sys.argv) \ No newline at end of file diff --git a/.github/utils/generate_release_notes.py b/.github/utils/generate_release_notes.py index e01c63c9e..6159fbea8 100755 --- a/.github/utils/generate_release_notes.py +++ b/.github/utils/generate_release_notes.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python +#!/usr/bin/env python3 # -*- coding:utf-8 -*- # generate release note for milestone @@ -15,139 +15,149 @@ from github import Github -releaseIssueRegex = "^v(.*) Release Planning$" -majorReleaseRegex = "^([0-9]+\.[0-9]+)\.[0-9]+.*$" -milestoneRegex = "https://github.com/apecloud/kubeblocks/milestone/([0-9]+)" - -githubToken = os.getenv("GITHUB_TOKEN") - -changeTypes = [ - "New Features", - "Bug Fixes", - "Miscellaneous" -] - - -def get_change_priority(name): - if name in changeTypes: - return changeTypes.index(name) - return len(changeTypes) - - -changes = [] -warnings = [] -changeLines = [] -breakingChangeLines = [] - -gh = Github(githubToken) - -# get milestone issue -issues = [i for i in gh.get_repo("apecloud/kubeblocks").get_issues(state='open') if - re.search(releaseIssueRegex, i.title)] -issues = sorted(issues, key=lambda i: i.id) - -if len(issues) == 0: - print("FATAL: failed to find issue for release.") - sys.exit(0) - -if len(issues) > 1: - print("WARNING: found more than one issue for release, so first issue created will be picked: {}". - format([i.title for i in issues])) - -issue = issues[0] -print("Found issue: {}".format(issue.title)) - -# get release version from issue name -releaseVersion = re.search(releaseIssueRegex, issue.title).group(1) -print("Generating release notes for KubeBlocks {}".format(releaseVersion)) - -# Set REL_VERSION -if os.getenv("GITHUB_ENV"): - with open(os.getenv("GITHUB_ENV"), "a") as githubEnv: - githubEnv.write("REL_VERSION={}\n".format(releaseVersion)) - githubEnv.write("REL_BRANCH=release-{}\n".format(re.search(majorReleaseRegex, releaseVersion).group(1))) - -releaseNotePath = "docs/release_notes/v{}.md".format(releaseVersion) - -# get milestone -repoMilestones = re.findall(milestoneRegex, issue.body) -if len(repoMilestones) == 0: - print("FATAL: failed to find milestone in release issue body") - sys.exit(0) -if len(repoMilestones) > 1: - print("WARNING: found more than one milestone in release issue body, first milestone will be picked: {}". - format([i for i in repoMilestones])) - -# find all issues and PRs in milestone -repo = gh.get_repo(f"apecloud/kubeblocks") -milestone = repo.get_milestone(int(repoMilestones[0])) -issueOrPRs = [i for i in repo.get_issues(milestone, state="closed")] -print("Detected {} issues or pull requests".format(len(issueOrPRs))) - -# find all contributors and build changes -allContributors = set() -for issueOrPR in issueOrPRs: - url = issueOrPR.html_url +RELEASE_ISSUE_RANGE = "^v(.*) Release Planning$" +MAJOR_RELEASE_REGEX = "^([0-9]+\.[0-9]+)\.[0-9]+.*$" +MILESTONE_REGEX = "https://github.com/apecloud/kubeblocks/milestone/([0-9]+)" +CHANGE_TYPES : list[str] = ["New Features", "Bug Fixes", "Miscellaneous"] + + +def get_change_priority(name: str) -> int: + if name in CHANGE_TYPES: + return CHANGE_TYPES.index(name) + return len(CHANGE_TYPES) + + +def main(argv: list[str]) -> None: + changes = [] + warnings = [] + change_lines = [] + breaking_change_lines = [] + gh_env = os.getenv("GITHUB_ENV") + gh = Github(os.getenv("GITHUB_TOKEN")) + + # get milestone issue + issues = [ + i + for i in gh.get_repo("apecloud/kubeblocks").get_issues(state="open") + if re.search(RELEASE_ISSUE_RANGE, i.title) + ] + issues = sorted(issues, key=lambda i: i.id) + + if len(issues) == 0: + print("FATAL: failed to find issue for release.") + sys.exit(0) + + if len(issues) > 1: + print(f"WARNING: found more than one issue for release, so first issue created will be picked: {[i.title for i in issues]}") + + issue = issues[0] + print(f"Found issue: {issue.title}") + + # get release version from issue name + release_version = re.search(RELEASE_ISSUE_RANGE, issue.title).group(1) + print(f"Generating release notes for KubeBlocks {release_version}") + + # Set REL_VERSION + if gh_env: + with open(gh_env, "a") as f: + f.write(f"REL_VERSION={release_version}\n") + f.write(f"REL_BRANCH=release-{re.search(MAJOR_RELEASE_REGEX, release_version).group(1)}\n") + + release_note_path = f"docs/release_notes/v{release_version}.md" + + # get milestone + repo_milestones = re.findall(MILESTONE_REGEX, issue.body) + if len(repo_milestones) == 0: + print("FATAL: failed to find milestone in release issue body") + sys.exit(0) + if len(repo_milestones) > 1: + print(f"WARNING: found more than one milestone in release issue body, first milestone will be picked: {[i for i in repo_milestones]}") + + # find all issues and PRs in milestone + repo = gh.get_repo(f"apecloud/kubeblocks") + milestone = repo.get_milestone(int(repo_milestones[0])) + issue_or_prs = [i for i in repo.get_issues(milestone, state="closed")] + print(f"Detected {len(issue_or_prs)} issues or pull requests") + + # find all contributors and build changes + allContributors = set() + for issue_or_pr in issue_or_prs: + url = issue_or_pr.html_url + try: + # only a PR can be converted to a PR object, otherwise will throw error. + pr = issue_or_pr.as_pull_request() + except: + continue + if not pr.merged: + continue + contributor = "@" + str(pr.user.login) + # Auto generate a release note + note = pr.title.strip() + change_type = "Miscellaneous" + title = note.split(":") + if len(title) > 1: + prefix = title[0].strip().lower() + if prefix in ("feat", "feature"): + change_type = "New Features" + elif prefix in ("fix", "bug"): + change_type = "Bug Fixes" + note = title[1].strip() + changes.append((change_type, pr, note, contributor, url)) + allContributors.add(contributor) + + last_subtitle = "" + # generate changes for release notes + for change in sorted(changes, key=lambda c: (get_change_priority(c[0]), c[1].id)): + subtitle = change[0] + if last_subtitle != subtitle: + last_subtitle = subtitle + change_lines.append("\n### " + subtitle) + breaking_change = "breaking-change" in [label.name for label in change[1].labels] + change_url = " ([#" + str(change[1].number) + "](" + change[4] + ")" + change_author = ", " + change[3] + ")" + change_lines.append("- " + change[2] + change_url + change_author) + if breaking_change: + breaking_change_lines.append("- " + change[2] + change_url + change_author) + + if len(breaking_change_lines) > 0: + warnings.append( + "> **Note: This release contains a few [breaking changes](#breaking-changes).**" + ) + + # generate release note from template + template = "" + release_note_template_path = "docs/release_notes/template.md" try: - # only a PR can be converted to a PR object, otherwise will throw error. - pr = issueOrPR.as_pull_request() - except: - continue - if not pr.merged: - continue - contributor = "@" + str(pr.user.login) - # Auto generate a release note - note = pr.title.strip() - changeType = "Miscellaneous" - title = note.split(":") - if len(title) > 1: - prefix = title[0].strip().lower() - if prefix in ("feat", "feature"): - changeType = "New Features" - elif prefix in ("fix", "bug"): - changeType = "Bug Fixes" - note = title[1].strip() - changes.append((changeType, pr, note, contributor, url)) - allContributors.add(contributor) - -lastSubtitle = "" -# generate changes for release notes -for change in sorted(changes, key=lambda c: (get_change_priority(c[0]), c[1].id)): - subtitle = change[0] - if lastSubtitle != subtitle: - lastSubtitle = subtitle - changeLines.append("\n### " + subtitle) - breakingChange = 'breaking-change' in [label.name for label in change[1].labels] - changeUrl = " ([#" + str(change[1].number) + "](" + change[4] + ")" - changeAuthor = ", " + change[3] + ")" - changeLines.append("- " + change[2] + changeUrl + changeAuthor) - if breakingChange: - breakingChangeLines.append("- " + change[2] + changeUrl + changeAuthor) - -if len(breakingChangeLines) > 0: - warnings.append("> **Note: This release contains a few [breaking changes](#breaking-changes).**") - -# generate release note from template -template = '' -releaseNoteTemplatePath = "docs/release_notes/template.md" -with open(releaseNoteTemplatePath, "r") as file: - template = file.read() - -changeText = "\n".join(changeLines) -breakingChangeText = "None." -if len(breakingChangeLines) > 0: - breakingChangeText = '\n'.join(breakingChangeLines) -warningsText = '' -if len(warnings) > 0: - warningsText = '\n'.join(warnings) - -with open(releaseNotePath, 'w') as file: - file.write(Template(template).safe_substitute( - kubeblocks_version=releaseVersion, - kubeblocks_changes=changeText, - kubeblocks_breaking_changes=breakingChangeText, - warnings=warningsText, - kubeblocks_contributors=', '.join(sorted(list(allContributors), key=str.casefold)), - today=date.today().strftime("%Y-%m-%d"))) - -print("Done") + with open(release_note_template_path, "r") as file: + template = file.read() + except FileNotFoundError as e: + print(f"template {release_note_template_path} not found, IGNORED") + + change_text = "\n".join(change_lines) + breaking_change_text = "None." + if len(breaking_change_lines) > 0: + breaking_change_text = "\n".join(breaking_change_lines) + + warnings_text = "" + if len(warnings) > 0: + warnings_text = "\n".join(warnings) + + with open(release_note_path, "w") as file: + file.write( + Template(template).safe_substitute( + kubeblocks_version=release_version, + kubeblocks_changes=change_text, + kubeblocks_breaking_changes=breaking_change_text, + warnings=warnings_text, + kubeblocks_contributors=", ".join( + sorted(list(allContributors), key=str.casefold) + ), + today=date.today().strftime("%Y-%m-%d"), + ) + ) + + print("Done") + + +if __name__ == "__main__": + main(sys.argv) diff --git a/.github/utils/get_release_version.py b/.github/utils/get_release_version.py deleted file mode 100755 index e1bcf4177..000000000 --- a/.github/utils/get_release_version.py +++ /dev/null @@ -1,37 +0,0 @@ -#!/usr/bin/env python -# -*- coding:utf-8 -*- - -# Get release version from git tag and set the parsed version to -# environment variable REL_VERSION, if found the release note, set -# WITH_RELEASE_NOTES to true. - -import os -import sys - -gitRef = os.getenv("GITHUB_REF") -tagRefPrefix = "refs/tags/v" - -with open(os.getenv("GITHUB_ENV"), "a") as githubEnv: - if gitRef is None or not gitRef.startswith(tagRefPrefix): - print("This is not a release tag") - sys.exit(1) - - releaseVersion = gitRef[len(tagRefPrefix):] - releaseNotePath = "docs/release_notes/v{}/v{}.md".format(releaseVersion,releaseVersion) - - if gitRef.find("-alpha.") > 0: - print("Alpha release build from {} ...".format(gitRef)) - elif gitRef.find("-beta.") > 0: - print("Beta release build from {} ...".format(gitRef)) - elif gitRef.find("-rc.") > 0: - print("Release Candidate build from {} ...".format(gitRef)) - else: - print("Checking if {} exists".format(releaseNotePath)) - if os.path.exists(releaseNotePath): - print("Found {}".format(releaseNotePath)) - githubEnv.write("WITH_RELEASE_NOTES=true\n") - else: - print("{} is not found".format(releaseNotePath)) - print("Release build from {} ...".format(gitRef)) - - githubEnv.write("REL_VERSION={}\n".format(releaseVersion)) diff --git a/.github/utils/gh_env b/.github/utils/gh_env new file mode 100644 index 000000000..18b51b01f --- /dev/null +++ b/.github/utils/gh_env @@ -0,0 +1,23 @@ +DEBUG=${DEBUG:-} + +export MILESTONE_ID=${MILESTONE_ID:-5} + +export BASE_BRANCH=${BASE_BRANCH:-'release-0.5'} +export HEAD_BRANCH=${HEAD_BRANCH:-'releasing-0.5'} +export PR_TITLE=${PR_TITLE:-"chore(releasing): ${BASE_BRANCH} tracker PR (no-need-to-review)"} + + +export REMOTE_URL=$(git config --get remote.origin.url) +export OWNER=$(dirname ${REMOTE_URL} | awk -F ":" '{print $2}') +prefix='^//' +if [[ $OWNER =~ $prefix ]]; then +export OWNER="${OWNER#*//github.com/}" +fi +export REPO=$(basename -s .git ${REMOTE_URL}) + + +if [ -n "$DEBUG" ]; then +echo "OWNER=${OWNER}" +echo "REPO=${REPO}" +echo "MILESTONE_ID=${MILESTONE_ID}" +fi \ No newline at end of file diff --git a/.github/utils/is_rc_or_stable_release_version.py b/.github/utils/is_rc_or_stable_release_version.py new file mode 100755 index 000000000..9bd43b97e --- /dev/null +++ b/.github/utils/is_rc_or_stable_release_version.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- + +# Get release version from git tag and set the parsed version to +# environment variable REL_VERSION, if found the release note, set +# WITH_RELEASE_NOTES to true. + +import os +import sys +from typing import TypeAlias + + +def main(argv: list[str]) -> None: + git_ref = os.getenv("GITHUB_REF") + tag_ref_prefix = "refs/tags/v" + github_env : str = str(os.getenv("GITHUB_ENV")) + + + with open(github_env, "a") as github_env_f: + if git_ref is None or not git_ref.startswith(tag_ref_prefix): + print("This is not a release tag") + sys.exit(1) + + release_version = git_ref[len(tag_ref_prefix) :] + release_note_path = f"docs/release_notes/v{release_version}/v{release_version}.md" + + def set_with_rel_note_to_true() -> None: + print(f"Checking if {release_note_path} exists") + if os.path.exists(release_note_path): + print(f"Found {release_note_path}") + github_env_f.write("WITH_RELEASE_NOTES=true\n") + else: + print("{} is not found".format(release_note_path)) + print(f"Release build from {git_ref} ...") + + + if git_ref.find("-alpha.") > 0: + print(f"Alpha release build from {git_ref} ...") + print(f"IGNORED") + elif git_ref.find("-beta.") > 0: + print(f"Beta release build from {git_ref} ...") + print(f"IGNORED") + elif git_ref.find("-rc.") > 0: + print(f"Release Candidate build from {git_ref} ...") + set_with_rel_note_to_true() + else: + set_with_rel_note_to_true() + + github_env_f.write(f"REL_VERSION={release_version}\n") + + +if __name__ == "__main__": + main(sys.argv) diff --git a/.github/utils/merge_releasing_branch.sh b/.github/utils/merge_releasing_branch.sh new file mode 100644 index 000000000..29903b7c3 --- /dev/null +++ b/.github/utils/merge_releasing_branch.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +set -x + +git switch ${BASE_BRANCH} +git merge ${HEAD_BRANCH} +git push \ No newline at end of file diff --git a/.github/utils/merge_releasing_pr.sh b/.github/utils/merge_releasing_pr.sh new file mode 100755 index 000000000..5e713be99 --- /dev/null +++ b/.github/utils/merge_releasing_pr.sh @@ -0,0 +1,62 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +get_pr_status() { + pr_info=$(gh pr --repo ${OWNER}/${REPO} view ${pr_number} --json "mergeStateStatus,mergeable") + pr_merge_status=$(echo ${pr_info} | jq -r '.mergeStateStatus') + pr_mergeable=$(echo ${pr_info} | jq -r '.mergeable') + if [ -n "$DEBUG" ]; then + echo "pr_number=${pr_number}" + echo "pr_merge_status=${pr_merge_status}" + echo "pr_mergeable=${pr_mergeable}" + fi +} + +echo "Merging ${PR_TITLE}" + +retry_times=0 +pr_info=$(gh pr list --repo ${OWNER}/${REPO} --head ${HEAD_BRANCH} --base ${BASE_BRANCH} --json "number" ) +pr_len=$(echo ${pr_info} | jq -r '. | length') +if [ "${pr_len}" == "0" ]; then +exit 0 +fi + +pr_number=$(echo ${pr_info} | jq -r '.[0].number') +get_pr_status + +if [ "${pr_mergeable}" == "MERGEABLE" ]; then + if [ "${pr_merge_status}" == "BLOCKED" ]; then + echo "Approve PR #${pr_number}" + gh pr --repo ${OWNER}/${REPO} comment ${pr_number} --body "/approve" + sleep 5 + get_pr_status + fi + + if [ "${pr_merge_status}" == "UNSTABLE" ]; then + retry_times=100 + while [ $retry_times -gt 0 ] && [ "${pr_merge_status}" == "UNSTABLE" ] + do + ((retry_times--)) + sleep 5 + get_pr_status + done + fi + + if [ "${pr_merge_status}" == "CLEAN" ]; then + echo "Merging PR #${pr_number}" + set -x + gh pr --repo ${OWNER}/${REPO} merge ${pr_number} --rebase + exit 0 + fi +fi +exit 1 + diff --git a/.github/utils/requirements.txt b/.github/utils/requirements.txt new file mode 100644 index 000000000..b393fb08a --- /dev/null +++ b/.github/utils/requirements.txt @@ -0,0 +1 @@ +PyGithub \ No newline at end of file diff --git a/.github/utils/sanitize_release_body.sh b/.github/utils/sanitize_release_body.sh new file mode 100755 index 000000000..bf273208a --- /dev/null +++ b/.github/utils/sanitize_release_body.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash + +set -o errexit +set -o nounset +set -o pipefail + +# requires `git` and `gh` commands, ref. https://cli.github.com/manual/installation for installation guides. + +workdir=$(dirname $0) +. ${workdir}/gh_env +. ${workdir}/functions.bash + +TAG=${TAG:-} +GITHUB_REF=${GITHUB_REF:-} +tag_ref_prefix="refs/tags/" + +if [ -z "$TAG" ] && [ -n "$GITHUB_REF" ]; then +TAG=${GITHUB_REF#"${tag_ref_prefix}"} +fi + +if [ -z "$TAG" ]; then + if [ -n "$DEBUG" ]; then echo "EMPTY TAG, NOOP"; fi + exit 0 +fi + +if [ -n "$DEBUG" ]; then +set -x +fi +echo "Processing tag ${TAG}" +rel_body=$(gh release \ + --repo ${OWNER}/${REPO} view "${TAG}" \ + --json 'body') + +rel_body_text=$(echo ${rel_body} | jq -r '.body') + +if [ -n "$DEBUG" ]; then echo $rel_body_text; fi + + +# set -o noglob +IFS=$'\r\n' rel_items=($rel_body_text) +# set +o noglob + +final_rel_notes="" +for val in "${rel_items[@]}"; +do + if [[ $val == "**Full Changelog**"* ]]; then + final_rel_notes="${final_rel_notes}\r\n\r\n${val}" + continue + fi + # ignore line if contain ${PR_TITLE} + if [[ $val != "* ${PR_TITLE}"* ]];then + final_rel_notes="${final_rel_notes}${val}\r\n" + fi +done + +if [ -n "$DEBUG" ]; then echo -e $final_rel_notes; fi + +# gh release --repo ${OWNER}/${REPO} edit ${TAG} --notes "$(echo -e ${final_rel_notes})" diff --git a/.github/utils/typos.toml b/.github/utils/typos.toml index 862285ef0..9b841c16d 100644 --- a/.github/utils/typos.toml +++ b/.github/utils/typos.toml @@ -1,8 +1,9 @@ [default.extend-identifiers] # *sigh* this just isn't worth the cost of fixing -ClusterPhaseMisMatch = "ClusterPhaseMisMatch" [default.extend-words] # Don't correct the "AKS" AKS = "AKS" -ons = "ons" \ No newline at end of file +ons = "ons" +HashiCorp = "HashiCorp" +Hashi = "Hashi" diff --git a/.github/utils/utils.sh b/.github/utils/utils.sh index 7be50ef4d..bc436c8a2 100644 --- a/.github/utils/utils.sh +++ b/.github/utils/utils.sh @@ -16,9 +16,23 @@ Usage: $(basename "$0") 4) get latest release tag 5) update release latest 6) get the ci trigger mode + 7) check package version + 8) kill apiserver and etcd + 9) remove runner + 10) trigger release + 11) release message + 12) send message + 13) patch release notes -tn, --tag-name Release tag name -gr, --github-repo Github Repo -gt, --github-token Github token + -rn, --runner-name The runner name + -bn, --branch-name The branch name + -c, --content The trigger request content + -bw, --bot-webhook The bot webhook + -tt, --trigger-type The trigger type (e.g. release/package) + -ru, --run-url The run url + -fl, --file The release notes file EOF } @@ -31,6 +45,14 @@ main() { local GITHUB_REPO local GITHUB_TOKEN local TRIGGER_MODE="" + local RUNNER_NAME="" + local BRANCH_NAME="" + local CONTENT="" + local BOT_WEBHOOK="" + local TRIGGER_TYPE="release" + local RELEASE_VERSION="" + local RUN_URL="" + local FILE="" parse_command_line "$@" @@ -53,6 +75,27 @@ main() { 6) get_trigger_mode ;; + 7) + check_package_version + ;; + 8) + kill_server_etcd + ;; + 9) + remove_runner + ;; + 10) + trigger_release + ;; + 11) + release_message + ;; + 12) + send_message + ;; + 13) + patch_release_notes + ;; *) show_help break @@ -91,6 +134,48 @@ parse_command_line() { shift fi ;; + -rn|--runner-name) + if [[ -n "${2:-}" ]]; then + RUNNER_NAME="$2" + shift + fi + ;; + -bn|--branch-name) + if [[ -n "${2:-}" ]]; then + BRANCH_NAME="$2" + shift + fi + ;; + -c|--content) + if [[ -n "${2:-}" ]]; then + CONTENT="$2" + shift + fi + ;; + -bw|--bot-webhook) + if [[ -n "${2:-}" ]]; then + BOT_WEBHOOK="$2" + shift + fi + ;; + -tt|--trigger-type) + if [[ -n "${2:-}" ]]; then + TRIGGER_TYPE="$2" + shift + fi + ;; + -ru|--run-url) + if [[ -n "${2:-}" ]]; then + RUN_URL="$2" + shift + fi + ;; + -fl|--file) + if [[ -n "${2:-}" ]]; then + FILE="$2" + shift + fi + ;; *) break ;; @@ -124,6 +209,131 @@ update_release_latest() { -d '{"draft":false,"prerelease":false,"make_latest":true}' } +kill_server_etcd() { + server="kube-apiserver\|etcd" + for pid in $( ps -ef | grep "$server" | grep -v "grep $server" | awk '{print $2}' ); do + kill $pid + done +} + +remove_runner() { + runners_url=$GITHUB_API/repos/$LATEST_REPO/actions/runners + runners_list=$( gh_curl -s $runners_url ) + total_count=$( echo "$runners_list" | jq '.total_count' ) + for i in $(seq 0 $total_count); do + if [[ "$i" == "$total_count" ]]; then + break + fi + runner_name=$( echo "$runners_list" | jq ".runners[$i].name" --raw-output ) + runner_status=$( echo "$runners_list" | jq ".runners[$i].status" --raw-output ) + runner_busy=$( echo "$runners_list" | jq ".runners[$i].busy" --raw-output ) + runner_id=$( echo "$runners_list" | jq ".runners[$i].id" --raw-output ) + if [[ "$runner_name" == "$RUNNER_NAME" && "$runner_status" == "online" && "$runner_busy" == "false" ]]; then + echo "runner_name:"$runner_name + gh_curl -L -X DELETE $runners_url/$runner_id + break + fi + done +} + +check_numeric() { + input=${1:-""} + if [[ $input =~ ^[0-9]+$ ]]; then + echo $(( ${input} )) + else + echo "no" + fi +} + +get_next_available_tag() { + tag_type="$1" + index="" + release_list=$( gh release list --repo $LATEST_REPO --limit 100 ) + for tag in $( echo "$release_list" | (grep "$tag_type" || true) ) ;do + if [[ "$tag" != "$tag_type"* ]]; then + continue + fi + tmp=${tag#*$tag_type} + numeric=$( check_numeric "$tmp" ) + if [[ "$numeric" == "no" ]]; then + continue + fi + if [[ $numeric -gt $index || -z "$index" ]]; then + index=$numeric + fi + done + + if [[ -z "$index" ]];then + index=0 + else + index=$(( $index + 1 )) + fi + + RELEASE_VERSION="${tag_type}${index}" +} + +release_next_available_tag() { + dispatches_url=$1 + v_major_minor="v$TAG_NAME" + stable_type="$v_major_minor." + get_next_available_tag $stable_type + v_number=$RELEASE_VERSION + alpha_type="$v_number-alpha." + beta_type="$v_number-beta." + rc_type="$v_number-rc." + case "$CONTENT" in + *alpha*) + get_next_available_tag "$alpha_type" + ;; + *beta*) + get_next_available_tag "$beta_type" + ;; + *rc*) + get_next_available_tag "$rc_type" + ;; + esac + + if [[ ! -z "$RELEASE_VERSION" ]];then + gh_curl -X POST $dispatches_url -d '{"ref":"'$BRANCH_NAME'","inputs":{"release_version":"'$RELEASE_VERSION'"}}' + fi +} + +usage_message() { + curl -H "Content-Type: application/json" -X POST $BOT_WEBHOOK \ + -d '{"msg_type":"post","content":{"post":{"zh_cn":{"title":"Usage:","content":[[{"tag":"text","text":"please enter the correct format\n"},{"tag":"text","text":"1. do release\n"},{"tag":"text","text":"2. {\"ref\":\"\",\"inputs\":{\"release_version\":\"\"}}"}]]}}}}' +} + +trigger_release() { + echo "CONTENT:$CONTENT" + dispatches_url=$GITHUB_API/repos/$LATEST_REPO/actions/workflows/$TRIGGER_TYPE-version.yml/dispatches + + if [[ "$CONTENT" == "do"*"release" ]]; then + release_next_available_tag "$dispatches_url" + else + usage_message + fi +} + +release_message() { + curl -H "Content-Type: application/json" -X POST $BOT_WEBHOOK \ + -d '{"msg_type":"post","content":{"post":{"zh_cn":{"title":"Release:","content":[[{"tag":"text","text":"yes master, release "},{"tag":"a","text":"['$TAG_NAME']","href":"https://github.com/'$LATEST_REPO'/releases/tag/'$TAG_NAME'"},{"tag":"text","text":" is on its way..."}]]}}}}' +} + +send_message() { + if [[ "$TAG_NAME" != "v"*"."*"."* ]]; then + echo "invalid tag name" + return + fi + + if [[ "$CONTENT" == *"success" ]]; then + curl -H "Content-Type: application/json" -X POST $BOT_WEBHOOK \ + -d '{"msg_type":"post","content":{"post":{"zh_cn":{"title":"Success:","content":[[{"tag":"text","text":"'$CONTENT'"}]]}}}}' + else + curl -H "Content-Type: application/json" -X POST $BOT_WEBHOOK \ + -d '{"msg_type":"post","content":{"post":{"zh_cn":{"title":"Error:","content":[[{"tag":"a","text":"['$CONTENT']","href":"'$RUN_URL'"}]]}}}}' + fi +} + add_trigger_mode() { trigger_mode=$1 if [[ "$TRIGGER_MODE" != *"$trigger_mode"* ]]; then @@ -165,4 +375,44 @@ get_trigger_mode() { echo $TRIGGER_MODE } +check_package_version() { + exit_status=0 + beta_tag="v"*"."*"."*"-beta."* + rc_tag="v"*"."*"."*"-rc."* + release_tag="v"*"."*"."* + not_release_tag="v"*"."*"."*"-"* + if [[ "$TAG_NAME" == $release_tag && "$TAG_NAME" != $not_release_tag ]]; then + echo "::error title=Release Version Not Allow::$(tput -T xterm setaf 1) $TAG_NAME does not allow packaging.$(tput -T xterm sgr0)" + exit_status=1 + elif [[ "$TAG_NAME" == $beta_tag ]]; then + echo "::error title=Beta Version Not Allow::$(tput -T xterm setaf 1) $TAG_NAME does not allow packaging.$(tput -T xterm sgr0)" + exit_status=1 + elif [[ "$TAG_NAME" == $rc_tag ]]; then + echo "::error title=Release Candidate Version Not Allow::$(tput -T xterm setaf 1) $TAG_NAME does not allow packaging.$(tput -T xterm sgr0)" + exit_status=1 + else + echo "$(tput -T xterm setaf 2)Version allows packaging$(tput -T xterm sgr0)" + fi + exit $exit_status +} + +patch_release_notes() { + release_note="" + while read line; do + if [[ -z "${release_note}" ]]; then + release_note="$line" + else + release_note="$release_note\n$line" + fi + done < ${FILE} + + release_id=`gh_curl -s $GITHUB_API/repos/$GITHUB_REPO/releases/tags/$TAG_NAME | jq -r '.id'` + + curl -H "Authorization: token $GITHUB_TOKEN" \ + -H "Accept: application/vnd.github.v3.raw" \ + -X PATCH \ + $GITHUB_API/repos/$GITHUB_REPO/releases/$release_id \ + -d '{"body":"'"$release_note"'"}' +} + main "$@" diff --git a/.github/workflows/cicd-merge.yml b/.github/workflows/cicd-merge.yml index 9a6730bf7..fae19bc6e 100644 --- a/.github/workflows/cicd-merge.yml +++ b/.github/workflows/cicd-merge.yml @@ -23,7 +23,7 @@ jobs: - name: setup Go uses: actions/setup-go@v3 with: - go-version: '1.19' + go-version: '1.20' - name: start minikube run: | diff --git a/.github/workflows/cicd-pull-request.yml b/.github/workflows/cicd-pull-request.yml index c3122f053..a116ac466 100644 --- a/.github/workflows/cicd-pull-request.yml +++ b/.github/workflows/cicd-pull-request.yml @@ -8,7 +8,8 @@ env: GITLAB_GO_CACHE_PROJECT_ID: 98800 GO_CACHE: "go-cache" GO_CACHE_DIR: "/root/.cache" - + GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }} + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} jobs: trigger-mode: @@ -32,26 +33,11 @@ jobs: name: make test needs: trigger-mode if: contains(needs.trigger-mode.outputs.trigger-mode, '[test]') - runs-on: [ self-hosted, eks-fargate-runner ] + outputs: + runner-name: ${{ steps.get_runner_name.outputs.runner_name }} + runs-on: [ self-hosted, kubeblocks-runner ] steps: - uses: apecloud/checkout@main - - - name: Download ${{ env.GO_CACHE }} - run: | - bash .github/utils/release_gitlab.sh \ - --type 6 \ - --project-id ${{ env.GITLAB_GO_CACHE_PROJECT_ID }} \ - --tag-name ${{ env.GO_CACHE }} \ - --asset-name ${{ env.GO_CACHE }}.tar.gz \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} - - - name: Extract ${{ env.GO_CACHE }} - uses: a7ul/tar-action@v1.1.3 - with: - command: x - cwd: ${{ env.GO_CACHE_DIR }} - files: ${{ env.GO_CACHE }}.tar.gz - - name: make mod-vendor and lint run: | mkdir -p ./bin @@ -63,41 +49,68 @@ jobs: run: | make test + - name: kill kube-apiserver and etcd + id: get_runner_name + if: ${{ always() }} + run: | + echo runner_name=${RUNNER_NAME} >> $GITHUB_OUTPUT + bash .github/utils/utils.sh --type 8 + + remove-runner: + needs: [ trigger-mode, make-test ] + runs-on: ubuntu-latest + if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[test]') && always() }} + steps: + - uses: actions/checkout@v3 + - name: remove runner + run: | + bash .github/utils/utils.sh --type 9 \ + --github-token ${{ env.GITHUB_TOKEN }} \ + --runner-name ${{ needs.make-test.outputs.runner-name }} + check-image: name: check image needs: trigger-mode - if: needs.trigger-mode.outputs.trigger-mode == '[docker]' - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + if: contains(needs.trigger-mode.outputs.trigger-mode, '[docker]') + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "build-manager-image" IMG: "apecloud/kubeblocks" VERSION: "check" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + BUILDX_PLATFORMS: "linux/amd64" + SYNC_ENABLE: false + APECD_REF: "v0.1.0" secrets: inherit check-tools-image: name: check image needs: trigger-mode - if: needs.trigger-mode.outputs.trigger-mode == '[docker]' - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + if: contains(needs.trigger-mode.outputs.trigger-mode, '[docker]') + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "build-tools-image" IMG: "apecloud/kubeblocks-tools" VERSION: "check" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + BUILDX_PLATFORMS: "linux/amd64" + SYNC_ENABLE: false + APECD_REF: "v0.1.0" secrets: inherit check-helm: name: check helm needs: trigger-mode - if: needs.trigger-mode.outputs.trigger-mode == '[deploy]' - uses: apecloud/apecd/.github/workflows/release-charts.yml@v0.5.2 + if: contains(needs.trigger-mode.outputs.trigger-mode, '[deploy]') + uses: apecloud/apecloud-cd/.github/workflows/release-charts.yml@v0.1.0 with: MAKE_OPS: "bump-chart-ver" VERSION: "v0.4.0-check" CHART_NAME: "kubeblocks" CHART_DIR: "deploy/helm" PUSH_ENABLE: false + DEP_REPO: "helm dep update deploy/delphic" + APECD_REF: "v0.1.0" secrets: inherit diff --git a/.github/workflows/cicd-push.yml b/.github/workflows/cicd-push.yml index abfcf92c7..0f081c26a 100644 --- a/.github/workflows/cicd-push.yml +++ b/.github/workflows/cicd-push.yml @@ -10,9 +10,7 @@ on: env: GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} - GITLAB_GO_CACHE_PROJECT_ID: 98800 - GO_CACHE: "go-cache" - GO_CACHE_DIR: "/root/.cache" + GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }} jobs: @@ -23,7 +21,7 @@ jobs: steps: - uses: actions/checkout@v3 with: - fetch-depth: 2 + fetch-depth: 0 - name: Get trigger mode id: get_trigger_mode run: | @@ -31,6 +29,13 @@ jobs: echo $TRIGGER_MODE echo trigger_mode=$TRIGGER_MODE >> $GITHUB_OUTPUT + - name: merge releasing to release + if: ${{ startsWith(github.ref_name, 'releasing-') }} + run: | + git config user.name "$GITHUB_ACTOR" + git config user.email "$GITHUB_ACTOR@users.noreply.github.com" + bash .github/utils/merge_releasing_branch.sh + pre-push: needs: trigger-mode runs-on: ubuntu-latest @@ -52,7 +57,7 @@ jobs: - name: pcregrep Chinese run: | FILE_PATH=`git diff --name-only HEAD HEAD^` - + python ${{ github.workspace }}/.github/utils/pcregrep.py \ --source="${{ github.workspace }}/pcregrep.out" \ --filter="$FILE_PATH" @@ -80,7 +85,7 @@ jobs: - name: Setup Go uses: actions/setup-go@v3 with: - go-version: 1.19 + go-version: "1.20" - name: Check cli doc id: check-cli-doc @@ -104,28 +109,12 @@ jobs: make-test: needs: trigger-mode - runs-on: [self-hosted, eks-fargate-runner ] + runs-on: [self-hosted, kubeblocks-runner ] if: contains(needs.trigger-mode.outputs.trigger-mode, '[test]') + outputs: + runner-name: ${{ steps.get_runner_name.outputs.runner_name }} steps: - uses: apecloud/checkout@main - - name: Download ${{ env.GO_CACHE }} - if: ${{ github.ref_name != 'main' }} - run: | - bash .github/utils/release_gitlab.sh \ - --type 6 \ - --project-id ${{ env.GITLAB_GO_CACHE_PROJECT_ID }} \ - --tag-name ${{ env.GO_CACHE }} \ - --asset-name ${{ env.GO_CACHE }}.tar.gz \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} - - - name: Extract ${{ env.GO_CACHE }} - if: ${{ github.ref_name != 'main' }} - uses: a7ul/tar-action@v1.1.3 - with: - command: x - cwd: ${{ env.GO_CACHE_DIR }} - files: ${{ env.GO_CACHE }}.tar.gz - - name: make manifests check run: | mkdir -p ./bin @@ -157,69 +146,108 @@ jobs: name: codecov-report verbose: true - - name: Compress ${{ env.GO_CACHE }} - if: ${{ github.ref_name == 'main' }} - uses: a7ul/tar-action@v1.1.3 - with: - command: c - cwd: ${{ env.GO_CACHE_DIR }} - files: | - ./ - outPath: ${{ env.GO_CACHE }}.tar.gz - - - name: Upload ${{ env.GO_CACHE }} to gitlab - if: ${{ github.ref_name == 'main' }} + - name: kill kube-apiserver and etcd + id: get_runner_name + if: ${{ always() }} run: | - bash .github/utils/release_gitlab.sh \ - --type 5 \ - --project-id ${{ env.GITLAB_GO_CACHE_PROJECT_ID }} \ - --tag-name ${{ env.GO_CACHE }} \ - --asset-path ${{ env.GO_CACHE }}.tar.gz \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} + echo runner_name=${RUNNER_NAME} >> $GITHUB_OUTPUT + bash .github/utils/utils.sh --type 8 + + remove-runner: + needs: [ trigger-mode, make-test ] + runs-on: ubuntu-latest + if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[test]') && always() }} + steps: + - uses: actions/checkout@v3 + - name: remove runner + run: | + bash .github/utils/utils.sh --type 9 \ + --github-token ${{ env.GITHUB_TOKEN }} \ + --runner-name ${{ needs.make-test.outputs.runner-name }} check-image: needs: trigger-mode if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[docker]') && github.ref_name != 'main' }} - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "build-manager-image" IMG: "apecloud/kubeblocks" VERSION: "check" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + BUILDX_PLATFORMS: "linux/amd64" + SYNC_ENABLE: false + APECD_REF: "v0.1.0" secrets: inherit check-tools-image: - name: check image needs: trigger-mode if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[docker]') && github.ref_name != 'main' }} - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "build-tools-image" IMG: "apecloud/kubeblocks-tools" VERSION: "check" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + BUILDX_PLATFORMS: "linux/amd64" + SYNC_ENABLE: false + APECD_REF: "v0.1.0" secrets: inherit check-helm: needs: trigger-mode if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[deploy]') && github.ref_name != 'main' }} - uses: apecloud/apecd/.github/workflows/release-charts.yml@v0.5.2 + uses: apecloud/apecloud-cd/.github/workflows/release-charts.yml@v0.1.0 with: MAKE_OPS: "bump-chart-ver" VERSION: "v0.4.0-check" CHART_NAME: "kubeblocks" CHART_DIR: "deploy/helm" PUSH_ENABLE: false + DEP_REPO: "helm dep update deploy/delphic" + APECD_REF: "v0.1.0" secrets: inherit deploy-kubeblocks-io: needs: trigger-mode if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[docs]') && (github.ref_name == 'main' || startsWith(github.ref_name, 'release')) }} - uses: apecloud/apecd/.github/workflows/trigger-workflow.yml@v0.5.0 + uses: apecloud/apecloud-cd/.github/workflows/trigger-workflow.yml@v0.1.0 with: GITHUB_REPO: "apecloud/kubeblocks.io" BRANCH_NAME: "master" WORKFLOW_ID: "deploy.yml" + APECD_REF: "v0.1.0" secrets: inherit + + build-kbcli: + needs: trigger-mode + runs-on: ubuntu-latest + if: ${{ contains(needs.trigger-mode.outputs.trigger-mode, '[cli]') && github.ref_name != 'main' }} + strategy: + matrix: + os: [linux-amd64, linux-arm64, darwin-amd64, darwin-arm64, windows-amd64] + steps: + - uses: actions/checkout@v3 + - name: install lib + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends \ + libbtrfs-dev \ + libdevmapper-dev + + - name: Setup Go + uses: actions/setup-go@v3 + with: + go-version: "1.20" + + - name: make generate + run: make generate + + - name: build cli + run: | + CLI_OS_ARCH=`bash .github/utils/utils.sh \ + --tag-name ${{ matrix.os }} \ + --type 2` + + make bin/kbcli.$CLI_OS_ARCH diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 000000000..fb37e2f69 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,29 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + uses: contributor-assistant/github-action@v2.3.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://cla-assistant.io/apecloud/kubeblocks' + branch: 'main' + allowlist: bot* + diff --git a/.github/workflows/milestoneclose.yml b/.github/workflows/milestoneclose.yml index 627d5b8df..67bff0a77 100644 --- a/.github/workflows/milestoneclose.yml +++ b/.github/workflows/milestoneclose.yml @@ -7,7 +7,7 @@ on: env: GITHUB_TOKEN: ${{ secrets.KUBEBLOCKS_TOKEN }} REPO: kubeblocks - milestone: 4 + milestone: 5 ORGANIZATION: apecloud PROJECT_NUMBER: 2 diff --git a/.github/workflows/package-version.yml b/.github/workflows/package-version.yml new file mode 100644 index 000000000..5a7e10c37 --- /dev/null +++ b/.github/workflows/package-version.yml @@ -0,0 +1,54 @@ +name: PACKAGE-VERSION + +on: + workflow_dispatch: + inputs: + release_version: + description: 'The tag name of release' + required: true + default: '' + +run-name: ref_name:${{ github.ref_name }} release_version:${{ inputs.release_version }} + +env: + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + PACKAGE_BOT_WEBHOOK: ${{ secrets.PACKAGE_BOT_WEBHOOK }} + + +jobs: + package-version: + runs-on: ubuntu-latest + steps: + - name: checkout branch ${{ github.ref_name }} + uses: actions/checkout@v3 + + - name: package message + run: | + bash .github/utils/utils.sh --type 11 \ + --tag-name "${{ inputs.release_version }}" \ + --bot-webhook ${{ env.PACKAGE_BOT_WEBHOOK }} + + - name: package check + run: | + bash .github/utils/utils.sh --type 7 --tag-name "${{ inputs.release_version }}" + + - name: push tag + uses: mathieudutour/github-tag-action@v6.1 + with: + custom_tag: ${{ inputs.release_version }} + github_token: ${{ env.GITHUB_TOKEN }} + tag_prefix: "" + + send-message: + runs-on: ubuntu-latest + needs: package-version + if: ${{ failure() || cancelled() }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ inputs.release_version }} \ + --content "package\u00a0${{ inputs.release_version }}\u00a0error" \ + --bot-webhook ${{ env.PACKAGE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml index 8fcb706ba..055e956db 100644 --- a/.github/workflows/pull-request-check.yml +++ b/.github/workflows/pull-request-check.yml @@ -10,13 +10,14 @@ env: jobs: pr-check: name: PR Pre-Check + if: ${{ !(startsWith(github.head_ref, 'releasing-') && startsWith(github.base_ref, 'release-')) }} runs-on: ubuntu-latest steps: - name: check branch name uses: apecloud/check-branch-name@v0.1.0 with: - branch_pattern: 'feature/|bugfix/|release/|hotfix/|support/|dependabot/' - comment_for_invalid_branch_name: 'This branch name is not following the standards: feature/|bugfix/|release/|hotfix/|support/|dependabot/' + branch_pattern: 'feature/|bugfix/|release/|hotfix/|support/|releasing/|dependabot/' + comment_for_invalid_branch_name: 'This branch name is not following the standards: feature/|bugfix/|release/|hotfix/|support/|releasing/|dependabot/' fail_if_invalid_branch_name: 'true' ignore_branch_pattern: 'main|master' diff --git a/.github/workflows/release-create.yml b/.github/workflows/release-create.yml index c35f5ea56..32017c720 100644 --- a/.github/workflows/release-create.yml +++ b/.github/workflows/release-create.yml @@ -5,25 +5,36 @@ on: tags: - v* +env: + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} + jobs: publish: name: create a release runs-on: ubuntu-latest + outputs: + rel-version: ${{ steps.get_rel_version.outputs.rel_version }} steps: - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Parse release version and set REL_VERSION - run: python ./.github/utils/get_release_version.py - - name: release without release notes + id: get_rel_version + run: | + python ./.github/utils/is_rc_or_stable_release_version.py + echo rel_version=v${{ env.REL_VERSION }} >> $GITHUB_OUTPUT + + - name: release pre-release without release notes uses: softprops/action-gh-release@v1 if: not ${{ env.WITH_RELEASE_NOTES }} with: + # body_path: ./docs/release_notes/v${{ env.REL_VERSION }}/v${{ env.REL_VERSION }}.md token: ${{ secrets.PERSONAL_ACCESS_TOKEN }} name: KubeBlocks v${{ env.REL_VERSION }} generate_release_notes: true tag_name: v${{ env.REL_VERSION }} prerelease: true - - name: release with release notes + - name: release RC with release notes uses: softprops/action-gh-release@v1 if: ${{ env.WITH_RELEASE_NOTES }} with: @@ -32,3 +43,20 @@ jobs: name: KubeBlocks v${{ env.REL_VERSION }} tag_name: v${{ env.REL_VERSION }} prerelease: true + - name: sanitized release body + if: not ${{ env.WITH_RELEASE_NOTES }} + run: ./.github/utils/sanitize_release_body.sh + + send-message: + runs-on: ubuntu-latest + needs: publish + if: ${{ failure() || cancelled() }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ needs.publish.outputs.rel-version }} \ + --content "release\u00a0${{ needs.publish.outputs.rel-version }}\u00a0create\u00a0error"\ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ No newline at end of file diff --git a/.github/workflows/release-delete.yml b/.github/workflows/release-delete.yml new file mode 100644 index 000000000..2ac943825 --- /dev/null +++ b/.github/workflows/release-delete.yml @@ -0,0 +1,17 @@ +name: RELEASE-DELETE + +on: + workflow_dispatch: + inputs: + release-version: + description: 'The version of KubeBlocks release' + required: true + default: '' + +jobs: + delete-release: + uses: apecloud/apecloud-cd/.github/workflows/release-delete.yml@v0.1.0 + with: + VERSION: "${{ inputs.release-version }}" + APECD_REF: "v0.1.0" + secrets: inherit diff --git a/.github/workflows/release-helm-chart.yml b/.github/workflows/release-helm-chart.yml index c8c3fb38e..ee97fc6c8 100644 --- a/.github/workflows/release-helm-chart.yml +++ b/.github/workflows/release-helm-chart.yml @@ -13,6 +13,7 @@ on: env: RELEASE_VERSION: ${{ github.ref_name }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} jobs: @@ -32,13 +33,34 @@ jobs: release-chart: needs: chart-version - uses: apecloud/apecd/.github/workflows/release-charts.yml@v0.5.2 + uses: apecloud/apecloud-cd/.github/workflows/release-charts.yml@v0.1.0 with: MAKE_OPS: "bump-chart-ver" VERSION: "${{ needs.chart-version.outputs.chart-version }}" CHART_NAME: "kubeblocks" CHART_DIR: "deploy/helm" DEP_CHART_DIR: "deploy/helm/depend-charts" + DEP_REPO: "helm dep update deploy/delphic" + APECD_REF: "v0.1.0" secrets: inherit + send-message: + runs-on: ubuntu-latest + needs: release-chart + if: ${{ always() && github.event.action == 'published' }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + CONTENT="release\u00a0${{ env.RELEASE_VERSION }}\u00a0chart\u00a0error" + if [[ "${{ needs.release-chart.result }}" == "success" ]]; then + CONTENT="release\u00a0${{ env.RELEASE_VERSION }}\u00a0chart\u00a0success" + fi + + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ env.RELEASE_VERSION }} \ + --content "${CONTENT}"\ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" + diff --git a/.github/workflows/release-image.yml b/.github/workflows/release-image.yml index 2c8095b78..c37ef845b 100644 --- a/.github/workflows/release-image.yml +++ b/.github/workflows/release-image.yml @@ -15,6 +15,7 @@ on: env: RELEASE_VERSION: ${{ github.ref_name }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} jobs: @@ -38,22 +39,43 @@ jobs: release-image: needs: image-tag - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "push-manager-image" IMG: "apecloud/kubeblocks" VERSION: "${{ needs.image-tag.outputs.tag-name }}" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + APECD_REF: "v0.1.0" secrets: inherit release-tools-image: needs: image-tag - uses: apecloud/apecd/.github/workflows/release-image.yml@v0.2.0 + uses: apecloud/apecloud-cd/.github/workflows/release-image.yml@v0.1.0 with: MAKE_OPS_PRE: "generate" MAKE_OPS: "push-tools-image" IMG: "apecloud/kubeblocks-tools" VERSION: "${{ needs.image-tag.outputs.tag-name }}" - GO_VERSION: 1.19 + GO_VERSION: "1.20" + APECD_REF: "v0.1.0" secrets: inherit + + send-message: + runs-on: ubuntu-latest + needs: [ release-image, release-tools-image ] + if: ${{ always() && github.event.action == 'published' }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + CONTENT="release\u00a0${{ env.RELEASE_VERSION }}\u00a0image\u00a0error" + if [[ "${{ needs.release-image.result }}" == "success" && "${{ needs.release-tools-image.result }}" == "success" ]]; then + CONTENT="release\u00a0${{ env.RELEASE_VERSION }}\u00a0image\u00a0success" + fi + + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ env.RELEASE_VERSION }} \ + --content "${CONTENT}"\ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 6ad92b85e..57fc61328 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -8,10 +8,12 @@ on: env: GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} TAG_NAME: ${{ github.ref_name }} - GO_VERSION: '1.19' + GO_VERSION: "1.20" CLI_NAME: 'kbcli' CLI_REPO: 'apecloud/kbcli' GITLAB_KBCLI_PROJECT_ID: 85948 + GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} jobs: create-release-kbcli: @@ -34,8 +36,7 @@ jobs: --type 1 \ --project-id ${{ env.GITLAB_KBCLI_PROJECT_ID }} \ --tag-name ${{ env.TAG_NAME }} \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} - + --access-token ${{ env.GITLAB_ACCESS_TOKEN }} upload-release-assert: needs: create-release-kbcli @@ -67,7 +68,7 @@ jobs: uses: bruceadams/get-release@v1.3.2 - name: make build - run: | + run: | CLI_OS_ARCH=`bash ${{ github.workspace }}/.github/utils/utils.sh \ --tag-name ${{ matrix.os }} \ --type 2` @@ -79,16 +80,20 @@ jobs: echo "CLI_OS_ARCH=${CLI_OS_ARCH}" >> $GITHUB_ENV echo "CLI_FILENAME=${{ env.CLI_NAME }}-${{ matrix.os }}-${{ env.TAG_NAME }}" >> $GITHUB_ENV echo "CLI_DIR=${{ matrix.os }}" >> $GITHUB_ENV - + - name: make zip if: matrix.os == 'windows-amd64' run: | mv bin/${{ env.CLI_NAME }}.${{ env.CLI_OS_ARCH }} ${{ matrix.os }}/${{ env.CLI_NAME }}.exe + cp ${{ matrix.os }}/${{ env.CLI_NAME }}.exe bin/ zip -r -o ${{ env.CLI_FILENAME }}.zip ${{ env.CLI_DIR }} file ${{ env.CLI_FILENAME }}.zip # for debug + mkdir -p sha256 + shasum -a 256 ${{ env.CLI_FILENAME }}.zip > sha256/${{ env.CLI_FILENAME }}.zip.sha256.txt mv ${{ env.CLI_FILENAME }}.zip bin/ echo "ASSET_NAME=${{ env.CLI_FILENAME }}.zip" >> $GITHUB_ENV echo "ASSET_CONTENT_TYPE=application/zip" >> $GITHUB_ENV + echo "CheckSum=${{ env.CLI_FILENAME }}.zip.sha256.txt" >> $GITHUB_ENV - name: make tar if: matrix.os != 'windows-amd64' @@ -96,17 +101,24 @@ jobs: mv bin/${{ env.CLI_NAME }}.${{ env.CLI_OS_ARCH }} ${{ matrix.os }}/${{ env.CLI_NAME }} tar -zcvf ${{ env.CLI_FILENAME }}.tar.gz ${{ env.CLI_DIR }} file ${{ env.CLI_FILENAME }}.tar.gz # for debug + mkdir -p sha256 + shasum -a 256 ${{ env.CLI_FILENAME }}.tar.gz > sha256/${{ env.CLI_FILENAME }}.tar.gz.sha256.txt mv ${{ env.CLI_FILENAME }}.tar.gz bin/ echo "ASSET_NAME=${{ env.CLI_FILENAME }}.tar.gz" >> $GITHUB_ENV echo "ASSET_CONTENT_TYPE=application/gzip" >> $GITHUB_ENV + echo "CheckSum=${{ env.CLI_FILENAME }}.tar.gz.sha256.txt" >> $GITHUB_ENV - - name: upload release asset ${{ matrix.os }} - uses: actions/upload-release-asset@main - with: - upload_url: ${{ steps.get_release.outputs.upload_url }} - asset_path: ./bin/${{ env.ASSET_NAME }} - asset_name: ${{ env.ASSET_NAME }} - asset_content_type: ${{ env.ASSET_CONTENT_TYPE }} + - name: upload gitlab kbcli asset ${{ matrix.os }} + env: + CLI_BINARY: ${{ env.CLI_NAME }}-${{ matrix.os }}-${{ env.TAG_NAME }}.tar.gz + run: | + bash ${{ github.workspace }}/.github/utils/release_gitlab.sh \ + --type 2 \ + --project-id ${{ env.GITLAB_KBCLI_PROJECT_ID }} \ + --tag-name ${{ env.TAG_NAME }} \ + --asset-path ./bin/${{ env.ASSET_NAME }} \ + --asset-name ${{ env.ASSET_NAME }} \ + --access-token ${{ env.GITLAB_ACCESS_TOKEN }} - name: get release kbcli upload url run: | @@ -124,14 +136,69 @@ jobs: asset_name: ${{ env.ASSET_NAME }} asset_content_type: ${{ env.ASSET_CONTENT_TYPE }} - - name: upload gitlab kbcli asset ${{ matrix.os }} - env: - CLI_BINARY: ${{ env.CLI_NAME }}-${{ matrix.os }}-${{ env.TAG_NAME }}.tar.gz + - name: upload kbcli binary release for winget + if: matrix.os == 'windows-amd64' + uses: actions/upload-release-asset@main + with: + upload_url: ${{ env.UPLOAD_URL }} + asset_path: ./bin/${{ env.CLI_NAME }}.exe + asset_name: ${{ env.CLI_NAME }}.exe + asset_content_type: application/octet-stream + + - name: upload checksum for kbcli assets + uses: actions/upload-artifact@v3 + with: + name: sha256 + path: sha256/ + + - name: upload release asset ${{ matrix.os }} + continue-on-error: true + uses: actions/upload-release-asset@main + with: + upload_url: ${{ steps.get_release.outputs.upload_url }} + asset_path: ./bin/${{ env.ASSET_NAME }} + asset_name: ${{ env.ASSET_NAME }} + asset_content_type: ${{ env.ASSET_CONTENT_TYPE }} + + generate-kbcli-sha256: + runs-on: ubuntu-latest + needs: upload-release-assert + steps: + - uses: actions/checkout@v3 + - uses: actions/download-artifact@v3 + with: + name: sha256 + path: sha256 + - name: generate kbcli release-notes run: | - bash ${{ github.workspace }}/.github/utils/release_gitlab.sh \ - --type 2 \ - --project-id ${{ env.GITLAB_KBCLI_PROJECT_ID }} \ + mkdir -p ./docs/release_notes/${{ env.TAG_NAME }} + python ./.github/utils/generate_kbcli_sha256.py ${{ env.TAG_NAME }} sha256 + - name: release kbcli with release notes + run: | + bash .github/utils/utils.sh --type 13 \ + --github-repo 'apecloud/kbcli' \ --tag-name ${{ env.TAG_NAME }} \ - --asset-path ./bin/${{ env.ASSET_NAME }} \ - --asset-name ${{ env.ASSET_NAME }} \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} + --file "./docs/release_notes/${{ env.TAG_NAME }}/kbcli.md" \ + --github-token ${{ env.GITHUB_TOKEN }} + - name: remove artifact + uses: geekyeggo/delete-artifact@v2 + with: + name: sha256 + + send-message: + runs-on: ubuntu-latest + needs: upload-release-assert + if: ${{ always() }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + CONTENT="release\u00a0${{ env.TAG_NAME }}\u00a0kbcli\u00a0error" + if [[ "${{ needs.upload-release-assert.result }}" == "success" ]]; then + CONTENT="release\u00a0${{ env.TAG_NAME }}\u00a0kbcli\u00a0success" + fi + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ env.TAG_NAME }} \ + --content "${CONTENT}"\ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" diff --git a/.github/workflows/release-sync.yml b/.github/workflows/release-sync.yml index 91d3eee1a..137a8c141 100644 --- a/.github/workflows/release-sync.yml +++ b/.github/workflows/release-sync.yml @@ -8,6 +8,7 @@ on: env: CLI_REPO: 'apecloud/kbcli' GITLAB_KBCLI_PROJECT_ID: 85948 + GITLAB_ACCESS_TOKEN: ${{ secrets.GITLAB_ACCESS_TOKEN }} jobs: update-release-kbcli: @@ -34,16 +35,36 @@ jobs: --type 4 \ --tag-name $LATEST_RELEASE_TAG \ --project-id ${{ env.GITLAB_KBCLI_PROJECT_ID }} \ - --access-token ${{ secrets.GITLAB_ACCESS_TOKEN }} + --access-token ${{ env.GITLAB_ACCESS_TOKEN }} echo release_version=$LATEST_RELEASE_TAG >> $GITHUB_OUTPUT release-homebrew-tap: needs: update-release-kbcli - uses: apecloud/apecd/.github/workflows/trigger-workflow.yml@v0.5.1 + uses: apecloud/apecloud-cd/.github/workflows/trigger-workflow.yml@v0.1.0 with: GITHUB_REPO: "apecloud/homebrew-tap" WORKFLOW_ID: "release.yml" VERSION: "${{ needs.update-release-kbcli.outputs.release-version }}" + APECD_REF: "v0.1.0" secrets: inherit + release-winget-kbcli: + needs: update-release-kbcli + uses: apecloud/apecloud-cd/.github/workflows/trigger-workflow.yml@v0.1.0 + with: + GITHUB_REPO: "apecloud/apecloud-cd" + WORKFLOW_ID: "publish-kbcli-winget.yml" + VERSION: "${{ needs.update-release-kbcli.outputs.release-version }}" + APECD_REF: "v0.1.0" + secrets: inherit + + release-scoop-kbcli: + needs: update-release-kbcli + uses: apecloud/apecloud-cd/.github/workflows/trigger-workflow.yml@v0.1.0 + with: + GITHUB_REPO: "apecloud/apecloud-cd" + WORKFLOW_ID: "publish-kbcli-scoop.yml" + VERSION: "${{ needs.update-release-kbcli.outputs.release-version }}" + APECD_REF: "v0.1.0" + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/release-version.yml b/.github/workflows/release-version.yml index f08b4db5e..ca22e96d3 100644 --- a/.github/workflows/release-version.yml +++ b/.github/workflows/release-version.yml @@ -12,18 +12,83 @@ run-name: ref_name:${{ github.ref_name }} release_version:${{ inputs.release_ver env: GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} jobs: + merge-releasing-branch: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Auto merge releasing PR + run: ./.github/utils/merge_releasing_pr.sh + + release-message: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: release message + run: | + bash .github/utils/utils.sh --type 11 \ + --tag-name "${{ inputs.release_version }}" \ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} + + release-test: + needs: release-message + runs-on: [ self-hosted, kubeblocks-runner ] + outputs: + runner-name: ${{ steps.get_runner_name.outputs.runner_name }} + steps: + - uses: apecloud/checkout@main + - name: vendor lint test + run: | + mkdir -p ./bin + cp -r /go/bin/controller-gen ./bin/controller-gen + cp -r /go/bin/setup-envtest ./bin/setup-envtest + make mod-vendor lint test + + - name: kill kube-apiserver and etcd + id: get_runner_name + if: ${{ always() }} + run: | + echo runner_name=${RUNNER_NAME} >> $GITHUB_OUTPUT + bash .github/utils/utils.sh --type 8 + + remove-runner: + needs: release-test + runs-on: ubuntu-latest + if: ${{ always() }} + steps: + - uses: actions/checkout@v3 + - name: remove runner + run: | + bash .github/utils/utils.sh --type 9 \ + --github-token ${{ env.GITHUB_TOKEN }} \ + --runner-name ${{ needs.make-test.outputs.runner-name }} + release-version: + needs: release-test runs-on: ubuntu-latest steps: - name: checkout branch ${{ github.ref_name }} uses: actions/checkout@v3 - - name: push tag uses: mathieudutour/github-tag-action@v6.1 with: custom_tag: ${{ inputs.release_version }} github_token: ${{ env.GITHUB_TOKEN }} tag_prefix: "" + + send-message: + runs-on: ubuntu-latest + needs: release-version + if: ${{ failure() || cancelled() }} + steps: + - uses: actions/checkout@v3 + - name: send message + run: | + bash .github/utils/utils.sh --type 12 \ + --tag-name ${{ inputs.release_version }} \ + --content "release\u00a0${{ inputs.release_version }}\u00a0error"\ + --bot-webhook ${{ env.RELEASE_BOT_WEBHOOK }} \ + --run-url "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" \ No newline at end of file diff --git a/.github/workflows/trigger-release.yml b/.github/workflows/trigger-release.yml new file mode 100644 index 000000000..52e2e432e --- /dev/null +++ b/.github/workflows/trigger-release.yml @@ -0,0 +1,41 @@ +name: TRIGGER-RELEASE + +on: + workflow_dispatch: + inputs: + trigger-content: + description: 'the trigger request content' + required: false + default: '' + trigger-type: + description: 'the trigger type (e.g. release/package)' + required: false + default: 'release' + +run-name: ${{ inputs.trigger-type }}:${{ inputs.trigger-content }} + +env: + GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + PACKAGE_BOT_WEBHOOK: ${{ secrets.PACKAGE_BOT_WEBHOOK }} + RELEASE_BOT_WEBHOOK: ${{ secrets.RELEASE_BOT_WEBHOOK }} + + +jobs: + trigger-release: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: trigger release + id: get_release_version + run: | + BOT_WEBHOOK=${{ env.RELEASE_BOT_WEBHOOK }} + if [[ "${{ inputs.trigger-type }}" == "package" ]]; then + BOT_WEBHOOK=${{ env.PACKAGE_BOT_WEBHOOK }} + fi + RELEASE_VERSION=`bash .github/utils/utils.sh --type 10 \ + --tag-name "${{ vars.CURRENT_RELEASE_VERSION }}" \ + --branch-name "${{ vars.CURRENT_RELEASE_BRANCH }}" \ + --content '${{ inputs.trigger-content }}' \ + --trigger-type "${{ inputs.trigger-type }}" \ + --bot-webhook ${BOT_WEBHOOK} \ + --github-token ${{ env.GITHUB_TOKEN }}` diff --git a/.gitignore b/.gitignore index 137b97888..dc47aa0a3 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,10 @@ bin testbin/* __debug_bin +output +helm-output + + # Test binary, build with `go test -c` *.test diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 000000000..b96ea53d5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,127 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in the KubeBlocks project and our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at kubeblocks@apecloud.com. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. \ No newline at end of file diff --git a/OWNERS b/GOVERNANCE.md similarity index 100% rename from OWNERS rename to GOVERNANCE.md diff --git a/LICENSE b/LICENSE index d64569567..be3f7b28e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,202 +1,661 @@ - - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - 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. + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/LICENSING.md b/LICENSING.md new file mode 100644 index 000000000..1d712003c --- /dev/null +++ b/LICENSING.md @@ -0,0 +1,28 @@ +# Licensing + +License names used in this document are as per [SPDX License List](https://spdx.org/licenses/). + +The default license for this project is [AGPL-3.0-only](LICENSE). + +## Apache-2.0 + +The following directories and their subdirectories are licensed under Apache-2.0: + +``` +apis/** +deploy/** +docker/** +externalapis/** +hack/** +pkg/** +test/** +tools/** +``` + +The following directories and their subdirectories are licensed under their original upstream licenses: + +``` +cmd/probe/internal/component/ +internal/cli/cmd/plugin/download/** +vendor/ +``` diff --git a/MAINTAINERS.md b/MAINTAINERS.md new file mode 100644 index 000000000..3c5e90305 --- /dev/null +++ b/MAINTAINERS.md @@ -0,0 +1,21 @@ +# KubeBlocks Maintainers + +[GOVERNANCE.md](https://github.com/apecloud/kubeblocks/blob/main/GOVERNANCE.md) describes governance guidelines and maintainer responsibilities. + +## Maintainers + +| Maintainer | GitHub ID | Affiliation | +|-------------|-----------------------------------------|-------------------------------------------| +| Nayutah | [nayutah](https://github.com/nayutah) | [ApeCloud](https://github.com/apecloud/) | +| Nash Tsai | [nashtsai](https://github.com/nashtsai) | [ApeCloud](https://github.com/apecloud/) | + + +## KubeBlocks Contributors & Stakeholders + +| Feature Area | Lead | +|------------------------|:-------------------------------------------------:| +| Architect | Nash Tsai [nashtsai](https://github.com/nashtsai) | +| Technical Lead | | +| Deployment | Yijing [ahjing99](https://github.com/ahjing99) | +| Community Management | | +| Product Management | | diff --git a/Makefile b/Makefile index 781fab822..387b40a13 100644 --- a/Makefile +++ b/Makefile @@ -33,10 +33,10 @@ SKIP_GO_GEN ?= true CHART_PATH = deploy/helm WEBHOOK_CERT_DIR ?= /tmp/k8s-webhook-server/serving-certs + + # Go setup export GO111MODULE = auto -# export GOPROXY = https://proxy.golang.org -export GOPROXY = https://goproxy.cn export GOSUMDB = sum.golang.org export GONOPROXY = github.com/apecloud export GONOSUMDB = github.com/apecloud @@ -51,6 +51,15 @@ GOBIN=$(shell $(GO) env GOPATH)/bin else GOBIN=$(shell $(GO) env GOBIN) endif +GOPROXY := $(shell go env GOPROXY) +ifeq ($(GOPROXY),) +GOPROXY := https://proxy.golang.org +## use following GOPROXY settings for Chinese mainland developers. +#GOPROXY := https://goproxy.cn +endif +export GOPROXY + + LD_FLAGS="-s -w -X main.version=v${VERSION} -X main.buildDate=`date -u +'%Y-%m-%dT%H:%M:%SZ'` -X main.gitCommit=`git rev-parse HEAD`" # Which architecture to build - see $(ALL_ARCH) for options. # if the 'local' rule is being run, detect the ARCH from 'go env' @@ -58,18 +67,15 @@ LD_FLAGS="-s -w -X main.version=v${VERSION} -X main.buildDate=`date -u +'%Y-%m-% local : ARCH ?= $(shell go env GOOS)-$(shell go env GOARCH) ARCH ?= linux-amd64 -# docker build setup -# BUILDX_PLATFORMS ?= $(subst -,/,$(ARCH)) -BUILDX_PLATFORMS ?= linux/amd64,linux/arm64 -BUILDX_OUTPUT_TYPE ?= docker + TAG_LATEST ?= false -BUILDX_ENABLED ?= false -ifneq ($(BUILDX_ENABLED), false) +BUILDX_ENABLED ?= "" +ifeq ($(BUILDX_ENABLED), "") ifeq ($(shell docker buildx inspect 2>/dev/null | awk '/Status/ { print $$2 }'), running) - BUILDX_ENABLED ?= true + BUILDX_ENABLED = true else - BUILDX_ENABLED ?= false + BUILDX_ENABLED = false endif endif @@ -136,8 +142,6 @@ ifeq ($(SKIP_GO_GEN), false) $(GO) generate -x ./internal/configuration/proto endif - - .PHONY: test-go-generate test-go-generate: ## Run go generate against test code. $(GO) generate -x ./internal/testutil/k8s/mocks/... @@ -182,7 +186,7 @@ mod-vendor: module ## Run go mod vendor against go modules. .PHONY: module module: ## Run go mod tidy->verify against go modules. - $(GO) mod tidy -compat=1.19 + $(GO) mod tidy -compat=1.20 $(GO) mod verify TEST_PACKAGES ?= ./internal/... ./apis/... ./controllers/... ./cmd/... @@ -207,6 +211,10 @@ test-fast: envtest .PHONY: test test: manifests generate test-go-generate fmt vet add-k8s-host test-fast ## Run tests. if existing k8s cluster is k3d or minikube, specify EXISTING_CLUSTER_TYPE. +.PHONY: race +race: + KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GO) test -race $(TEST_PACKAGES) + .PHONY: test-integration test-integration: manifests generate fmt vet envtest add-k8s-host ## Run tests. if existing k8s cluster is k3d or minikube, specify EXISTING_CLUSTER_TYPE. KUBEBUILDER_ASSETS="$(shell $(ENVTEST) use $(ENVTEST_K8S_VERSION) -p path)" $(GO) test ./test/integration @@ -247,7 +255,7 @@ CLI_LD_FLAGS ="-s -w \ -X github.com/apecloud/kubeblocks/version.K3dVersion=$(K3D_VERSION) \ -X github.com/apecloud/kubeblocks/version.DefaultKubeBlocksVersion=$(VERSION)" -bin/kbcli.%: ## Cross build bin/kbcli.$(OS).$(ARCH). +bin/kbcli.%: test-go-generate ## Cross build bin/kbcli.$(OS).$(ARCH). GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) CGO_ENABLED=0 $(GO) build -ldflags=${CLI_LD_FLAGS} -o $@ cmd/cli/main.go .PHONY: kbcli-fast @@ -264,11 +272,17 @@ kbcli: test-go-generate build-checks kbcli-fast ## Build bin/kbcli. clean-kbcli: ## Clean bin/kbcli*. rm -f bin/kbcli* -.PHONY: doc -kbcli-doc: generate ## generate CLI command reference manual. +.PHONY: kbcli-doc +kbcli-doc: generate test-go-generate ## generate CLI command reference manual. $(GO) run ./hack/docgen/cli/main.go ./docs/user_docs/cli + +.PHONY: api-doc +api-doc: ## generate API reference manual. + @./hack/docgen/api/generate.sh + + ##@ Operator Controller Manager .PHONY: manager @@ -333,7 +347,7 @@ undeploy: ## Undeploy controller from the K8s cluster specified in ~/.kube/confi .PHONY: reviewable reviewable: generate build-checks test check-license-header ## Run code checks to proceed with PR reviews. - $(GO) mod tidy -compat=1.19 + $(GO) mod tidy -compat=1.20 .PHONY: check-diff check-diff: reviewable ## Run git code diff checker. @@ -388,6 +402,7 @@ bump-chart-ver: \ bump-single-chart-ver.redis \ bump-single-chart-ver.redis-cluster \ bump-single-chart-ver.milvus \ + bump-single-chart-ver.milvus-cluster \ bump-single-chart-ver.qdrant \ bump-single-chart-ver.qdrant-cluster \ bump-single-chart-ver.weaviate \ @@ -746,14 +761,14 @@ endif render-smoke-testdata-manifests: ## Update E2E test dataset $(HELM) template mycluster deploy/apecloud-mysql-cluster > test/e2e/testdata/smoketest/wesql/00_wesqlcluster.yaml $(HELM) template mycluster deploy/postgresql-cluster > test/e2e/testdata/smoketest/postgresql/00_postgresqlcluster.yaml - $(HELM) template mycluster deploy/redis > test/e2e/testdata/smoketest/redis/00_rediscluster.yaml - $(HELM) template mycluster deploy/redis-cluster >> test/e2e/testdata/smoketest/redis/00_rediscluster.yaml + $(HELM) template mycluster deploy/redis-cluster > test/e2e/testdata/smoketest/redis/00_rediscluster.yaml + $(HELM) template mycluster deploy/mongodb-cluster > test/e2e/testdata/smoketest/mongodb/00_mongodbcluster.yaml + .PHONY: test-e2e test-e2e: helm-package render-smoke-testdata-manifests ## Run E2E tests. - $(MAKE) -e VERSION=$(VERSION) -C test/e2e run + $(MAKE) -e VERSION=$(VERSION) PROVIDER=$(PROVIDER) REGION=$(REGION) SECRET_ID=$(SECRET_ID) SECRET_KEY=$(SECRET_KEY) -C test/e2e run # NOTE: include must be placed at the end include docker/docker.mk include cmd/cmd.mk - diff --git a/NOTICE b/NOTICE new file mode 100644 index 000000000..6951952b0 --- /dev/null +++ b/NOTICE @@ -0,0 +1,8 @@ +KubeBlocks Project, (C) 2022-2023 ApeCloud Co., Ltd. + +This product includes software developed at ApeCloud Co., Ltd. + +The KubeBlocks project contains unmodified/modified subcomponents too with +separate copyright notices and license terms. Your use of the source +code for these subcomponents is subject to the terms and conditions +of GNU Affero General Public License 3.0. \ No newline at end of file diff --git a/PROJECT b/PROJECT index 847d7728e..1d0d5e579 100644 --- a/PROJECT +++ b/PROJECT @@ -128,7 +128,28 @@ resources: namespaced: true domain: kubeblocks.io group: apps - kind: ClassFamily + kind: ComponentResourceConstraint path: github.com/apecloud/kubeblocks/apis/apps/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + domain: kubeblocks.io + group: apps + kind: ComponentClassDefinition + path: github.com/apecloud/kubeblocks/apis/apps/v1alpha1 + version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: kubeblocks.io + group: workloads + kind: ConsensusSet + path: github.com/apecloud/kubeblocks/apis/workloads/v1alpha1 + version: v1alpha1 + webhooks: + defaulting: true + validation: true + webhookVersion: v1 version: "3" diff --git a/README.md b/README.md index a1302bb0a..00ea4bf26 100644 --- a/README.md +++ b/README.md @@ -9,30 +9,23 @@ [![TODOs](https://img.shields.io/endpoint?url=https://api.tickgit.com/badge?repo=github.com/apecloud/kubeblocks)](https://www.tickgit.com/browse?repo=github.com/apecloud/kubeblocks) [![Artifact HUB](https://img.shields.io/endpoint?url=https://artifacthub.io/badge/repository/apecloud)](https://artifacthub.io/packages/search?repo=apecloud) - -![image](./docs/img/banner_website_version.png) - - +![image](./docs/img/banner-readme.png) - [KubeBlocks](#kubeblocks) - [What is KubeBlocks](#what-is-kubeblocks) + - [Why you need KubeBlocks](#why-you-need-kubeblocks) - [Goals](#goals) - - [Key Features](#key-features) - - [Documents](#documents) - - [Quick start with KubeBlocks](#quick-start-with-kubeblocks) - - [Introduction](#introduction) - - [Installation](#installation) - - [User documents](#user-documents) - - [Design proposal](#design-proposal) + - [Key features](#key-features) + - [Get started with KubeBlocks](#get-started-with-kubeblocks) - [Community](#community) - [Contributing to KubeBlocks](#contributing-to-kubeblocks) - [License](#license) - ## What is KubeBlocks -KubeBlocks is an open-source tool designed to help developers and platform engineers build and manage stateful workloads, such as databases and analytics, on Kubernetes. It is cloud-neutral and supports multiple public cloud providers, providing a unified and declarative approach to increase productivity in DevOps practices. -The name KubeBlocks is derived from Kubernetes and building blocks, which indicates that standardizing databases and analytics on Kubernetes can be both productive and enjoyable, like playing with construction toys. KubeBlocks combines the large-scale production experiences of top public cloud providers with enhanced usability and stability. +KubeBlocks is an open-source, cloud-native data infrastructure designed to help application developers and platform engineers manage database and analytical workloads on Kubernetes. It is cloud-neutral and supports multiple cloud service providers, offering a unified and declarative approach to increase productivity in DevOps practices. + +The name KubeBlocks is derived from Kubernetes and LEGO blocks, which indicates that building database and analytical workloads on Kubernetes can be both productive and enjoyable, like playing with construction toys. KubeBlocks combines the large-scale production experiences of top cloud service providers with enhanced usability and stability. ### Why you need KubeBlocks @@ -41,50 +34,42 @@ Kubernetes has become the de facto standard for container orchestration. It mana To address these challenges, and solve the problem of complexity, KubeBlocks introduces ReplicationSet and ConsensusSet, with the following capabilities: - Role-based update order reduces downtime caused by upgrading versions, scaling, and rebooting. -- Latency-based election weight reduces the possibility of related workloads or components being located in different available zones. - Maintains the status of data replication and automatically repairs replication errors or delays. ### Goals -- Enhance stateful applications control plane manageability on Kubernetes clusters, being open sourced and cloud neutral -- Manage data platforms without a high cognitive load of cloud computing, Kubernetes, and database knowledge -- Be community-driven, embracing extensibility, and providing domain functions without vendor lock-in -- Reduce costs by only paying for the infrastructure and increasing the utilization of resources with flexible scheduling -- Support the most popular databases, analytical software, and their bundled tools -- Provide the most advanced user experience based on the concepts of IaC and GitOps + +- Enhance stateful workloads on Kubernetes, being open-source and cloud-neutral. +- Manage data infrastructure without a high cognitive load of cloud computing, Kubernetes, and database knowledge. +- Reduce costs by only paying for the infrastructure and increasing the utilization of resources with flexible scheduling. +- Support the most popular RDBMS, NoSQL, streaming and analytical systems, and their bundled tools. +- Provide the most advanced user experience based on the concepts of IaC and GitOps. ### Key features -- Kubernetes-native and multi-cloud supported. -- Supports multiple database engines, including MySQL, PostgreSQL, Redis, MongoDB, and more. + +- Be compatible with AWS, GCP, Azure, and Alibaba Cloud. +- Supports MySQL, PostgreSQL, Redis, MongoDB, Kafka, and more. - Provides production-level performance, resilience, scalability, and observability. - Simplifies day-2 operations, such as upgrading, scaling, monitoring, backup, and restore. -- Declarative configuration is made simple, and imperative commands are made powerful. -- The learning curve is flat, and you are welcome to submit new issues on GitHub. +- Contains a powerful and intuitive command line tool. +- Sets up a full-stack, production-ready data infrastructure in minutes. +## Get started with KubeBlocks -For detailed feature information, see [Feature list](https://github.com/apecloud/kubeblocks/blob/support/rewrite_kb_introduction/docs/user_docs/Introduction/feature_list.md) - -## Documents -### Quick start with KubeBlocks -[Quick Start](docs/user_docs/quick_start_guide.md) shows you the quickest way to get started with KubeBlocks. -### Introduction -[Introduction](docs/user_docs/introduction/introduction.md) is a detailed information on KubeBlocks. -### Installation -[Installation](docs/user_docs/installation) document for install KubeBlocks, playground, kbctl, and create database clusters. -### User documents -[User documents](docs/user_docs) for instruction to use KubeBlocks. -### Design proposal -[Design proposal](docs/design_docs) for design motivation and methodology. +[Quick Start](./docs/user_docs/quick-start/) shows you the quickest way to get started with KubeBlocks. ## Community + - KubeBlocks [Slack Channel](https://kubeblocks.slack.com/ssb/redirect) - KubeBlocks Github [Discussions](https://github.com/apecloud/kubeblocks/discussions) -- Questions tagged [#KubeBlocks](https://stackoverflow.com/questions/tagged/KubeBlocks) on StackOverflow -- Follow us on Twitter [@KubeBlocks](https://twitter.com/KubeBlocks) + ## Contributing to KubeBlocks -Your contributions and suggestions are welcomed and appreciated. -- See the [Contributing Guide](docs/CONTRIBUTING.md) for details on typical contribution workflows. -- See the [Development Guide](docs/DEVELOPING.md) to get started with building and developing. -- See the [Docs Contributing Guide](docs/CONTRIBUTING_DOCS.md) to get started with contributing to the KubeBlocks docs. + +Your contributions are welcomed and appreciated. + +- See the [Contributor Guide](docs/CONTRIBUTING.md) for details on typical contribution workflows. +- See the [Developer Guide](docs/DEVELOPING.md) to get started with building and developing. ## License -KubeBlocks is under the Apache 2.0 license. See the [LICENSE](./LICENSE) file for details. + +KubeBlocks is under the GNU Affero General Public License v3.0. +See the [LICENSE](./LICENSE) file for details. diff --git a/SECURITY.md b/SECURITY.md index 04e907afa..35310aba0 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1 +1,62 @@ -# Security Policy \ No newline at end of file +# KubeBlocks Security Policy + +## Introduction + +This document outlines the security policy for the KubeBlocks project, an open-source tool for building and managing stateful workloads, such as databases and analytics, on Kubernetes. The purpose of this policy is to establish guidelines and best practices to ensure the security of the KubeBlocks project, its users, and the environments in which it is deployed. + +## Scope + +This security policy applies to all contributors, maintainers, users, and any third-party services utilized by the KubeBlocks project. It covers the project's source code, documentation, infrastructure, and any other resources related to the project. + +## Objectives + +The primary objectives of this security policy are to: + +1. Protect the confidentiality, integrity, and availability of the KubeBlocks project and its resources. +2. Establish and maintain a secure environment for users to deploy and manage stateful workloads on Kubernetes. +3. Promote a culture of security awareness and best practices among KubeBlocks contributors and users. + +## Security Best Practices + +### Code and Dependency Management + +1. All contributors must follow secure coding practices, such as input validation, output encoding, and proper error handling. +2. Use static code analysis tools and integrate them into the project's CI/CD pipeline to identify and fix potential security issues before they are merged into the main branch. +3. Regularly update project dependencies to ensure that known security vulnerabilities are addressed promptly. + +### Access Control + +1. Implement role-based access control (RBAC) to restrict access to KubeBlocks resources based on the user's role and the principle of least privilege. +2. Ensure that all actions performed within the KubeBlocks environment are logged and monitored for unauthorized access or suspicious activity. +3. Implement strong authentication and authorization mechanisms, such as multi-factor authentication (MFA), to protect access to critical resources. + +### Data Protection + +1. Ensure that sensitive data, such as credentials, API keys, and tokens, are securely stored and managed, using encryption and secret management tools. +2. Implement proper data backup and recovery mechanisms to protect against data loss, corruption, or unauthorized access. +3. Provide guidelines for users to secure their own data and workloads within the KubeBlocks environment. + +### Incident Response + +1. Develop and maintain an incident response plan to address potential security breaches and incidents promptly and effectively. +2. Regularly review and update the incident response plan to ensure its effectiveness and alignment with the evolving threat landscape. +3. Communicate security incidents to affected users and stakeholders, as required by law and industry best practices. + +### Security Awareness and Training + +1. Encourage a security-conscious culture among KubeBlocks contributors and users through regular security training and awareness programs. +2. Collaborate with the open-source community to share security best practices and learn from the experiences of other projects. +3. Provide clear and concise documentation to guide users in securely deploying and managing KubeBlocks in their environments. + +## Reporting Security Issues + +Security is of the highest importance and all security vulnerabilities or suspected security vulnerabilities should be reported to KubeBlocks privately, to minimize attacks against current users of KubeBlocks before they are fixed. Vulnerabilities will be investigated and patched on the next patch (or minor) release as soon as possible. This information could be kept entirely internal to the project. + +**IMPORTANT: Please do not disclose security vulnerabilities publicly until the KubeBlocks security team has had a reasonable amount of time to address the issue.** + +If you discover a security vulnerability or have concerns about the security of the KubeBlocks project, please report the issue by emailing the KubeBlocks security team at [kubeblocks@apecloud.com](mailto:kubeblocks@apecloud.com). The team will work with you to address the issue and provide appropriate credit for your contributions. + + +## Policy Review and Updates + +This security policy will be reviewed and updated periodically to ensure its continued effectiveness and alignment with industry best practices and regulatory requirements. All updates will be communicated to KubeBlocks contributors and users through appropriate channels. diff --git a/TODO.md b/TODO.md deleted file mode 100644 index 480a6fe53..000000000 --- a/TODO.md +++ /dev/null @@ -1,34 +0,0 @@ -# TODO items - -## KubeBlocks controllers - -### Cluster CR controller -- [x] secondary resources finalizer -- [x] CR delete handling - - [x] delete secondary resources - - [x] CR spec.terminationPolicy handling -- [x] managed resources handling - - [x] deployment workloads - - [x] PDB - - [x] label handling: - - [x] deploy & sts workloads' labels and spec.template.metadata.labels (check https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/) -- [x] immutable spec properties handling (via validating webhook) -- [x] CR status handling -- [x] checked ClusterVersion CR status -- [x] checked ClusterDefinition CR status -- [x] CR update handling - - [x] PVC volume expansion (spec.components[].volumeClaimTemplates only works for initial statefulset creation) - - [x] spec.components[].serviceType -- [x] merge components from all the CRs - -### ClusterDefinition CR controller -- [x] track changes and update associated CRs (Cluster, ClusterVersion) status -- [x] cannot delete ClusterDefinition CR if any referencing CRs (Cluster, ClusterVersion) - -### ClusterVersion CR controller -- [x] immutable spec handling (via validating webhook) -- [x] CR status handling -- [x] cannot delete ClusterVersion CR if any referencing CRs (Cluster) - -### Test -- [x] unit test \ No newline at end of file diff --git a/apis/apps/v1alpha1/backuppolicytemplate_types.go b/apis/apps/v1alpha1/backuppolicytemplate_types.go new file mode 100644 index 000000000..eacc331f2 --- /dev/null +++ b/apis/apps/v1alpha1/backuppolicytemplate_types.go @@ -0,0 +1,254 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +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" +) + +// BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate +type BackupPolicyTemplateSpec struct { + // clusterDefinitionRef references ClusterDefinition name, this is an immutable attribute. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + ClusterDefRef string `json:"clusterDefinitionRef"` + + // backupPolicies is a list of backup policy template for the specified componentDefinition. + // +patchMergeKey=componentDefRef + // +patchStrategy=merge,retainKeys + // +listType=map + // +listMapKey=componentDefRef + // +kubebuilder:validation:Required + // +kubebuilder:validation:MinItems=1 + BackupPolicies []BackupPolicy `json:"backupPolicies"` + + // Identifier is a unique identifier for this BackupPolicyTemplate. + // this identifier will be the suffix of the automatically generated backupPolicy name. + // and must be added when multiple BackupPolicyTemplates exist, + // otherwise the generated backupPolicy override will occur. + // +optional + // +kubebuilder:validation:MaxLength=20 + Identifier string `json:"identifier,omitempty"` +} + +type BackupPolicy struct { + // componentDefRef references componentDef defined in ClusterDefinition spec. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + ComponentDefRef string `json:"componentDefRef"` + + // retention describe how long the Backup should be retained. if not set, will be retained forever. + // +optional + Retention *RetentionSpec `json:"retention,omitempty"` + + // schedule policy for backup. + // +optional + Schedule Schedule `json:"schedule,omitempty"` + + // the policy for snapshot backup. + // +optional + Snapshot *SnapshotPolicy `json:"snapshot,omitempty"` + + // the policy for datafile backup. + // +optional + Datafile *CommonBackupPolicy `json:"datafile,omitempty"` + + // the policy for logfile backup. + // +optional + Logfile *CommonBackupPolicy `json:"logfile,omitempty"` +} + +type RetentionSpec struct { + // ttl is a time string ending with the 'd'|'D'|'h'|'H' character to describe how long + // the Backup should be retained. if not set, will be retained forever. + // +kubebuilder:validation:Pattern:=`^\d+[d|D|h|H]$` + // +optional + TTL *string `json:"ttl,omitempty"` +} + +type Schedule struct { + // schedule policy for snapshot backup. + // +optional + Snapshot *SchedulePolicy `json:"snapshot,omitempty"` + + // schedule policy for datafile backup. + // +optional + Datafile *SchedulePolicy `json:"datafile,omitempty"` + + // schedule policy for logfile backup. + // +optional + Logfile *SchedulePolicy `json:"logfile,omitempty"` +} + +type SchedulePolicy struct { + // the cron expression for schedule, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron. + // +kubebuilder:validation:Required + CronExpression string `json:"cronExpression"` + + // enable or disable the schedule. + // +kubebuilder:validation:Required + Enable bool `json:"enable"` +} + +type SnapshotPolicy struct { + BasePolicy `json:",inline"` + + // execute hook commands for backup. + // +optional + Hooks *BackupPolicyHook `json:"hooks,omitempty"` +} + +type CommonBackupPolicy struct { + BasePolicy `json:",inline"` + + // which backup tool to perform database backup, only support one tool. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + BackupToolName string `json:"backupToolName,omitempty"` +} + +type BasePolicy struct { + // target instance for backup. + // +optional + Target TargetInstance `json:"target"` + + // the number of automatic backups to retain. Value must be non-negative integer. + // 0 means NO limit on the number of backups. + // +kubebuilder:default=7 + // +optional + BackupsHistoryLimit int32 `json:"backupsHistoryLimit,omitempty"` + + // count of backup stop retries on fail. + // +optional + OnFailAttempted int32 `json:"onFailAttempted,omitempty"` + + // define how to update metadata for backup status. + // +optional + BackupStatusUpdates []BackupStatusUpdate `json:"backupStatusUpdates,omitempty"` +} + +type TargetInstance struct { + // select instance of corresponding role for backup, role are: + // - the name of Leader/Follower/Leaner for Consensus component. + // - primary or secondary for Replication component. + // finally, invalid role of the component will be ignored. + // such as if workload type is Replication and component's replicas is 1, + // the secondary role is invalid. and it also will be ignored when component is Stateful/Stateless. + // the role will be transformed to a role LabelSelector for BackupPolicy's target attribute. + // +optional + Role string `json:"role"` + + // refer to spec.componentDef.systemAccounts.accounts[*].name in ClusterDefinition. + // the secret created by this account will be used to connect the database. + // if not set, the secret created by spec.ConnectionCredential of the ClusterDefinition will be used. + // it will be transformed to a secret for BackupPolicy's target secret. + // +optional + Account string `json:"account,omitempty"` + + // connectionCredentialKey defines connection credential key in secret + // which created by spec.ConnectionCredential of the ClusterDefinition. + // it will be ignored when "account" is set. + ConnectionCredentialKey ConnectionCredentialKey `json:"connectionCredentialKey,omitempty"` +} + +type ConnectionCredentialKey struct { + // the key of password in the ConnectionCredential secret. + // if not set, the default key is "password". + // +optional + PasswordKey *string `json:"passwordKey,omitempty"` + + // the key of username in the ConnectionCredential secret. + // if not set, the default key is "username". + // +optional + UsernameKey *string `json:"usernameKey,omitempty"` +} + +// BackupPolicyHook defines for the database execute commands before and after backup. +type BackupPolicyHook struct { + // pre backup to perform commands + // +optional + PreCommands []string `json:"preCommands,omitempty"` + + // post backup to perform commands + // +optional + PostCommands []string `json:"postCommands,omitempty"` + + // exec command with image + // +optional + Image string `json:"image,omitempty"` + + // which container can exec command + // +optional + ContainerName string `json:"containerName,omitempty"` +} + +type BackupStatusUpdate struct { + // specify the json path of backup object for patch. + // example: manifests.backupLog -- means patch the backup json path of status.manifests.backupLog. + // +optional + Path string `json:"path,omitempty"` + + // which container name that kubectl can execute. + // +optional + ContainerName string `json:"containerName,omitempty"` + + // the shell Script commands to collect backup status metadata. + // The script must exist in the container of ContainerName and the output format must be set to JSON. + // Note that outputting to stderr may cause the result format to not be in JSON. + // +optional + Script string `json:"script,omitempty"` + + // when to update the backup status, pre: before backup, post: after backup + // +optional + UpdateStage BackupStatusUpdateStage `json:"updateStage,omitempty"` +} + +// BackupPolicyTemplateStatus defines the observed state of BackupPolicyTemplate +type BackupPolicyTemplateStatus struct { +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={kubeblocks},scope=Cluster,shortName=bpt +// +kubebuilder:printcolumn:name="CLUSTER-DEFINITION",type="string",JSONPath=".spec.clusterDefinitionRef",description="ClusterDefinition referenced by cluster." +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// BackupPolicyTemplate is the Schema for the BackupPolicyTemplates API (defined by provider) +type BackupPolicyTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec BackupPolicyTemplateSpec `json:"spec,omitempty"` + Status BackupPolicyTemplateStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// BackupPolicyTemplateList contains a list of BackupPolicyTemplate +type BackupPolicyTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []BackupPolicyTemplate `json:"items"` +} + +func init() { + SchemeBuilder.Register(&BackupPolicyTemplate{}, &BackupPolicyTemplateList{}) +} diff --git a/apis/apps/v1alpha1/classfamily_types_test.go b/apis/apps/v1alpha1/classfamily_types_test.go deleted file mode 100644 index be491451d..000000000 --- a/apis/apps/v1alpha1/classfamily_types_test.go +++ /dev/null @@ -1,83 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 ( - "testing" - - "github.com/stretchr/testify/assert" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/apimachinery/pkg/util/yaml" -) - -const classFamilyBytes = ` -# API scope: cluster -# ClusterClassFamily -apiVersion: "apps.kubeblocks.io/v1alpha1" -kind: "ClassFamily" -metadata: - name: kb-class-family-general -spec: - models: - - cpu: - min: 0.5 - max: 128 - step: 0.5 - memory: - sizePerCPU: 4Gi - - cpu: - slots: [0.1, 0.2, 0.4, 0.6, 0.8, 1] - memory: - minPerCPU: 200Mi - - cpu: - min: 0.1 - max: 64 - step: 0.1 - memory: - minPerCPU: 4Gi - maxPerCPU: 8Gi -` - -func TestClassFamily_ValidateResourceRequirements(t *testing.T) { - var cf ClassFamily - err := yaml.Unmarshal([]byte(classFamilyBytes), &cf) - if err != nil { - panic("Failed to unmarshal class family: %v" + err.Error()) - } - cases := []struct { - cpu string - memory string - expect bool - }{ - {cpu: "0.5", memory: "2Gi", expect: true}, - {cpu: "0.2", memory: "40Mi", expect: true}, - {cpu: "1", memory: "6Gi", expect: true}, - {cpu: "2", memory: "20Gi", expect: false}, - {cpu: "2", memory: "6Gi", expect: false}, - } - - for _, item := range cases { - requirements := &corev1.ResourceRequirements{ - Requests: map[corev1.ResourceName]resource.Quantity{ - corev1.ResourceCPU: resource.MustParse(item.cpu), - corev1.ResourceMemory: resource.MustParse(item.memory), - }, - } - assert.Equal(t, item.expect, len(cf.FindMatchingModels(requirements)) > 0) - } -} diff --git a/apis/apps/v1alpha1/cluster_types.go b/apis/apps/v1alpha1/cluster_types.go index 79dbd2aa4..54bb654b8 100644 --- a/apis/apps/v1alpha1/cluster_types.go +++ b/apis/apps/v1alpha1/cluster_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,21 +23,22 @@ import ( "github.com/pkg/errors" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" ) -// ClusterSpec defines the desired state of Cluster +// ClusterSpec defines the desired state of Cluster. type ClusterSpec struct { - // Cluster referenced ClusterDefinition name, this is an immutable attribute. + // Cluster referencing ClusterDefinition name. This is an immutable attribute. // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` ClusterDefRef string `json:"clusterDefinitionRef"` - // Cluster referenced ClusterVersion name. + // Cluster referencing ClusterVersion name. // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` // +optional ClusterVersionRef string `json:"clusterVersionRef,omitempty"` - // Cluster termination policy. One of DoNotTerminate, Halt, Delete, WipeOut. + // Cluster termination policy. Valid values are DoNotTerminate, Halt, Delete, WipeOut. // DoNotTerminate will block delete operation. // Halt will delete workload resources such as statefulset, deployment workloads but keep PVCs. // Delete is based on Halt and deletes PVCs. @@ -58,13 +59,13 @@ type ClusterSpec struct { // +optional Affinity *Affinity `json:"affinity,omitempty"` - // tolerations are attached to tolerate any taint that matches the triple using the matching operator . + // tolerations are attached to tolerate any taint that matches the triple `key,value,effect` using the matching operator `operator`. // +kubebuilder:pruning:PreserveUnknownFields // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` } -// ClusterStatus defines the observed state of Cluster +// ClusterStatus defines the observed state of Cluster. type ClusterStatus struct { // observedGeneration is the most recent generation observed for this // Cluster. It corresponds to the Cluster's generation, which is @@ -106,13 +107,17 @@ type ClusterComponentSpec struct { // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` Name string `json:"name"` - // componentDefRef reference componentDef defined in ClusterDefinition spec. + // componentDefRef references the componentDef defined in ClusterDefinition spec. // +kubebuilder:validation:Required // +kubebuilder:validation:MaxLength=63 // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` ComponentDefRef string `json:"componentDefRef"` - // monitor which is a switch to enable monitoring, default is false + // classDefRef references the class defined in ComponentClassDefinition. + // +optional + ClassDefRef *ClassDefRef `json:"classDefRef,omitempty"` + + // monitor is a switch to enable monitoring and is set as false by default. // KubeBlocks provides an extension mechanism to support component level monitoring, // which will scrape metrics auto or manually from servers in component and export // metrics to Time Series Database. @@ -120,20 +125,20 @@ type ClusterComponentSpec struct { // +optional Monitor bool `json:"monitor,omitempty"` - // enabledLogs indicate which log file takes effect in database cluster - // element is the log type which defined in cluster definition logConfig.name, + // enabledLogs indicates which log file takes effect in the database cluster. + // element is the log type which is defined in cluster definition logConfig.name, // and will set relative variables about this log type in database kernel. // +listType=set // +optional EnabledLogs []string `json:"enabledLogs,omitempty"` - // Component replicas, use default value in ClusterDefinition spec. if not specified. + // Component replicas. The default value is used in ClusterDefinition spec if not specified. // +kubebuilder:validation:Required // +kubebuilder:validation:Minimum=0 // +kubebuilder:default=1 Replicas int32 `json:"replicas"` - // affinity describes affinities which specific by users. + // affinity describes affinities specified by users. // +optional Affinity *Affinity `json:"affinity,omitempty"` @@ -142,7 +147,7 @@ type ClusterComponentSpec struct { // +optional Tolerations []corev1.Toleration `json:"tolerations,omitempty"` - // resources requests and limits of workload. + // Resources requests and limits of workload. // +kubebuilder:pruning:PreserveUnknownFields // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty"` @@ -153,11 +158,11 @@ type ClusterComponentSpec struct { // +patchStrategy=merge,retainKeys VolumeClaimTemplates []ClusterComponentVolumeClaimTemplate `json:"volumeClaimTemplates,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"` - // services expose endpoints can be accessed by clients + // Services expose endpoints that can be accessed by clients. // +optional Services []ClusterComponentService `json:"services,omitempty"` - // primaryIndex determines which index is primary when workloadType is Replication, index number starts from zero. + // primaryIndex determines which index is primary when workloadType is Replication. Index number starts from zero. // +kubebuilder:validation:Minimum=0 // +optional PrimaryIndex *int32 `json:"primaryIndex,omitempty"` @@ -166,33 +171,59 @@ type ClusterComponentSpec struct { // +optional SwitchPolicy *ClusterSwitchPolicy `json:"switchPolicy,omitempty"` - // tls should be enabled or not + // Enables or disables TLS certs. // +optional TLS bool `json:"tls,omitempty"` - // issuer who provides tls certs + // issuer defines provider context for TLS certs. // required when TLS enabled // +optional Issuer *Issuer `json:"issuer,omitempty"` + + // serviceAccountName is the name of the ServiceAccount that running component depends on. + // +optional + ServiceAccountName string `json:"serviceAccountName,omitempty"` + + // noCreatePDB defines the PodDistruptionBudget creation behavior and is set to true if creation of PodDistruptionBudget + // for this component is not needed. It defaults to false. + // +kubebuilder:default=false + // +optional + NoCreatePDB bool `json:"noCreatePDB,omitempty"` +} + +// GetMinAvailable wraps the 'prefer' value return. As for component replicaCount <= 1, it will return 0, +// and as for replicaCount=2 it will return 1. +func (r *ClusterComponentSpec) GetMinAvailable(prefer *intstr.IntOrString) *intstr.IntOrString { + if r == nil || r.NoCreatePDB || prefer == nil { + return nil + } + if r.Replicas <= 1 { + m := intstr.FromInt(0) + return &m + } else if r.Replicas == 2 { + m := intstr.FromInt(1) + return &m + } + return prefer } type ComponentMessageMap map[string]string -// ClusterComponentStatus record components status information +// ClusterComponentStatus records components status. type ClusterComponentStatus struct { - // phase describes the phase of the component, the detail information of the phases are as following: - // Running: component is running. [terminal state] - // Stopped: component is stopped, as no running pod. [terminal state] - // Failed: component is unavailable. i.e, all pods are not ready for Stateless/Stateful component, + // phase describes the phase of the component and the detail information of the phases are as following: + // Running: the component is running. [terminal state] + // Stopped: the component is stopped, as no running pod. [terminal state] + // Failed: the component is unavailable, i.e. all pods are not ready for Stateless/Stateful component and // Leader/Primary pod is not ready for Consensus/Replication component. [terminal state] - // Abnormal: component is running but part of its pods are not ready. + // Abnormal: the component is running but part of its pods are not ready. // Leader/Primary pod is ready for Consensus/Replication component. [terminal state] - // Creating: component has entered creating process. - // Updating: component has entered updating process, triggered by Spec. updated. + // Creating: the component has entered creating process. + // Updating: the component has entered updating process, triggered by Spec. updated. Phase ClusterComponentPhase `json:"phase,omitempty"` // message records the component details message in current phase. - // keys are podName or deployName or statefulSetName, the format is `/`. + // Keys are podName or deployName or statefulSetName. The format is `ObjectKind/Name`. // +optional Message ComponentMessageMap `json:"message,omitempty"` @@ -205,58 +236,58 @@ type ClusterComponentStatus struct { // +optional PodsReadyTime *metav1.Time `json:"podsReadyTime,omitempty"` - // consensusSetStatus role and pod name mapping. + // consensusSetStatus specifies the mapping of role and pod name. // +optional ConsensusSetStatus *ConsensusSetStatus `json:"consensusSetStatus,omitempty"` - // replicationSetStatus role and pod name mapping. + // replicationSetStatus specifies the mapping of role and pod name. // +optional ReplicationSetStatus *ReplicationSetStatus `json:"replicationSetStatus,omitempty"` } type ConsensusSetStatus struct { - // leader status. + // Leader status. // +kubebuilder:validation:Required Leader ConsensusMemberStatus `json:"leader"` - // followers status. + // Followers status. // +optional Followers []ConsensusMemberStatus `json:"followers,omitempty"` - // learner status. + // Learner status. // +optional Learner *ConsensusMemberStatus `json:"learner,omitempty"` } type ConsensusMemberStatus struct { - // name role name. + // Defines the role name. // +kubebuilder:validation:Required // +kubebuilder:default=leader Name string `json:"name"` - // accessMode, what service this pod provides. + // accessMode defines what service this pod provides. // +kubebuilder:validation:Required // +kubebuilder:default=ReadWrite AccessMode AccessMode `json:"accessMode"` - // pod name. + // Pod name. // +kubebuilder:validation:Required // +kubebuilder:default=Unknown Pod string `json:"pod"` } type ReplicationSetStatus struct { - // primary status. + // Primary status. // +kubebuilder:validation:Required Primary ReplicationMemberStatus `json:"primary"` - // secondaries status. + // Secondaries status. // +optional Secondaries []ReplicationMemberStatus `json:"secondaries,omitempty"` } type ReplicationMemberStatus struct { - // pod name. + // Pod name. // +kubebuilder:validation:Required // +kubebuilder:default=Unknown Pod string `json:"pod"` @@ -273,11 +304,10 @@ type ClusterSwitchPolicy struct { } type ClusterComponentVolumeClaimTemplate struct { - // Ref ClusterVersion.spec.components.containers.volumeMounts.name + // Reference `ClusterDefinition.spec.componentDefs.containers.volumeMounts.name`. // +kubebuilder:validation:Required Name string `json:"name"` // spec defines the desired characteristics of a volume requested by a pod author. - // +kubebuilder:pruning:PreserveUnknownFields // +optional Spec PersistentVolumeClaimSpec `json:"spec,omitempty"` } @@ -291,18 +321,20 @@ func (r *ClusterComponentVolumeClaimTemplate) toVolumeClaimTemplate() corev1.Per type PersistentVolumeClaimSpec struct { // accessModes contains the desired access modes the volume should have. - // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1 + // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1. + // +kubebuilder:pruning:PreserveUnknownFields // +optional AccessModes []corev1.PersistentVolumeAccessMode `json:"accessModes,omitempty" protobuf:"bytes,1,rep,name=accessModes,casttype=PersistentVolumeAccessMode"` // resources represents the minimum resources the volume should have. // If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements // that are lower than previous value but must still be higher than capacity recorded in the // status field of the claim. - // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources + // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources. + // +kubebuilder:pruning:PreserveUnknownFields // +optional Resources corev1.ResourceRequirements `json:"resources,omitempty" protobuf:"bytes,2,opt,name=resources"` // storageClassName is the name of the StorageClass required by the claim. - // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1 + // More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1. // +optional StorageClassName *string `json:"storageClassName,omitempty" protobuf:"bytes,5,opt,name=storageClassName"` // TODO: @@ -322,8 +354,8 @@ func (r PersistentVolumeClaimSpec) ToV1PersistentVolumeClaimSpec() corev1.Persis } } -// GetStorageClassName return PersistentVolumeClaimSpec.StorageClassName if value is assigned, otherwise -// return preferSC argument. +// GetStorageClassName returns PersistentVolumeClaimSpec.StorageClassName if a value is assigned; otherwise, +// it returns preferSC argument. func (r PersistentVolumeClaimSpec) GetStorageClassName(preferSC string) *string { if r.StorageClassName != nil && *r.StorageClassName != "" { return r.StorageClassName @@ -362,8 +394,8 @@ type Affinity struct { // Issuer defines Tls certs issuer type Issuer struct { - // name of issuer - // options supported: + // Name of issuer. + // Options supported: // - KubeBlocks - Certificates signed by KubeBlocks Operator. // - UserProvided - User provided own CA-signed certificates. // +kubebuilder:validation:Enum={KubeBlocks, UserProvided} @@ -371,7 +403,7 @@ type Issuer struct { // +kubebuilder:validation:Required Name IssuerName `json:"name"` - // secretRef, Tls certs Secret reference + // secretRef. TLS certs Secret reference // required when from is UserProvided // +optional SecretRef *TLSSecretRef `json:"secretRef,omitempty"` @@ -379,19 +411,19 @@ type Issuer struct { // TLSSecretRef defines Secret contains Tls certs type TLSSecretRef struct { - // name of the Secret + // Name of the Secret // +kubebuilder:validation:Required Name string `json:"name"` - // ca cert key in Secret + // CA cert key in Secret // +kubebuilder:validation:Required CA string `json:"ca"` - // cert key in Secret + // Cert key in Secret // +kubebuilder:validation:Required Cert string `json:"cert"` - // key of TLS private key in Secret + // Key of TLS private key in Secret // +kubebuilder:validation:Required Key string `json:"key"` } @@ -399,13 +431,14 @@ type TLSSecretRef struct { type ClusterComponentService struct { // Service name // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=15 Name string `json:"name"` // serviceType determines how the Service is exposed. Valid // options are ClusterIP, NodePort, and LoadBalancer. // "ClusterIP" allocates a cluster-internal IP address for load-balancing // to endpoints. Endpoints are determined by the selector or if that is not - // specified, by manual construction of an Endpoints object or + // specified, they are determined by manual construction of an Endpoints object or // EndpointSlice objects. If clusterIP is "None", no virtual IP is // allocated and the endpoints are published as a set of endpoints rather // than a virtual IP. @@ -414,7 +447,7 @@ type ClusterComponentService struct { // "LoadBalancer" builds on NodePort and creates an external load-balancer // (if supported in the current cloud) which routes to the same endpoints // as the clusterIP. - // More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types + // More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types. // +kubebuilder:default=ClusterIP // +kubebuilder:validation:Enum={ClusterIP,NodePort,LoadBalancer} // +kubebuilder:pruning:PreserveUnknownFields @@ -422,11 +455,23 @@ type ClusterComponentService struct { ServiceType corev1.ServiceType `json:"serviceType,omitempty"` // If ServiceType is LoadBalancer, cloud provider related parameters can be put here - // More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer + // More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer. // +optional Annotations map[string]string `json:"annotations,omitempty"` } +type ClassDefRef struct { + // Name refers to the name of the ComponentClassDefinition. + // +optional + Name string `json:"name,omitempty"` + + // Class refers to the name of the class that is defined in the ComponentClassDefinition. + // +kubebuilder:validation:Required + Class string `json:"class"` +} + +// +genclient +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks,all} @@ -436,7 +481,7 @@ type ClusterComponentService struct { // +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.phase",description="Cluster Status." // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" -// Cluster is the Schema for the clusters API +// Cluster is the Schema for the clusters API. type Cluster struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -447,7 +492,7 @@ type Cluster struct { // +kubebuilder:object:root=true -// ClusterList contains a list of Cluster +// ClusterList contains a list of Cluster. type ClusterList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -458,6 +503,70 @@ func init() { SchemeBuilder.Register(&Cluster{}, &ClusterList{}) } +func (r Cluster) IsDeleting() bool { + return !r.GetDeletionTimestamp().IsZero() +} + +func (r Cluster) IsUpdating() bool { + return r.Status.ObservedGeneration != r.Generation +} + +func (r Cluster) IsStatusUpdating() bool { + return !r.IsDeleting() && !r.IsUpdating() +} + +// GetVolumeClaimNames gets all PVC names of component compName. +// +// r.Spec.GetComponentByName(compName).VolumeClaimTemplates[*].Name will be used if no claimNames provided +// +// nil return if: +// 1. component compName not found or +// 2. len(VolumeClaimTemplates)==0 or +// 3. any claimNames not found +func (r *Cluster) GetVolumeClaimNames(compName string, claimNames ...string) []string { + if r == nil { + return nil + } + comp := r.Spec.GetComponentByName(compName) + if comp == nil { + return nil + } + if len(comp.VolumeClaimTemplates) == 0 { + return nil + } + if len(claimNames) == 0 { + for _, template := range comp.VolumeClaimTemplates { + claimNames = append(claimNames, template.Name) + } + } + allExist := true + for _, name := range claimNames { + found := false + for _, template := range comp.VolumeClaimTemplates { + if template.Name == name { + found = true + break + } + } + if !found { + allExist = false + break + } + } + if !allExist { + return nil + } + + pvcNames := make([]string, 0) + for _, claimName := range claimNames { + for i := 0; i < int(comp.Replicas); i++ { + pvcName := fmt.Sprintf("%s-%s-%s-%d", claimName, r.Name, compName, i) + pvcNames = append(pvcNames, pvcName) + } + } + return pvcNames +} + // GetComponentByName gets component by name. func (r ClusterSpec) GetComponentByName(componentName string) *ClusterComponentSpec { for _, v := range r.ComponentSpecs { @@ -478,7 +587,7 @@ func (r ClusterSpec) GetComponentDefRefName(componentName string) string { return "" } -// ValidateEnabledLogs validates enabledLogs config in cluster.yaml, and returns metav1.Condition when detect invalid values. +// ValidateEnabledLogs validates enabledLogs config in cluster.yaml, and returns metav1.Condition when detecting invalid values. func (r ClusterSpec) ValidateEnabledLogs(cd *ClusterDefinition) error { message := make([]string, 0) for _, comp := range r.ComponentSpecs { @@ -505,7 +614,7 @@ func (r ClusterSpec) GetDefNameMappingComponents() map[string][]ClusterComponent return m } -// GetMessage gets message map deep copy object +// GetMessage gets message map deep copy object. func (r ClusterComponentStatus) GetMessage() ComponentMessageMap { messageMap := map[string]string{} for k, v := range r.Message { @@ -514,7 +623,7 @@ func (r ClusterComponentStatus) GetMessage() ComponentMessageMap { return messageMap } -// SetMessage override message map object +// SetMessage overrides message map object. func (r *ClusterComponentStatus) SetMessage(messageMap ComponentMessageMap) { if r == nil { return @@ -522,7 +631,7 @@ func (r *ClusterComponentStatus) SetMessage(messageMap ComponentMessageMap) { r.Message = messageMap } -// SetObjectMessage sets k8s workload message to component status message map +// SetObjectMessage sets K8s workload message to component status message map. func (r *ClusterComponentStatus) SetObjectMessage(objectKind, objectName, message string) { if r == nil { return @@ -573,7 +682,7 @@ func (r *ClusterComponentSpec) ToVolumeClaimTemplates() []corev1.PersistentVolum return ts } -// GetPrimaryIndex provide safe operation get ClusterComponentSpec.PrimaryIndex, if value is nil, it's treated as 0. +// GetPrimaryIndex provides safe operation get ClusterComponentSpec.PrimaryIndex, if value is nil, it's treated as 0. func (r *ClusterComponentSpec) GetPrimaryIndex() int32 { if r == nil || r.PrimaryIndex == nil { return 0 @@ -581,7 +690,7 @@ func (r *ClusterComponentSpec) GetPrimaryIndex() int32 { return *r.PrimaryIndex } -// GetClusterTerminalPhases return Cluster terminal phases. +// GetClusterTerminalPhases returns Cluster terminal phases. func GetClusterTerminalPhases() []ClusterPhase { return []ClusterPhase{ RunningClusterPhase, @@ -591,7 +700,7 @@ func GetClusterTerminalPhases() []ClusterPhase { } } -// GetClusterUpRunningPhases return Cluster running or partially running phases. +// GetClusterUpRunningPhases returns Cluster running or partially running phases. func GetClusterUpRunningPhases() []ClusterPhase { return []ClusterPhase{ RunningClusterPhase, @@ -617,3 +726,17 @@ func GetComponentTerminalPhases() []ClusterComponentPhase { AbnormalClusterCompPhase, } } + +// GetComponentUpRunningPhase returns component running or partially running phases. +func GetComponentUpRunningPhase() []ClusterComponentPhase { + return []ClusterComponentPhase{ + RunningClusterCompPhase, + AbnormalClusterCompPhase, + FailedClusterCompPhase, + } +} + +// ComponentPodsAreReady checks if the pods of component are ready. +func ComponentPodsAreReady(podsAreReady *bool) bool { + return podsAreReady != nil && *podsAreReady +} diff --git a/apis/apps/v1alpha1/cluster_types_test.go b/apis/apps/v1alpha1/cluster_types_test.go index 6245259f6..c858be360 100644 --- a/apis/apps/v1alpha1/cluster_types_test.go +++ b/apis/apps/v1alpha1/cluster_types_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/cluster_webhook.go b/apis/apps/v1alpha1/cluster_webhook.go index d7ac7cf69..a1f0e6bf8 100644 --- a/apis/apps/v1alpha1/cluster_webhook.go +++ b/apis/apps/v1alpha1/cluster_webhook.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/cluster_webhook_test.go b/apis/apps/v1alpha1/cluster_webhook_test.go index 9488f3334..baa128764 100644 --- a/apis/apps/v1alpha1/cluster_webhook_test.go +++ b/apis/apps/v1alpha1/cluster_webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -84,19 +84,13 @@ var _ = Describe("cluster webhook", func() { Expect(testCtx.CreateObj(ctx, clusterDefSecond)).Should(Succeed()) // wait until ClusterDefinition created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef)).Should(Succeed()) By("By creating a new clusterVersion") clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName) Expect(testCtx.CreateObj(ctx, clusterVersion)).Should(Succeed()) // wait until ClusterVersion created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterVersionName}, clusterVersion) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterVersionName}, clusterVersion)).Should(Succeed()) By("By creating a new Cluster") cluster, _ = createTestCluster(clusterDefinitionName, clusterVersionName, clusterName) @@ -204,19 +198,13 @@ var _ = Describe("cluster webhook", func() { Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) // wait until ClusterDefinition created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef)).Should(Succeed()) By("By creating a new clusterVersion") clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName) Expect(testCtx.CreateObj(ctx, clusterVersion)).Should(Succeed()) // wait until ClusterVersion created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterVersionName}, clusterVersion) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterVersionName}, clusterVersion)).Should(Succeed()) }) It("should assure tls fields setting properly", func() { By("creating cluster with nil issuer") @@ -234,10 +222,8 @@ var _ = Describe("cluster webhook", func() { By("creating cluster with UserProvided issuer and secret ref provided") Expect(k8sClient.Delete(ctx, cluster)).Should(Succeed()) - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), &Cluster{}) - return apierrors.IsNotFound(err) - }).Should(BeTrue()) + err := k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), &Cluster{}) + Expect(apierrors.IsNotFound(err)).Should(BeTrue()) cluster, _ = createTestCluster(clusterDefinitionName, clusterVersionName, clusterName) cluster.Spec.ComponentSpecs[0].TLS = true cluster.Spec.ComponentSpecs[0].Issuer = &Issuer{ @@ -267,10 +253,8 @@ var _ = Describe("cluster webhook", func() { cluster, _ := createTestReplicationSetCluster(rsClusterDefinitionName, rsClusterVersionName, rsClusterName) cluster.Spec.ComponentSpecs[0].PrimaryIndex = nil Expect(testCtx.CreateObj(ctx, cluster)).Should(Succeed()) - Eventually(func(g Gomega) int32 { - g.Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).Should(Succeed()) - return *cluster.Spec.ComponentSpecs[0].PrimaryIndex - }).Should(Equal(int32(0))) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(cluster), cluster)).Should(Succeed()) + Expect(*cluster.Spec.ComponentSpecs[0].PrimaryIndex).Should(Equal(int32(0))) By("By update Replication component replicas to 0, expect succeed") cluster.Spec.ComponentSpecs[0].Replicas = int32(0) @@ -296,6 +280,7 @@ spec: volumeClaimTemplates: - name: data spec: + storageClassName: standard resources: requests: storage: 1Gi @@ -309,7 +294,7 @@ spec: return cluster, err } -func createTestReplicationSetCluster(clusterDefinitionName, clusterVerisonName, clusterName string) (*Cluster, error) { +func createTestReplicationSetCluster(clusterDefinitionName, clusterVersionName, clusterName string) (*Cluster, error) { clusterYaml := fmt.Sprintf(` apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster @@ -333,7 +318,7 @@ spec: resources: requests: storage: 1Gi -`, clusterName, clusterDefinitionName, clusterVerisonName) +`, clusterName, clusterDefinitionName, clusterVersionName) cluster := &Cluster{} err := yaml.Unmarshal([]byte(clusterYaml), cluster) cluster.Spec.TerminationPolicy = WipeOut diff --git a/apis/apps/v1alpha1/clusterdefinition_types.go b/apis/apps/v1alpha1/clusterdefinition_types.go index e9bd2ce22..f254d5116 100644 --- a/apis/apps/v1alpha1/clusterdefinition_types.go +++ b/apis/apps/v1alpha1/clusterdefinition_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -19,6 +19,7 @@ package v1alpha1 import ( "strings" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" @@ -43,16 +44,18 @@ type ClusterDefinitionSpec struct { ComponentDefs []ClusterComponentDefinition `json:"componentDefs" patchStrategy:"merge,retainKeys" patchMergeKey:"name"` // Connection credential template used for creating a connection credential - // secret for cluster.apps.kubeblock.io object. Built-in objects are: + // secret for cluster.apps.kubeblocks.io object. Built-in objects are: // `$(RANDOM_PASSWD)` - random 8 characters. // `$(UUID)` - generate a random UUID v4 string. - // `$(UUID_B64)` - generate a random UUID v4 BASE64 encoded string``. - // `$(UUID_STR_B64)` - generate a random UUID v4 string then BASE64 encoded``. - // `$(UUID_HEX)` - generate a random UUID v4 wth HEX representation``. + // `$(UUID_B64)` - generate a random UUID v4 BASE64 encoded string. + // `$(UUID_STR_B64)` - generate a random UUID v4 string then BASE64 encoded. + // `$(UUID_HEX)` - generate a random UUID v4 HEX representation. + // `$(HEADLESS_SVC_FQDN)` - headless service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc, + // where 1ST_COMP_NAME is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` attribute; // `$(SVC_FQDN)` - service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc, // where 1ST_COMP_NAME is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` attribute; - // `$(SVC_PORT_)` - a ServicePort's port value with specified port name, i.e, a servicePort JSON struct: - // { "name": "mysql", "targetPort": "mysqlContainerPort", "port": 3306 }, and "$(SVC_PORT_mysql)" in the + // `$(SVC_PORT_{PORT-NAME})` - a ServicePort's port value with specified port name, i.e, a servicePort JSON struct: + // `"name": "mysql", "targetPort": "mysqlContainerPort", "port": 3306`, and "$(SVC_PORT_mysql)" in the // connection credential value is 3306. // +optional ConnectionCredential map[string]string `json:"connectionCredential,omitempty"` @@ -79,8 +82,7 @@ type SystemAccountSpec struct { // CmdExecutorConfig specifies how to perform creation and deletion statements. type CmdExecutorConfig struct { CommandExecutorEnvItem `json:",inline"` - - CommandExecutorItem `json:",inline"` + CommandExecutorItem `json:",inline"` } // PasswordConfig helps provide to customize complexity of password generation pattern. @@ -150,14 +152,19 @@ type ProvisionStatements struct { // creation specifies statement how to create this account with required privileges. // +kubebuilder:validation:Required CreationStatement string `json:"creation"` + // update specifies statement how to update account's password. + // +kubebuilder:validation:Required + UpdateStatement string `json:"update,omitempty"` // deletion specifies statement how to delete this account. + // Used in combination with `CreateionStatement` to delete the account before create it. + // For instance, one usually uses `drop user if exists` statement followed by `create user` statement to create an account. // +optional DeletionStatement string `json:"deletion,omitempty"` } // ClusterDefinitionStatus defines the observed state of ClusterDefinition type ClusterDefinitionStatus struct { - // ClusterDefinition phase, valid values are , Available. + // ClusterDefinition phase, valid values are `empty`, `Available`, 'Unavailable`. // Available is ClusterDefinition become available, and can be referenced for co-related objects. Phase Phase `json:"phase,omitempty"` @@ -176,64 +183,11 @@ func (r ClusterDefinitionStatus) GetTerminalPhases() []Phase { return []Phase{AvailablePhase} } -type ComponentTemplateSpec struct { - // Specify the name of configuration template. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` - Name string `json:"name"` - - // Specify the name of the referenced the configuration template ConfigMap object. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` - TemplateRef string `json:"templateRef"` - - // Specify the namespace of the referenced the configuration template ConfigMap object. - // An empty namespace is equivalent to the "default" namespace. - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:default="default" - // +optional - Namespace string `json:"namespace,omitempty"` - - // volumeName is the volume name of PodTemplate, which the configuration file produced through the configuration template will be mounted to the corresponding volume. - // The volume name must be defined in podSpec.containers[*].volumeMounts. - // +kubebuilder:validation:Required - // +kubebuilder:validation:MaxLength=32 - VolumeName string `json:"volumeName"` - - // defaultMode is optional: mode bits used to set permissions on created files by default. - // Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. - // YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. - // Defaults to 0644. - // Directories within the path are not affected by this setting. - // This might be in conflict with other options that affect the file - // mode, like fsGroup, and the result can be other mode bits set. - // +optional - DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"varint,3,opt,name=defaultMode"` -} - -type ComponentConfigSpec struct { - ComponentTemplateSpec `json:",inline"` - - // Specify a list of keys. - // If empty, ConfigConstraint takes effect for all keys in configmap. - // +optional - Keys []string `json:"keys,omitempty"` - - // Specify the name of the referenced the configuration constraints object. - // +kubebuilder:validation:MaxLength=63 - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` - // +optional - ConfigConstraintRef string `json:"constraintRef,omitempty"` -} - type ExporterConfig struct { // scrapePort is exporter port for Time Series Database to scrape metrics. // +kubebuilder:validation:Required - // +kubebuilder:validation:Maximum=65535 - // +kubebuilder:validation:Minimum=0 - ScrapePort int32 `json:"scrapePort"` + // +kubebuilder:validation:XIntOrString + ScrapePort intstr.IntOrString `json:"scrapePort"` // scrapePath is exporter url path for Time Series Database to scrape metrics. // +kubebuilder:validation:MaxLength=128 @@ -244,8 +198,8 @@ type ExporterConfig struct { type MonitorConfig struct { // builtIn is a switch to enable KubeBlocks builtIn monitoring. + // If BuiltIn is set to true, monitor metrics will be scraped automatically. // If BuiltIn is set to false, the provider should set ExporterConfig and Sidecar container own. - // BuiltIn set to true is not currently supported but will be soon. // +kubebuilder:default=false // +optional BuiltIn bool `json:"builtIn,omitempty"` @@ -272,9 +226,9 @@ type LogConfig struct { type VolumeTypeSpec struct { // name definition is the same as the name of the VolumeMounts field in PodSpec.Container, // similar to the relations of Volumes[*].name and VolumesMounts[*].name in Pod.Spec. + // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` - // +optional - Name string `json:"name,omitempty"` + Name string `json:"name"` // type is in enum of {data, log}. // VolumeTypeData: the volume is for the persistent data storage. @@ -286,6 +240,7 @@ type VolumeTypeSpec struct { // ClusterComponentDefinition provides a workload component specification template, // with attributes that strongly work with stateful workloads and day-2 operations // behaviors. +// +kubebuilder:validation:XValidation:rule="has(self.workloadType) && self.workloadType == 'Consensus' ? has(self.consensusSpec) : !has(self.consensusSpec)",message="componentDefs.consensusSpec is required when componentDefs.workloadType is Consensus, and forbidden otherwise" type ClusterComponentDefinition struct { // name of the component, it can be any valid string. // +kubebuilder:validation:Required @@ -310,14 +265,6 @@ type ClusterComponentDefinition struct { // +optional CharacterType string `json:"characterType,omitempty"` - // The maximum number of pods that can be unavailable during scaling. - // Value can be an absolute number (ex: 5) or a percentage of desired pods (ex: 10%). - // Absolute number is calculated from percentage by rounding down. This value is ignored - // if workloadType is Consensus. - // +kubebuilder:validation:XIntOrString - // +optional - MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` - // The configSpec field provided by provider, and // finally this configTemplateRefs will be rendered into the user's own configuration file according to the user's cluster. // +optional @@ -363,13 +310,21 @@ type ClusterComponentDefinition struct { // +optional Service *ServiceSpec `json:"service,omitempty"` + // statelessSpec defines stateless related spec if workloadType is Stateless. + // +optional + StatelessSpec *StatelessSetSpec `json:"statelessSpec,omitempty"` + + // statefulSpec defines stateful related spec if workloadType is Stateful. + // +optional + StatefulSpec *StatefulSetSpec `json:"statefulSpec,omitempty"` + // consensusSpec defines consensus related spec if workloadType is Consensus, required if workloadType is Consensus. // +optional ConsensusSpec *ConsensusSetSpec `json:"consensusSpec,omitempty"` - // replicationSpec defines replication related spec if workloadType is Replication, required if workloadType is Replication. + // replicationSpec defines replication related spec if workloadType is Replication. // +optional - ReplicationSpec *ReplicationSpec `json:"replicationSpec,omitempty"` + ReplicationSpec *ReplicationSetSpec `json:"replicationSpec,omitempty"` // horizontalScalePolicy controls the behavior of horizontal scale. // +optional @@ -386,12 +341,14 @@ type ClusterComponentDefinition struct { // according to the volumeType. // // For example: - // `{name: data, type: data}` means that the volume named `data` is used to store `data`. - // `{name: binlog, type: log}` means that the volume named `binlog` is used to store `log`. + // `name: data, type: data` means that the volume named `data` is used to store `data`. + // `name: binlog, type: log` means that the volume named `binlog` is used to store `log`. // // NOTE: // When volumeTypes is not defined, the backup function will not be supported, // even if a persistent volume has been specified. + // +listType=map + // +listMapKey=name // +optional VolumeTypes []VolumeTypeSpec `json:"volumeTypes,omitempty"` @@ -402,6 +359,118 @@ type ClusterComponentDefinition struct { CustomLabelSpecs []CustomLabelSpec `json:"customLabelSpecs,omitempty"` } +func (r *ClusterComponentDefinition) GetStatefulSetWorkload() StatefulSetWorkload { + switch r.WorkloadType { + case Stateless: + return nil + case Stateful: + return r.StatefulSpec + case Consensus: + return r.ConsensusSpec + case Replication: + return r.ReplicationSpec + } + panic("unreachable") +} + +// GetMinAvailable get workload's minAvailable settings, return 51% for workloadType=Consensus, +// value 1 pod for workloadType=[Stateless|Stateful|Replication]. +func (r *ClusterComponentDefinition) GetMinAvailable() *intstr.IntOrString { + if r == nil { + return nil + } + switch r.WorkloadType { + case Consensus: + // Consensus workload have min pods of >50%. + v := intstr.FromString("51%") + return &v + case Replication, Stateful, Stateless: + // Stateful & Replication workload have min. pod being 1. + v := intstr.FromInt(1) + return &v + } + return nil +} + +// GetMaxUnavailable get workload's maxUnavailable settings, this value is not suitable for PDB.spec.maxUnavailable +// usage, as a PDB with maxUnavailable=49% and if workload's replicaCount=3 and allowed disruption pod count is 2, +// check following setup: +// +// #cmd: kubectl get sts,po,pdb -l app.kubernetes.io/instance=consul +// NAME READY AGE +// statefulset.apps/consul 3/3 3h23m +// +// NAME READY STATUS RESTARTS AGE +// pod/consul-0 1/1 Running 0 3h +// pod/consul-2 1/1 Running 0 16s +// pod/consul-1 1/1 Running 0 16s +// +// NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE +// poddisruptionbudget.policy/consul N/A 49% 2 3h23m +// +// VS. using minAvailable=51% will result allowed disruption pod count is 1 +// +// NAME READY AGE +// statefulset.apps/consul 3/3 3h26m +// +// NAME READY STATUS RESTARTS AGE +// pod/consul-0 1/1 Running 0 3h3m +// pod/consul-2 1/1 Running 0 3m35s +// pod/consul-1 1/1 Running 0 3m35s +// +// NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE +// poddisruptionbudget.policy/consul 51% N/A 1 3h26m +func (r *ClusterComponentDefinition) GetMaxUnavailable() *intstr.IntOrString { + if r == nil { + return nil + } + + getMaxUnavailable := func(ssus appsv1.StatefulSetUpdateStrategy) *intstr.IntOrString { + if ssus.RollingUpdate == nil { + return nil + } + return ssus.RollingUpdate.MaxUnavailable + } + + switch r.WorkloadType { + case Stateless: + if r.StatelessSpec == nil || r.StatelessSpec.UpdateStrategy.RollingUpdate == nil { + return nil + } + return r.StatelessSpec.UpdateStrategy.RollingUpdate.MaxUnavailable + case Stateful, Consensus, Replication: + _, s := r.GetStatefulSetWorkload().FinalStsUpdateStrategy() + return getMaxUnavailable(s) + } + panic("unreachable") +} + +func (r *ClusterComponentDefinition) IsStatelessWorkload() bool { + return r.WorkloadType == Stateless +} + +func (r *ClusterComponentDefinition) GetCommonStatefulSpec() (*StatefulSetSpec, error) { + if r.IsStatelessWorkload() { + return nil, ErrWorkloadTypeIsStateless + } + switch r.WorkloadType { + case Stateful: + return r.StatefulSpec, nil + case Consensus: + if r.ConsensusSpec != nil { + return &r.ConsensusSpec.StatefulSetSpec, nil + } + case Replication: + if r.ReplicationSpec != nil { + return &r.ReplicationSpec.StatefulSetSpec, nil + } + default: + panic("unreachable") + // return nil, ErrWorkloadTypeIsUnknown + } + return nil, nil +} + type ServiceSpec struct { // The list of ports that are exposed by this service. // More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies @@ -410,6 +479,7 @@ type ServiceSpec struct { // +listType=map // +listMapKey=port // +listMapKey=protocol + // +optional Ports []ServicePort `json:"ports,omitempty" patchStrategy:"merge" patchMergeKey:"port" protobuf:"bytes,1,rep,name=ports"` } @@ -462,6 +532,7 @@ type ServicePort struct { // This field is ignored for services with clusterIP=None, and should be // omitted or set equal to the 'port' field. // More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service + // +kubebuilder:validation:XIntOrString // +optional TargetPort intstr.IntOrString `json:"targetPort,omitempty" protobuf:"bytes,4,opt,name=targetPort"` } @@ -481,7 +552,7 @@ type HorizontalScalePolicy struct { // Policy is in enum of {None, Snapshot}. The default policy is `None`. // None: Default policy, do nothing. // Snapshot: Do native volume snapshot before scaling and restore to newly scaled pods. - // Prefer backup job to create snapshot if `BackupTemplateSelector` can find a template. + // Prefer backup job to create snapshot if can find a backupPolicy from 'BackupPolicyTemplateName'. // Notice that 'Snapshot' policy will only take snapshot on one volumeMount, default is // the first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]), // since take multiple snapshots at one time might cause consistency problem. @@ -489,9 +560,9 @@ type HorizontalScalePolicy struct { // +optional Type HScaleDataClonePolicyType `json:"type,omitempty"` - // backupTemplateSelector defines the label selector for finding associated BackupTemplate API object. + // BackupPolicyTemplateName reference the backup policy template. // +optional - BackupTemplateSelector map[string]string `json:"backupTemplateSelector,omitempty"` + BackupPolicyTemplateName string `json:"backupPolicyTemplateName,omitempty"` // volumeMountsName defines which volumeMount of the container to do backup, // only work if Type is not None @@ -542,20 +613,113 @@ type ClusterDefinitionProbes struct { // Probe for DB role changed check. // +optional - RoleChangedProbe *ClusterDefinitionProbe `json:"roleChangedProbe,omitempty"` + RoleProbe *ClusterDefinitionProbe `json:"roleProbe,omitempty"` // roleProbeTimeoutAfterPodsReady(in seconds), when all pods of the component are ready, // it will detect whether the application is available in the pod. // if pods exceed the InitializationTimeoutSeconds time without a role label, // this component will enter the Failed/Abnormal phase. - // Note that this configuration will only take effect if the component supports RoleChangedProbe + // Note that this configuration will only take effect if the component supports RoleProbe // and will not affect the life cycle of the pod. default values are 60 seconds. // +optional // +kubebuilder:validation:Minimum=30 RoleProbeTimeoutAfterPodsReady int32 `json:"roleProbeTimeoutAfterPodsReady,omitempty"` } +type StatelessSetSpec struct { + // updateStrategy defines the underlying deployment strategy to use to replace existing pods with new ones. + // +optional + // +patchStrategy=retainKeys + UpdateStrategy appsv1.DeploymentStrategy `json:"updateStrategy,omitempty"` +} + +type StatefulSetSpec struct { + // updateStrategy, Pods update strategy. + // In case of workloadType=Consensus the update strategy will be following: + // + // serial: update Pods one by one that guarantee minimum component unavailable time. + // Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) -> Follower(with AccessMode=readWrite) -> Leader + // bestEffortParallel: update Pods in parallel that guarantee minimum component un-writable time. + // Learner, Follower(minority) in parallel -> Follower(majority) -> Leader, keep majority online all the time. + // parallel: force parallel + // +kubebuilder:default=Serial + // +optional + UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` + + // llPodManagementPolicy is the low-level controls how pods are created during initial scale up, + // when replacing pods on nodes, or when scaling down. + // `OrderedReady` policy specify where pods are created in increasing order (pod-0, then + // pod-1, etc) and the controller will wait until each pod is ready before + // continuing. When scaling down, the pods are removed in the opposite order. + // `Parallel` policy specify create pods in parallel + // to match the desired scale without waiting, and on scale down will delete + // all pods at once. + // +optional + LLPodManagementPolicy appsv1.PodManagementPolicyType `json:"llPodManagementPolicy,omitempty"` + + // llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy that will be + // employed to update Pods in the StatefulSet when a revision is made to + // Template. Will ignore `updateStrategy` attribute if provided. + // +optional + LLUpdateStrategy *appsv1.StatefulSetUpdateStrategy `json:"llUpdateStrategy,omitempty"` +} + +var _ StatefulSetWorkload = &StatefulSetSpec{} + +func (r *StatefulSetSpec) GetUpdateStrategy() UpdateStrategy { + if r == nil { + return SerialStrategy + } + return r.UpdateStrategy +} + +func (r *StatefulSetSpec) FinalStsUpdateStrategy() (appsv1.PodManagementPolicyType, appsv1.StatefulSetUpdateStrategy) { + if r == nil { + r = &StatefulSetSpec{ + UpdateStrategy: SerialStrategy, + } + } + return r.finalStsUpdateStrategy() +} + +func (r *StatefulSetSpec) finalStsUpdateStrategy() (appsv1.PodManagementPolicyType, appsv1.StatefulSetUpdateStrategy) { + if r.LLUpdateStrategy != nil { + return r.LLPodManagementPolicy, *r.LLUpdateStrategy + } + + switch r.UpdateStrategy { + case BestEffortParallelStrategy: + m := intstr.FromString("49%") + return appsv1.ParallelPodManagement, appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + // alpha feature since v1.24 + // ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#maximum-unavailable-pods + MaxUnavailable: &m, + }, + } + case ParallelStrategy: + return appsv1.ParallelPodManagement, appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + } + case SerialStrategy: + fallthrough + default: + m := intstr.FromInt(1) + return appsv1.OrderedReadyPodManagement, appsv1.StatefulSetUpdateStrategy{ + Type: appsv1.RollingUpdateStatefulSetStrategyType, + RollingUpdate: &appsv1.RollingUpdateStatefulSetStrategy{ + // alpha feature since v1.24 + // ref: https://kubernetes.io/docs/concepts/workloads/controllers/statefulset/#maximum-unavailable-pods + MaxUnavailable: &m, + }, + } + } +} + type ConsensusSetSpec struct { + StatefulSetSpec `json:",inline"` + // leader, one single leader. // +kubebuilder:validation:Required Leader ConsensusMember `json:"leader"` @@ -567,16 +731,40 @@ type ConsensusSetSpec struct { // learner, no voting right. // +optional Learner *ConsensusMember `json:"learner,omitempty"` +} - // updateStrategy, Pods update strategy. - // serial: update Pods one by one that guarantee minimum component unavailable time. - // Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) -> Follower(with AccessMode=readWrite) -> Leader - // bestEffortParallel: update Pods in parallel that guarantee minimum component un-writable time. - // Learner, Follower(minority) in parallel -> Follower(majority) -> Leader, keep majority online all the time. - // parallel: force parallel - // +kubebuilder:default=Serial - // +optional - UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` +var _ StatefulSetWorkload = &ConsensusSetSpec{} + +func (r *ConsensusSetSpec) GetUpdateStrategy() UpdateStrategy { + if r == nil { + return SerialStrategy + } + return r.UpdateStrategy +} + +func (r *ConsensusSetSpec) FinalStsUpdateStrategy() (appsv1.PodManagementPolicyType, appsv1.StatefulSetUpdateStrategy) { + if r == nil { + r = NewConsensusSetSpec() + } + if r.LLUpdateStrategy != nil { + return r.LLPodManagementPolicy, *r.LLUpdateStrategy + } + _, s := r.StatefulSetSpec.finalStsUpdateStrategy() + // switch r.UpdateStrategy { + // case SerialStrategy, BestEffortParallelStrategy: + s.Type = appsv1.OnDeleteStatefulSetStrategyType + s.RollingUpdate = nil + // } + return appsv1.ParallelPodManagement, s +} + +func NewConsensusSetSpec() *ConsensusSetSpec { + return &ConsensusSetSpec{ + Leader: DefaultLeader, + StatefulSetSpec: StatefulSetSpec{ + UpdateStrategy: SerialStrategy, + }, + } } type ConsensusMember struct { @@ -600,7 +788,9 @@ type ConsensusMember struct { Replicas *int32 `json:"replicas,omitempty"` } -type ReplicationSpec struct { +type ReplicationSetSpec struct { + StatefulSetSpec `json:",inline"` + // switchPolicies defines a collection of different types of switchPolicy, and each type of switchPolicy is limited to one. // +kubebuilder:validation:Required // +kubebuilder:validation:MinItems=1 @@ -611,6 +801,29 @@ type ReplicationSpec struct { SwitchCmdExecutorConfig *SwitchCmdExecutorConfig `json:"switchCmdExecutorConfig"` } +var _ StatefulSetWorkload = &ReplicationSetSpec{} + +func (r *ReplicationSetSpec) GetUpdateStrategy() UpdateStrategy { + if r == nil { + return SerialStrategy + } + return r.UpdateStrategy +} + +func (r *ReplicationSetSpec) FinalStsUpdateStrategy() (appsv1.PodManagementPolicyType, appsv1.StatefulSetUpdateStrategy) { + if r == nil { + r = &ReplicationSetSpec{} + } + if r.LLUpdateStrategy != nil { + return r.LLPodManagementPolicy, *r.LLUpdateStrategy + } + _, s := r.StatefulSetSpec.finalStsUpdateStrategy() + // TODO(xingran): The update of the replicationSet needs to generate a plan according to the role + s.Type = appsv1.OnDeleteStatefulSetStrategyType + s.RollingUpdate = nil + return appsv1.ParallelPodManagement, s +} + type SwitchPolicy struct { // switchPolicyType defines type of the switchPolicy. // MaximumAvailability: when the primary is active, do switch if the synchronization delay = 0 in the user-defined lagProbe data delay detection logic, otherwise do not switch. The primary is down, switch immediately. @@ -709,6 +922,9 @@ type GVKResource struct { Selector map[string]string `json:"selector,omitempty"` } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Cluster,shortName=cd diff --git a/apis/apps/v1alpha1/clusterdefinition_types_test.go b/apis/apps/v1alpha1/clusterdefinition_types_test.go index 778f6fcc8..9f6af5fbf 100644 --- a/apis/apps/v1alpha1/clusterdefinition_types_test.go +++ b/apis/apps/v1alpha1/clusterdefinition_types_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/clusterdefinition_webhook.go b/apis/apps/v1alpha1/clusterdefinition_webhook.go index 2f0f1a662..7a6e910ea 100644 --- a/apis/apps/v1alpha1/clusterdefinition_webhook.go +++ b/apis/apps/v1alpha1/clusterdefinition_webhook.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -57,13 +57,13 @@ func (r *ClusterDefinition) Default() { if probes == nil { continue } - if probes.RoleChangedProbe != nil { + if probes.RoleProbe != nil { // set default values if probes.RoleProbeTimeoutAfterPodsReady == 0 { probes.RoleProbeTimeoutAfterPodsReady = DefaultRoleProbeTimeoutAfterPodsReady } } else { - // if component does not support RoleChangedProbe, reset RoleProbeTimeoutAtPodsReady to zero + // if component does not support RoleProbe, reset RoleProbeTimeoutAtPodsReady to zero if probes.RoleProbeTimeoutAfterPodsReady != 0 { probes.RoleProbeTimeoutAfterPodsReady = 0 } @@ -234,7 +234,7 @@ func (r *SystemAccountSpec) validateSysAccounts(allErrs *field.ErrorList) { if _, exists := accountName[sysAccount.Name]; exists { *allErrs = append(*allErrs, field.Invalid(field.NewPath("spec.components[*].systemAccounts.accounts"), - sysAccount.Name, "duplicated system account names are not allowd.")) + sysAccount.Name, "duplicated system account names are not allowed.")) continue } else { accountName[sysAccount.Name] = true diff --git a/apis/apps/v1alpha1/clusterdefinition_webhook_test.go b/apis/apps/v1alpha1/clusterdefinition_webhook_test.go index 9296eaee9..7b204636d 100644 --- a/apis/apps/v1alpha1/clusterdefinition_webhook_test.go +++ b/apis/apps/v1alpha1/clusterdefinition_webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -56,19 +56,13 @@ var _ = Describe("clusterDefinition webhook", func() { clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName) Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) // wait until ClusterDefinition created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName}, clusterDef)).Should(Succeed()) By("By creating a new clusterDefinition") clusterDef, _ = createTestClusterDefinitionObj3(clusterDefinitionName3) Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) // wait until ClusterDefinition created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef)).Should(Succeed()) By("By creating a new clusterDefinition with workloadType==Consensus but consensusSpec not present") clusterDef, _ = createTestClusterDefinitionObj2(clusterDefinitionName2) @@ -164,10 +158,7 @@ var _ = Describe("clusterDefinition webhook", func() { By("By creating a new clusterDefinition with valid accounts") Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) // wait until ClusterDefinition created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef) - return err == nil - }).Should(BeTrue()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDefinitionName3}, clusterDef)).Should(Succeed()) }) It("Should webhook validate configSpec", func() { @@ -281,25 +272,21 @@ var _ = Describe("clusterDefinition webhook", func() { It("test mutating webhook", func() { clusterDef, _ := createTestClusterDefinitionObj3(clusterDefinitionName + "-mutating") - By("test set the default value to RoleProbeTimeoutAfterPodsReady when roleChangedProbe is not nil") + By("test set the default value to RoleProbeTimeoutAfterPodsReady when roleProbe is not nil") clusterDef.Spec.ComponentDefs[0].Probes = &ClusterDefinitionProbes{ - RoleChangedProbe: &ClusterDefinitionProbe{}, + RoleProbe: &ClusterDefinitionProbe{}, } Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) - Eventually(func(g Gomega) int32 { - g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) - return clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady - }).Should(Equal(DefaultRoleProbeTimeoutAfterPodsReady)) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) + Expect(clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady).Should(Equal(DefaultRoleProbeTimeoutAfterPodsReady)) - By("test set zero to RoleProbeTimeoutAfterPodsReady when roleChangedProbe is nil") + By("test set zero to RoleProbeTimeoutAfterPodsReady when roleProbe is nil") clusterDef.Spec.ComponentDefs[0].Probes = &ClusterDefinitionProbes{ RoleProbeTimeoutAfterPodsReady: 60, } Expect(k8sClient.Update(ctx, clusterDef)).Should(Succeed()) - Eventually(func(g Gomega) int32 { - g.Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) - return clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady - }).Should(Equal(int32(0))) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: clusterDef.Name}, clusterDef)).Should(Succeed()) + Expect(clusterDef.Spec.ComponentDefs[0].Probes.RoleProbeTimeoutAfterPodsReady).Should(Equal(int32(0))) }) }) diff --git a/apis/apps/v1alpha1/clusterversion_types.go b/apis/apps/v1alpha1/clusterversion_types.go index 5e233c5d9..6907c27d3 100644 --- a/apis/apps/v1alpha1/clusterversion_types.go +++ b/apis/apps/v1alpha1/clusterversion_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -79,11 +79,24 @@ type ClusterComponentVersion struct { // +listMapKey=name ConfigSpecs []ComponentConfigSpec `json:"configSpecs,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"name"` + // systemAccountSpec define image for the component to connect database or engines. + // It overrides `image` and `env` attributes defined in ClusterDefinition.spec.componentDefs.systemAccountSpec.cmdExecutorConfig. + // To clean default envs settings, set `SystemAccountSpec.CmdExecutorConfig.Env` to empty list. + // +optional + SystemAccountSpec *SystemAccountShortSpec `json:"systemAccountSpec,omitempty"` + // versionContext defines containers images' context for component versions, // this value replaces ClusterDefinition.spec.componentDefs.podSpec.[initContainers | containers] VersionsCtx VersionsContext `json:"versionsContext"` } +// SystemAccountShortSpec is a short version of SystemAccountSpec, with only CmdExecutorConfig field. +type SystemAccountShortSpec struct { + // cmdExecutorConfig configs how to get client SDK and perform statements. + // +kubebuilder:validation:Required + CmdExecutorConfig *CommandExecutorEnvItem `json:"cmdExecutorConfig"` +} + type VersionsContext struct { // Provide ClusterDefinition.spec.componentDefs.podSpec.initContainers override // values, typical scenarios are application container image updates. @@ -106,6 +119,9 @@ type VersionsContext struct { Containers []corev1.Container `json:"containers,omitempty"` } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Cluster,shortName=cv diff --git a/apis/apps/v1alpha1/clusterversion_types_test.go b/apis/apps/v1alpha1/clusterversion_types_test.go index d98502cfb..7bea44c5f 100644 --- a/apis/apps/v1alpha1/clusterversion_types_test.go +++ b/apis/apps/v1alpha1/clusterversion_types_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/clusterversion_webhook.go b/apis/apps/v1alpha1/clusterversion_webhook.go index 41fd39a7f..16c37b92b 100644 --- a/apis/apps/v1alpha1/clusterversion_webhook.go +++ b/apis/apps/v1alpha1/clusterversion_webhook.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -157,7 +157,7 @@ func (r *ClusterVersion) GetInconsistentComponentsInfo(clusterDef *ClusterDefini } func getComponentDefNotFoundMsg(invalidComponentDefNames []string, clusterDefName string) string { - return fmt.Sprintf(" %v is not found in ClusterDefinition.spec.componentDefs[*].name %s", + return fmt.Sprintf(" %v is not found in ClusterDefinition.spec.componentDefs[*].name of %s", invalidComponentDefNames, clusterDefName) } diff --git a/apis/apps/v1alpha1/clusterversion_webhook_test.go b/apis/apps/v1alpha1/clusterversion_webhook_test.go index 2921c93a3..ca49bec4f 100644 --- a/apis/apps/v1alpha1/clusterversion_webhook_test.go +++ b/apis/apps/v1alpha1/clusterversion_webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -57,16 +57,13 @@ var _ = Describe("clusterVersion webhook", func() { clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName) Expect(testCtx.CreateObj(ctx, clusterDef)).Should(Succeed()) - Eventually(func() bool { - By("By testing component name is not found in cluserDefinition") - clusterVersion.Spec.ComponentVersions[1].ComponentDefRef = "proxy1" - Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).ShouldNot(Succeed()) + By("By testing component name is not found in clusterDefinition") + clusterVersion.Spec.ComponentVersions[1].ComponentDefRef = "proxy1" + Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).ShouldNot(Succeed()) - By("By creating an clusterVersion") - clusterVersion.Spec.ComponentVersions[1].ComponentDefRef = "proxy" - err := testCtx.CheckedCreateObj(ctx, clusterVersion) - return err == nil - }).Should(BeTrue()) + By("By creating an clusterVersion") + clusterVersion.Spec.ComponentVersions[1].ComponentDefRef = "proxy" + Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).Should(Succeed()) By("By testing create a new clusterVersion with invalid config template") clusterVersionDup := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName+"-for-config") diff --git a/apis/apps/v1alpha1/componentclassdefinition_types.go b/apis/apps/v1alpha1/componentclassdefinition_types.go new file mode 100644 index 000000000..acd4dd9e8 --- /dev/null +++ b/apis/apps/v1alpha1/componentclassdefinition_types.go @@ -0,0 +1,148 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +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" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// ComponentClassDefinitionSpec defines the desired state of ComponentClassDefinition +type ComponentClassDefinitionSpec struct { + // group defines a list of class series that conform to the same constraint. + // +optional + Groups []ComponentClassGroup `json:"groups,omitempty"` +} + +type ComponentClassGroup struct { + // resourceConstraintRef reference to the resource constraint object name, indicates that the series + // defined below all conform to the constraint. + // +kubebuilder:validation:Required + ResourceConstraintRef string `json:"resourceConstraintRef"` + + // template is a class definition template that uses the Go template syntax and allows for variable declaration. + // When defining a class in Series, specifying the variable's value is sufficient, as the complete class + // definition will be generated through rendering the template. + // + // For example: + // template: | + // cpu: "{{ or .cpu 1 }}" + // memory: "{{ or .memory 4 }}Gi" + // + // +optional + Template string `json:"template,omitempty"` + + // vars defines the variables declared in the template and will be used to generating the complete class definition by + // render the template. + // +listType=set + // +optional + Vars []string `json:"vars,omitempty"` + + // series is a series of class definitions. + // +optional + Series []ComponentClassSeries `json:"series,omitempty"` +} + +type ComponentClassSeries struct { + // namingTemplate is a template that uses the Go template syntax and allows for referencing variables defined + // in ComponentClassGroup.Template. This enables dynamic generation of class names. + // For example: + // name: "general-{{ .cpu }}c{{ .memory }}g" + // +optional + NamingTemplate string `json:"namingTemplate,omitempty"` + + // classes are definitions of classes that come in two forms. In the first form, only ComponentClass.Args + // need to be defined, and the complete class definition is generated by rendering the ComponentClassGroup.Template + // and Name. In the second form, the Name, CPU and Memory must be defined. + // +optional + Classes []ComponentClass `json:"classes,omitempty"` +} + +type ComponentClass struct { + // name is the class name + // +optional + Name string `json:"name,omitempty"` + + // args are variable's value + // +optional + Args []string `json:"args,omitempty"` + + // the CPU of the class + // +optional + CPU resource.Quantity `json:"cpu,omitempty"` + + // the memory of the class + // +optional + Memory resource.Quantity `json:"memory,omitempty"` +} + +// ComponentClassDefinitionStatus defines the observed state of ComponentClassDefinition +type ComponentClassDefinitionStatus struct { + // observedGeneration is the most recent generation observed for this + // ComponentClassDefinition. It corresponds to the ComponentClassDefinition's generation, which is + // updated on mutation by the API Server. + // +optional + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // classes is the list of classes that have been observed for this ComponentClassDefinition + Classes []ComponentClassInstance `json:"classes,omitempty"` +} + +type ComponentClassInstance struct { + ComponentClass `json:",inline"` + + // resourceConstraintRef reference to the resource constraint object name. + ResourceConstraintRef string `json:"resourceConstraintRef,omitempty"` +} + +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={kubeblocks},scope=Cluster,shortName=ccd + +// ComponentClassDefinition is the Schema for the componentclassdefinitions API +type ComponentClassDefinition struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ComponentClassDefinitionSpec `json:"spec,omitempty"` + Status ComponentClassDefinitionStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ComponentClassDefinitionList contains a list of ComponentClassDefinition +type ComponentClassDefinitionList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ComponentClassDefinition `json:"items"` +} + +func init() { + SchemeBuilder.Register(&ComponentClassDefinition{}, &ComponentClassDefinitionList{}) +} + +func (r *ComponentClass) ToResourceRequirements() corev1.ResourceRequirements { + requests := corev1.ResourceList{ + corev1.ResourceCPU: r.CPU, + corev1.ResourceMemory: r.Memory, + } + return corev1.ResourceRequirements{Requests: requests, Limits: requests} +} diff --git a/apis/apps/v1alpha1/classfamily_types.go b/apis/apps/v1alpha1/componentresourceconstraint_types.go similarity index 69% rename from apis/apps/v1alpha1/classfamily_types.go rename to apis/apps/v1alpha1/componentresourceconstraint_types.go index e833e3019..b6e424196 100644 --- a/apis/apps/v1alpha1/classfamily_types.go +++ b/apis/apps/v1alpha1/componentresourceconstraint_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -24,20 +24,20 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// ClassFamilySpec defines the desired state of ClassFamily -type ClassFamilySpec struct { - // Class family models, generally, a model is a specific constraint for CPU, memory and their relation. - Models []ClassFamilyModel `json:"models,omitempty"` +// ComponentResourceConstraintSpec defines the desired state of ComponentResourceConstraint +type ComponentResourceConstraintSpec struct { + // Component resource constraints + Constraints []ResourceConstraint `json:"constraints,omitempty"` } -type ClassFamilyModel struct { +type ResourceConstraint struct { // The constraint for vcpu cores. // +kubebuilder:validation:Required - CPU CPUConstraint `json:"cpu,omitempty"` + CPU CPUConstraint `json:"cpu"` // The constraint for memory size. // +kubebuilder:validation:Required - Memory MemoryConstraint `json:"memory,omitempty"` + Memory MemoryConstraint `json:"memory"` } type CPUConstraint struct { @@ -91,53 +91,52 @@ type MemoryConstraint struct { MinPerCPU *resource.Quantity `json:"minPerCPU,omitempty"` } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true -// +kubebuilder:resource:categories={kubeblocks,all},scope=Cluster,shortName=cf +// +kubebuilder:resource:categories={kubeblocks,all},scope=Cluster,shortName=crc -// ClassFamily is the Schema for the classfamilies API -type ClassFamily struct { +// ComponentResourceConstraint is the Schema for the componentresourceconstraints API +type ComponentResourceConstraint struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` - Spec ClassFamilySpec `json:"spec,omitempty"` + Spec ComponentResourceConstraintSpec `json:"spec,omitempty"` } // +kubebuilder:object:root=true -// ClassFamilyList contains a list of ClassFamily -type ClassFamilyList struct { +// ComponentResourceConstraintList contains a list of ComponentResourceConstraint +type ComponentResourceConstraintList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` - Items []ClassFamily `json:"items"` + Items []ComponentResourceConstraint `json:"items"` } func init() { - SchemeBuilder.Register(&ClassFamily{}, &ClassFamilyList{}) + SchemeBuilder.Register(&ComponentResourceConstraint{}, &ComponentResourceConstraintList{}) } -// ValidateCPU validate if the CPU matches the class family model constraint -func (m *ClassFamilyModel) ValidateCPU(cpu resource.Quantity) bool { - if m == nil { - return false - } +// ValidateCPU validate if the CPU matches the resource constraints +func (m ResourceConstraint) ValidateCPU(cpu resource.Quantity) bool { if m.CPU.Min != nil && m.CPU.Min.Cmp(cpu) > 0 { return false } if m.CPU.Max != nil && m.CPU.Max.Cmp(cpu) < 0 { return false } + if m.CPU.Step != nil && inf.NewDec(1, 0).QuoExact(cpu.AsDec(), m.CPU.Step.AsDec()).Scale() != 0 { + return false + } if m.CPU.Slots != nil && slices.Index(m.CPU.Slots, cpu) < 0 { return false } return true } -// ValidateMemory validate if the memory matches the class family model constraint -func (m *ClassFamilyModel) ValidateMemory(cpu *resource.Quantity, memory *resource.Quantity) bool { - if m == nil { - return false - } - +// ValidateMemory validate if the memory matches the resource constraints +func (m ResourceConstraint) ValidateMemory(cpu *resource.Quantity, memory *resource.Quantity) bool { if memory == nil { return true } @@ -158,17 +157,13 @@ func (m *ClassFamilyModel) ValidateMemory(cpu *resource.Quantity, memory *resour return true } -// ValidateResourceRequirements validate if the resource matches the class family model constraints -func (m *ClassFamilyModel) ValidateResourceRequirements(r *corev1.ResourceRequirements) bool { +// ValidateResourceRequirements validate if the resource matches the resource constraints +func (m ResourceConstraint) ValidateResourceRequirements(r *corev1.ResourceRequirements) bool { var ( cpu = r.Requests.Cpu() memory = r.Requests.Memory() ) - if m == nil { - return false - } - if cpu.IsZero() && memory.IsZero() { return true } @@ -184,16 +179,29 @@ func (m *ClassFamilyModel) ValidateResourceRequirements(r *corev1.ResourceRequir return true } -// FindMatchingModels find all class family models that resource matches -func (c *ClassFamily) FindMatchingModels(r *corev1.ResourceRequirements) []ClassFamilyModel { +// FindMatchingConstraints find all constraints that resource matches +func (c *ComponentResourceConstraint) FindMatchingConstraints(r *corev1.ResourceRequirements) []ResourceConstraint { if c == nil { return nil } - var models []ClassFamilyModel - for _, model := range c.Spec.Models { - if model.ValidateResourceRequirements(r) { - models = append(models, model) + var constraints []ResourceConstraint + for _, constraint := range c.Spec.Constraints { + if constraint.ValidateResourceRequirements(r) { + constraints = append(constraints, constraint) } } - return models + return constraints +} + +func (c *ComponentResourceConstraint) MatchClass(class *ComponentClassInstance) bool { + request := corev1.ResourceList{ + corev1.ResourceCPU: class.CPU, + corev1.ResourceMemory: class.Memory, + } + resource := &corev1.ResourceRequirements{ + Limits: request, + Requests: request, + } + constraints := c.FindMatchingConstraints(resource) + return len(constraints) > 0 } diff --git a/apis/apps/v1alpha1/componentresourceconstraint_types_test.go b/apis/apps/v1alpha1/componentresourceconstraint_types_test.go new file mode 100644 index 000000000..fd39063f6 --- /dev/null +++ b/apis/apps/v1alpha1/componentresourceconstraint_types_test.go @@ -0,0 +1,143 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +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 ( + "testing" + + "github.com/stretchr/testify/assert" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "k8s.io/apimachinery/pkg/util/yaml" +) + +const resourceConstraints = ` +# API scope: cluster +apiVersion: "apps.kubeblocks.io/v1alpha1" +kind: "ComponentResourceConstraint" +metadata: + name: kb-resource-constraint-general +spec: + constraints: + - cpu: + min: 0.5 + max: 128 + step: 0.5 + memory: + sizePerCPU: 4Gi + - cpu: + slots: [0.1, 0.2, 0.4, 0.6, 0.8, 1] + memory: + minPerCPU: 200Mi + - cpu: + min: 0.1 + max: 64 + step: 0.1 + memory: + minPerCPU: 4Gi + maxPerCPU: 8Gi +` + +var ( + cf ComponentResourceConstraint +) + +func init() { + if err := yaml.Unmarshal([]byte(resourceConstraints), &cf); err != nil { + panic("Failed to unmarshal resource constraints: %v" + err.Error()) + } +} + +func TestResourceConstraints(t *testing.T) { + cases := []struct { + desc string + cpu string + memory string + expect bool + }{ + { + desc: "test memory constraint with sizePerCPU", + cpu: "0.5", + memory: "2Gi", + expect: true, + }, + { + desc: "test memory constraint with unit Mi", + cpu: "0.2", + memory: "40Mi", + expect: true, + }, + { + desc: "test memory constraint with minPerCPU and maxPerCPU", + cpu: "1", + memory: "6Gi", + expect: true, + }, + { + desc: "test cpu with decimal", + cpu: "0.3", + memory: "1.2Gi", + expect: true, + }, + { + desc: "test CPU with invalid step", + cpu: "100.6", + memory: "402.4Gi", + expect: false, + }, + { + desc: "test CPU with invalid step", + cpu: "1.05", + memory: "4.2Gi", + expect: false, + }, + { + desc: "test invalid memory", + cpu: "2", + memory: "20Gi", + expect: false, + }, + { + desc: "test invalid memory", + cpu: "2", + memory: "6Gi", + expect: false, + }, + } + + for _, item := range cases { + var ( + cpu = resource.MustParse(item.cpu) + memory = resource.MustParse(item.memory) + ) + requirements := &corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceCPU: cpu, + corev1.ResourceMemory: memory, + }, + } + assert.Equal(t, item.expect, len(cf.FindMatchingConstraints(requirements)) > 0) + + class := &ComponentClassInstance{ + ComponentClass: ComponentClass{ + CPU: cpu, + Memory: memory, + }, + } + assert.Equal(t, item.expect, cf.MatchClass(class)) + } +} diff --git a/apis/apps/v1alpha1/configconstraint_types.go b/apis/apps/v1alpha1/configconstraint_types.go index fa0b9eb5d..8c08e083a 100644 --- a/apis/apps/v1alpha1/configconstraint_types.go +++ b/apis/apps/v1alpha1/configconstraint_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -66,10 +66,9 @@ type ConfigConstraintSpec struct { // ConfigConstraintStatus defines the observed state of ConfigConstraint. type ConfigConstraintStatus struct { - // phase is status of configuration template, when set to AvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. - // +kubebuilder:validation:Enum={Available,Unavailable,Deleting} + // phase is status of configuration template, when set to CCAvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. // +optional - Phase Phase `json:"phase,omitempty"` + Phase ConfigConstraintPhase `json:"phase,omitempty"` // message field describes the reasons of abnormal status. // +optional @@ -82,6 +81,10 @@ type ConfigConstraintStatus struct { ObservedGeneration int64 `json:"observedGeneration,omitempty"` } +func (cs ConfigConstraintStatus) IsConfigConstraintTerminalPhases() bool { + return cs.Phase == CCAvailablePhase +} + type CustomParametersValidation struct { // schema provides a way for providers to validate the changed parameters through json. // +kubebuilder:validation:Schemaless @@ -195,6 +198,9 @@ type IniConfig struct { SectionName string `json:"sectionName,omitempty"` } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Cluster,shortName=cc diff --git a/apis/apps/v1alpha1/doc.go b/apis/apps/v1alpha1/doc.go new file mode 100644 index 000000000..85cd38698 --- /dev/null +++ b/apis/apps/v1alpha1/doc.go @@ -0,0 +1,25 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +//go:generate go run ../../../hack/docgen/api/main.go -api-dir . -config ../../../hack/docgen/api/gen-api-doc-config.json -template-dir ../../../hack/docgen/api/template -out-file ../../../docs/user_docs/api-reference/cluster.md + +// +k8s:deepcopy-gen=package,register +// +k8s:openapi-gen=true +// +groupName=apps.kubeblocks.io +package v1alpha1 diff --git a/apis/apps/v1alpha1/groupversion_info.go b/apis/apps/v1alpha1/groupversion_info.go index fc705499f..2d740214a 100644 --- a/apis/apps/v1alpha1/groupversion_info.go +++ b/apis/apps/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/opsrequest_conditions.go b/apis/apps/v1alpha1/opsrequest_conditions.go index df9e58233..f31af7c40 100644 --- a/apis/apps/v1alpha1/opsrequest_conditions.go +++ b/apis/apps/v1alpha1/opsrequest_conditions.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,7 +25,7 @@ import ( const ( // condition types - + ConditionTypeCancelled = "Cancelled" ConditionTypeProgressing = "Progressing" ConditionTypeValidated = "Validated" ConditionTypeSucceed = "Succeed" @@ -48,11 +48,14 @@ const ( ReasonReconfigureNoChanged = "ReconfigureNoChanged" ReasonReconfigureSucceed = "ReconfigureSucceed" ReasonReconfigureRunning = "ReconfigureRunning" - ReasonClusterPhaseMisMatch = "ClusterPhaseMisMatch" + ReasonClusterPhaseMismatch = "ClusterPhaseMismatch" ReasonOpsTypeNotSupported = "OpsTypeNotSupported" ReasonValidateFailed = "ValidateFailed" ReasonClusterNotFound = "ClusterNotFound" ReasonOpsRequestFailed = "OpsRequestFailed" + ReasonOpsCanceling = "Canceling" + ReasonOpsCancelFailed = "CancelFailed" + ReasonOpsCancelSucceed = "CancelSucceed" ) func (r *OpsRequest) SetStatusCondition(condition metav1.Condition) { @@ -71,7 +74,45 @@ func NewProgressingCondition(ops *OpsRequest) *metav1.Condition { } } -// NewValidatePassedCondition creates a condition that the operation validation. +// NewCancelingCondition the controller is canceling the OpsRequest +func NewCancelingCondition(ops *OpsRequest) *metav1.Condition { + return &metav1.Condition{ + Type: ConditionTypeCancelled, + Status: metav1.ConditionFalse, + Reason: ReasonOpsCanceling, + LastTransitionTime: metav1.Now(), + Message: fmt.Sprintf(`Start to cancel the OpsRequest "%s" in Cluster: "%s"`, + ops.Name, ops.Spec.ClusterRef), + } +} + +// NewCancelFailedCondition creates a condition for canceling failed. +func NewCancelFailedCondition(ops *OpsRequest, err error) *metav1.Condition { + msg := fmt.Sprintf(`Failed to cancel OpsRequest "%s"`, ops.Name) + if err != nil { + msg = err.Error() + } + return &metav1.Condition{ + Type: ConditionTypeCancelled, + Status: metav1.ConditionTrue, + Reason: ReasonOpsCancelFailed, + LastTransitionTime: metav1.Now(), + Message: msg, + } +} + +// NewCancelSucceedCondition creates a condition for canceling successfully. +func NewCancelSucceedCondition(opsName string) *metav1.Condition { + return &metav1.Condition{ + Type: ConditionTypeCancelled, + Status: metav1.ConditionTrue, + Reason: ReasonOpsCancelSucceed, + LastTransitionTime: metav1.Now(), + Message: fmt.Sprintf(`Cancel OpsRequest "%s" successfully`, opsName), + } +} + +// NewValidatePassedCondition creates a condition for operation validation to pass. func NewValidatePassedCondition(opsRequestName string) *metav1.Condition { return &metav1.Condition{ Type: ConditionTypeValidated, @@ -82,7 +123,7 @@ func NewValidatePassedCondition(opsRequestName string) *metav1.Condition { } } -// NewValidateFailedCondition creates a condition that the operation validation. +// NewValidateFailedCondition creates a condition for operation validation failure. func NewValidateFailedCondition(reason, message string) *metav1.Condition { return &metav1.Condition{ Type: ConditionTypeValidated, diff --git a/apis/apps/v1alpha1/opsrequest_conditions_test.go b/apis/apps/v1alpha1/opsrequest_conditions_test.go index 9d25dd37b..4d4b1a7a4 100644 --- a/apis/apps/v1alpha1/opsrequest_conditions_test.go +++ b/apis/apps/v1alpha1/opsrequest_conditions_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -33,7 +33,7 @@ func TestNewAllCondition(t *testing.T) { NewSucceedCondition(opsRequest) NewVerticalScalingCondition(opsRequest) NewUpgradingCondition(opsRequest) - NewValidateFailedCondition(ReasonClusterPhaseMisMatch, "fail") + NewValidateFailedCondition(ReasonClusterPhaseMismatch, "fail") NewFailedCondition(opsRequest, nil) NewFailedCondition(opsRequest, errors.New("opsRequest run failed")) diff --git a/apis/apps/v1alpha1/opsrequest_types.go b/apis/apps/v1alpha1/opsrequest_types.go index 4e540b2ac..9d2f19a78 100644 --- a/apis/apps/v1alpha1/opsrequest_types.go +++ b/apis/apps/v1alpha1/opsrequest_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -25,13 +25,21 @@ import ( // TODO: @wangyelei could refactor to ops group // OpsRequestSpec defines the desired state of OpsRequest +// +kubebuilder:validation:XValidation:rule="has(self.cancel) && self.cancel ? (self.type in ['VerticalScaling', 'HorizontalScaling']) : true",message="forbidden to cancel the opsRequest which type not in ['VerticalScaling','HorizontalScaling']" type OpsRequestSpec struct { // clusterRef references clusterDefinition. // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.clusterRef" ClusterRef string `json:"clusterRef"` + // cancel defines the action to cancel the Pending/Creating/Running opsRequest, supported types: [VerticalScaling, HorizontalScaling]. + // once cancel is set to true, this opsRequest will be canceled and modifying this property again will not take effect. + // +optional + Cancel bool `json:"cancel,omitempty"` + // type defines the operation type. // +kubebuilder:validation:Required + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.type" Type OpsType `json:"type"` // ttlSecondsAfterSucceed OpsRequest will be deleted after TTLSecondsAfterSucceed second when OpsRequest.status.phase is Succeed. @@ -40,6 +48,7 @@ type OpsRequestSpec struct { // upgrade specifies the cluster version by specifying clusterVersionRef. // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.upgrade" Upgrade *Upgrade `json:"upgrade,omitempty"` // horizontalScaling defines what component need to horizontal scale the specified replicas. @@ -48,6 +57,7 @@ type OpsRequestSpec struct { // +patchStrategy=merge,retainKeys // +listType=map // +listMapKey=componentName + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.horizontalScaling" HorizontalScalingList []HorizontalScaling `json:"horizontalScaling,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` // volumeExpansion defines what component and volumeClaimTemplate need to expand the specified storage. @@ -56,6 +66,7 @@ type OpsRequestSpec struct { // +patchStrategy=merge,retainKeys // +listType=map // +listMapKey=componentName + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.volumeExpansion" VolumeExpansionList []VolumeExpansion `json:"volumeExpansion,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` // restart the specified component. @@ -64,6 +75,7 @@ type OpsRequestSpec struct { // +patchStrategy=merge,retainKeys // +listType=map // +listMapKey=componentName + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.restart" RestartList []ComponentOps `json:"restart,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` // verticalScaling defines what component need to vertical scale the specified compute resources. @@ -72,10 +84,12 @@ type OpsRequestSpec struct { // +patchStrategy=merge,retainKeys // +listType=map // +listMapKey=componentName + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.verticalScaling" VerticalScalingList []VerticalScaling `json:"verticalScaling,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` // reconfigure defines the variables that need to input when updating configuration. // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.reconfigure" Reconfigure *Reconfigure `json:"reconfigure,omitempty"` // expose defines services the component needs to expose. @@ -84,7 +98,13 @@ type OpsRequestSpec struct { // +patchStrategy=merge,retainKeys // +listType=map // +listMapKey=componentName + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.expose" ExposeList []Expose `json:"expose,omitempty" patchStrategy:"merge,retainKeys" patchMergeKey:"componentName"` + + // cluster RestoreFrom backup or point in time + // +optional + // +kubebuilder:validation:XValidation:rule="self == oldSelf",message="forbidden to update spec.restoreFrom" + RestoreFrom *RestoreFromSpec `json:"restoreFrom,omitempty"` } // ComponentOps defines the common variables of component scope operations. @@ -106,9 +126,12 @@ type VerticalScaling struct { ComponentOps `json:",inline"` // resources specifies the computing resource size of verticalScaling. - // +kubebuilder:validation:Required // +kubebuilder:pruning:PreserveUnknownFields corev1.ResourceRequirements `json:",inline"` + + // class specifies the class name of the component + // +optional + Class string `json:"class,omitempty"` } // VolumeExpansion defines the variables of volume expansion operation. @@ -220,6 +243,42 @@ type Expose struct { Services []ClusterComponentService `json:"services"` } +type RestoreFromSpec struct { + // use the backup name and component name for restore, support for multiple components' recovery. + // +optional + Backup []BackupRefSpec `json:"backup,omitempty"` + + // specified the point in time to recovery + // +optional + PointInTime *PointInTimeRefSpec `json:"pointInTime,omitempty"` +} + +type RefNamespaceName struct { + // specified the name + // +optional + Name string `json:"name,omitempty"` + + // specified the namespace + // +optional + Namespace string `json:"namespace,omitempty"` +} + +type BackupRefSpec struct { + // specify a reference backup to restore + // +optional + Ref RefNamespaceName `json:"ref,omitempty"` +} + +type PointInTimeRefSpec struct { + // specify the time point to restore, with UTC as the time zone. + // +optional + Time *metav1.Time `json:"time,omitempty"` + + // specify a reference source cluster to restore + // +optional + Ref RefNamespaceName `json:"ref,omitempty"` +} + // OpsRequestStatus defines the observed state of OpsRequest type OpsRequestStatus struct { @@ -250,6 +309,10 @@ type OpsRequestStatus struct { // +optional CompletionTimestamp metav1.Time `json:"completionTimestamp,omitempty"` + // CancelTimestamp defines cancel time. + // +optional + CancelTimestamp metav1.Time `json:"cancelTimestamp,omitempty"` + // reconfiguringStatus defines the status information of reconfiguring. // +optional ReconfiguringStatus *ReconfiguringStatus `json:"reconfiguringStatus,omitempty"` @@ -300,6 +363,10 @@ type LastComponentConfiguration struct { // +optional corev1.ResourceRequirements `json:",inline,omitempty"` + // the last class name of the component. + // +optional + Class string `json:"class,omitempty"` + // volumeClaimTemplates records the last volumeClaimTemplates of the component. // +optional VolumeClaimTemplates []OpsRequestVolumeClaimTemplate `json:"volumeClaimTemplates,omitempty"` @@ -401,6 +468,8 @@ type UpdatedParameters struct { UpdatedKeys map[string]string `json:"updatedKeys,omitempty"` } +// +genclient +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks,all},shortName=ops diff --git a/apis/apps/v1alpha1/opsrequest_types_test.go b/apis/apps/v1alpha1/opsrequest_types_test.go index d041caa7f..84eb6d728 100644 --- a/apis/apps/v1alpha1/opsrequest_types_test.go +++ b/apis/apps/v1alpha1/opsrequest_types_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/apps/v1alpha1/opsrequest_webhook.go b/apis/apps/v1alpha1/opsrequest_webhook.go index db701388b..ec0df5fda 100644 --- a/apis/apps/v1alpha1/opsrequest_webhook.go +++ b/apis/apps/v1alpha1/opsrequest_webhook.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -35,6 +35,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook" + + "github.com/apecloud/kubeblocks/internal/constant" ) // log is for logging in this package. @@ -65,16 +67,23 @@ func (r *OpsRequest) ValidateCreate() error { // ValidateUpdate implements webhook.Validator so a webhook will be registered for the type func (r *OpsRequest) ValidateUpdate(old runtime.Object) error { opsrequestlog.Info("validate update", "name", r.Name) - lastOpsRequest := old.(*OpsRequest) - if r.isForbiddenUpdate() && !reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) { - return fmt.Errorf("update OpsRequest: %s is forbidden when status.Phase is %s", r.Name, r.Status.Phase) - } + lastOpsRequest := old.(*OpsRequest).DeepCopy() // if no spec updated, we should skip validation. // if not, we can not delete the OpsRequest when cluster has been deleted. // because when cluster not existed, r.validate will report an error. if reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) { return nil } + + if r.IsComplete() { + return fmt.Errorf("update OpsRequest: %s is forbidden when status.Phase is %s", r.Name, r.Status.Phase) + } + + // Keep the cancel consistent between the two opsRequest for comparing the diff. + lastOpsRequest.Spec.Cancel = r.Spec.Cancel + if !reflect.DeepEqual(lastOpsRequest.Spec, r.Spec) { + return fmt.Errorf("update OpsRequest: %s is forbidden except for cancel when status.Phase is %s", r.Name, r.Status.Phase) + } return r.validateEntry(false) } @@ -84,15 +93,18 @@ func (r *OpsRequest) ValidateDelete() error { return nil } -// IsForbiddenUpdate OpsRequest cannot modify the spec when status is in [Succeed,Running,Failed]. -func (r *OpsRequest) isForbiddenUpdate() bool { - return slices.Contains([]OpsPhase{OpsCreatingPhase, OpsRunningPhase, OpsSucceedPhase, OpsFailedPhase}, r.Status.Phase) +// IsComplete checks if opsRequest has been completed. +func (r *OpsRequest) IsComplete(phases ...OpsPhase) bool { + if len(phases) == 0 { + return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, r.Status.Phase) + } + return slices.Contains([]OpsPhase{OpsCancelledPhase, OpsSucceedPhase, OpsFailedPhase}, phases[0]) } // validateClusterPhase validates whether the current cluster state supports the OpsRequest func (r *OpsRequest) validateClusterPhase(cluster *Cluster) error { opsBehaviour := OpsRequestBehaviourMapper[r.Spec.Type] - // if the OpsType has no cluster phases, ignores it + // if the OpsType has no cluster phases, ignore it if len(opsBehaviour.FromClusterPhases) == 0 { return nil } @@ -121,7 +133,7 @@ func (r *OpsRequest) validateClusterPhase(cluster *Cluster) error { // check if the opsRequest can be executed in the current cluster phase unless this opsRequest is reentrant. if !slices.Contains(opsBehaviour.FromClusterPhases, cluster.Status.Phase) && !slices.Contains(opsNamesInQueue, r.Name) { - return fmt.Errorf("opsRequest kind: %s is forbidden when Cluster.status.Phase is %s", r.Spec.Type, cluster.Status.Phase) + return fmt.Errorf("OpsRequest.spec.type=%s is forbidden when Cluster.status.phase=%s", r.Spec.Type, cluster.Status.Phase) } return nil } @@ -228,6 +240,7 @@ func (r *OpsRequest) validateVerticalScaling(cluster *Cluster) error { componentNames := make([]string, len(verticalScalingList)) for i, v := range verticalScalingList { componentNames[i] = v.ComponentName + if invalidValue, err := validateVerticalResourceList(v.Requests); err != nil { return invalidValueError(invalidValue, err.Error()) } @@ -301,7 +314,13 @@ func (r *OpsRequest) validateVolumeExpansion(ctx context.Context, cli client.Cli if err := r.checkComponentExistence(cluster, componentNames); err != nil { return err } - + runningOpsList, err := GetRunningOpsByOpsType(ctx, cli, r.Spec.ClusterRef, r.Namespace, string(VolumeExpansionType)) + if err != nil { + return err + } + if len(runningOpsList) > 0 && runningOpsList[0].Name != r.Name { + return fmt.Errorf("existing other VolumeExpansion OpsRequest: %s is running in Cluster: %s, handle this OpsRequest first", runningOpsList[0].Name, cluster.Name) + } return r.checkVolumesAllowExpansion(ctx, cli, cluster) } @@ -331,6 +350,7 @@ func (r *OpsRequest) checkVolumesAllowExpansion(ctx context.Context, cli client. existInSpec bool storageClassName *string allowExpansion bool + requestStorage resource.Quantity } // component name -> vct name -> entity @@ -340,38 +360,43 @@ func (r *OpsRequest) checkVolumesAllowExpansion(ctx context.Context, cli client. if _, ok := vols[comp.ComponentName]; !ok { vols[comp.ComponentName] = make(map[string]Entity) } - vols[comp.ComponentName][vct.Name] = Entity{false, nil, false} + vols[comp.ComponentName][vct.Name] = Entity{false, nil, false, vct.Storage} } } - // traverse the spec to update volumes for _, comp := range cluster.Spec.ComponentSpecs { if _, ok := vols[comp.Name]; !ok { continue // ignore not-exist component } for _, vct := range comp.VolumeClaimTemplates { - if _, ok := vols[comp.Name][vct.Name]; !ok { + e, ok := vols[comp.Name][vct.Name] + if !ok { continue } - vols[comp.Name][vct.Name] = Entity{true, vct.Spec.StorageClassName, false} + e.existInSpec = true + e.storageClassName = vct.Spec.StorageClassName + vols[comp.Name][vct.Name] = e } } // check all used storage classes + var err error for cname, compVols := range vols { for vname := range compVols { e := vols[cname][vname] if !e.existInSpec { continue } - if e.storageClassName == nil { - e.storageClassName = r.getSCNameByPvc(ctx, cli, cname, vname) + e.storageClassName, err = r.getSCNameByPvcAndCheckStorageSize(ctx, cli, cname, vname, e.requestStorage) + if err != nil { + return err } allowExpansion, err := r.checkStorageClassAllowExpansion(ctx, cli, e.storageClassName) if err != nil { continue // ignore the error and take it as not-supported } - vols[cname][vname] = Entity{e.existInSpec, e.storageClassName, allowExpansion} + e.allowExpansion = allowExpansion + vols[cname][vname] = e } } @@ -426,23 +451,30 @@ func (r *OpsRequest) checkStorageClassAllowExpansion(ctx context.Context, return *storageClass.AllowVolumeExpansion, nil } -// getSCNameByPvc gets the storageClassName by pvc. -func (r *OpsRequest) getSCNameByPvc(ctx context.Context, +// getSCNameByPvcAndCheckStorageSize gets the storageClassName by pvc and checks if the storage size is valid. +func (r *OpsRequest) getSCNameByPvcAndCheckStorageSize(ctx context.Context, cli client.Client, compName, - vctName string) *string { + vctName string, + requestStorage resource.Quantity) (*string, error) { pvcList := &corev1.PersistentVolumeClaimList{} if err := cli.List(ctx, pvcList, client.InNamespace(r.Namespace), client.MatchingLabels{ - "app.kubernetes.io/instance": r.Spec.ClusterRef, - "apps.kubeblocks.io/component-name": compName, - "vct.kubeblocks.io/name": vctName, + constant.AppInstanceLabelKey: r.Spec.ClusterRef, + constant.KBAppComponentLabelKey: compName, + constant.VolumeClaimTemplateNameLabelKey: vctName, }, client.Limit(1)); err != nil { - return nil + return nil, err } if len(pvcList.Items) == 0 { - return nil + return nil, nil + } + pvc := pvcList.Items[0] + previousValue := *pvc.Status.Capacity.Storage() + if requestStorage.Cmp(previousValue) < 0 { + return nil, fmt.Errorf(`requested storage size of volumeClaimTemplate "%s" can not less than status.capacity.storage "%s" `, + vctName, previousValue.String()) } - return pvcList.Items[0].Spec.StorageClassName + return pvc.Spec.StorageClassName, nil } // validateVerticalResourceList checks if k8s resourceList is legal @@ -452,6 +484,7 @@ func validateVerticalResourceList(resourceList map[corev1.ResourceName]resource. return string(k), fmt.Errorf("resource key is not cpu or memory or hugepages- ") } } + return "", nil } @@ -462,3 +495,26 @@ func notEmptyError(target string) error { func invalidValueError(target string, value string) error { return fmt.Errorf(`invalid value for "%s": %s`, target, value) } + +// GetRunningOpsByOpsType gets the running opsRequests by type. +func GetRunningOpsByOpsType(ctx context.Context, cli client.Client, + clusterName, namespace, opsType string) ([]OpsRequest, error) { + opsRequestList := &OpsRequestList{} + if err := cli.List(ctx, opsRequestList, client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterName, + constant.OpsRequestTypeLabelKey: opsType, + }, client.InNamespace(namespace)); err != nil { + return nil, err + } + if len(opsRequestList.Items) == 0 { + return nil, nil + } + var runningOpsList []OpsRequest + for _, v := range opsRequestList.Items { + if v.Status.Phase == OpsRunningPhase { + runningOpsList = append(runningOpsList, v) + break + } + } + return runningOpsList, nil +} diff --git a/apis/apps/v1alpha1/opsrequest_webhook_test.go b/apis/apps/v1alpha1/opsrequest_webhook_test.go index d5ef49961..e55b1c8d5 100644 --- a/apis/apps/v1alpha1/opsrequest_webhook_test.go +++ b/apis/apps/v1alpha1/opsrequest_webhook_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,10 +31,15 @@ import ( "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/kubectl/pkg/util/storage" "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/internal/constant" ) var _ = Describe("OpsRequest webhook", func() { - + const ( + componentName = "replicasets" + proxyComponentName = "proxy" + ) var ( randomStr = testCtx.GetRandomStr() clusterDefinitionName = "opswebhook-mysql-definition-" + randomStr @@ -42,8 +47,6 @@ var _ = Describe("OpsRequest webhook", func() { clusterVersionNameForUpgrade = "opswebhook-mysql-upgrade-" + randomStr clusterName = "opswebhook-mysql-" + randomStr opsRequestName = "opswebhook-mysql-ops-" + randomStr - replicaSetComponentName = "replicasets" - proxyComponentName = "proxy" ) cleanupObjects := func() { // Add any setup steps that needs to be executed before each test @@ -88,13 +91,43 @@ var _ = Describe("OpsRequest webhook", func() { AllowVolumeExpansion: &allowVolumeExpansion, } err := testCtx.CheckedCreateObj(ctx, storageClass) - if err != nil { - fmt.Printf("create storage class error: %s\n", err.Error()) - } Expect(err).Should(BeNil()) return storageClass } + createPVC := func(clusterName, compName, storageClassName, vctName string, index int) *corev1.PersistentVolumeClaim { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s-%s-%d", vctName, clusterName, compName, index), + Namespace: testCtx.DefaultNamespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.VolumeClaimTemplateNameLabelKey: vctName, + constant.KBAppComponentLabelKey: compName, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": resource.MustParse("1Gi"), + }, + }, + StorageClassName: &storageClassName, + }, + } + Expect(testCtx.CheckedCreateObj(ctx, pvc)).ShouldNot(HaveOccurred()) + Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(pvc), pvc)).ShouldNot(HaveOccurred()) + patch := client.MergeFrom(pvc.DeepCopy()) + pvc.Status.Capacity = corev1.ResourceList{ + "storage": resource.MustParse("1Gi"), + } + Expect(k8sClient.Status().Patch(ctx, pvc, patch)).ShouldNot(HaveOccurred()) + return pvc + } + notFoundComponentsString := func(notFoundComponents string) string { return fmt.Sprintf("components: [%s] not found", notFoundComponents) } @@ -131,45 +164,19 @@ var _ = Describe("OpsRequest webhook", func() { By("Test existing other operations in cluster") // update cluster existing operations addClusterRequestAnnotation(cluster, "testOpsName", SpecReconcilingClusterPhase) - Eventually(func() string { - err := testCtx.CreateObj(ctx, opsRequest) - if err == nil { - return "" - } - return err.Error() - }).Should(ContainSubstring("existing OpsRequest: testOpsName")) + Expect(testCtx.CreateObj(ctx, opsRequest).Error()).Should(ContainSubstring("existing OpsRequest: testOpsName")) // test opsRequest reentry addClusterRequestAnnotation(cluster, opsRequest.Name, SpecReconcilingClusterPhase) - By("By creating a upgrade opsRequest, it should be succeed") - Eventually(func() bool { - opsRequest.Spec.Upgrade.ClusterVersionRef = newClusterVersion.Name - err := testCtx.CheckedCreateObj(ctx, opsRequest) - return err == nil - }).Should(BeTrue()) - - // wait until OpsRequest created - Eventually(func() bool { - err := k8sClient.Get(context.Background(), client.ObjectKey{Name: opsRequest.Name, - Namespace: opsRequest.Namespace}, opsRequest) - return err == nil - }).Should(BeTrue()) - - newClusterName := clusterName + "1" - newCluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, newClusterName) - Expect(testCtx.CheckedCreateObj(ctx, newCluster)).Should(Succeed()) - - By("By testing Immutable when status.phase in Succeed") - // if running in real cluster, the opsRequest will reconcile all the time. - // so we should add eventually block. - Eventually(func() bool { - patch := client.MergeFrom(opsRequest.DeepCopy()) - opsRequest.Status.Phase = OpsSucceedPhase - Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).Should(Succeed()) - patch = client.MergeFrom(opsRequest.DeepCopy()) - opsRequest.Spec.ClusterRef = newClusterName - return Expect(k8sClient.Patch(ctx, opsRequest, patch).Error()).To(ContainSubstring("is forbidden when status.Phase is Succeed")) - }).Should(BeTrue()) + By("By creating a upgrade opsRequest, it should be succeed") + opsRequest.Spec.Upgrade.ClusterVersionRef = newClusterVersion.Name + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed()) + Expect(k8sClient.Get(context.Background(), client.ObjectKey{Name: opsRequest.Name, + Namespace: opsRequest.Namespace}, opsRequest)).Should(Succeed()) + + By("expect an error for cancelling this opsRequest") + opsRequest.Spec.Cancel = true + Expect(k8sClient.Update(context.Background(), opsRequest).Error()).Should(ContainSubstring("forbidden to cancel the opsRequest which type not in ['VerticalScaling','HorizontalScaling']")) } testVerticalScaling := func(cluster *Cluster) { @@ -188,7 +195,7 @@ var _ = Describe("OpsRequest webhook", func() { }, }, { - ComponentOps: ComponentOps{ComponentName: replicaSetComponentName}, + ComponentOps: ComponentOps{ComponentName: componentName}, ResourceRequirements: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ "cpu": resource.MustParse("200m"), @@ -222,92 +229,121 @@ var _ = Describe("OpsRequest webhook", func() { opsRequest = createTestOpsRequest(clusterName, opsRequestName, VerticalScalingType) opsRequest.Spec.VerticalScalingList = []VerticalScaling{verticalScalingList[2]} Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("must be less than or equal to cpu limit")) - Eventually(func() bool { - opsRequest.Spec.VerticalScalingList[0].Requests[corev1.ResourceCPU] = resource.MustParse("100m") - err := testCtx.CheckedCreateObj(ctx, opsRequest) - return err == nil - }).Should(BeTrue()) + + By("expect successful") + opsRequest.Spec.VerticalScalingList[0].Requests[corev1.ResourceCPU] = resource.MustParse("100m") + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed()) + + By("test spec immutable") + newClusterName := clusterName + "1" + newCluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, newClusterName) + Expect(testCtx.CheckedCreateObj(ctx, newCluster)).Should(Succeed()) + + testSpecImmutable := func(phase OpsPhase) { + By(fmt.Sprintf("spec is immutable when status.phase in %s", phase)) + patch := client.MergeFrom(opsRequest.DeepCopy()) + opsRequest.Status.Phase = phase + Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).Should(Succeed()) + + patch = client.MergeFrom(opsRequest.DeepCopy()) + opsRequest.Spec.Cancel = true + Expect(k8sClient.Patch(ctx, opsRequest, patch).Error()).To(ContainSubstring(fmt.Sprintf("is forbidden when status.Phase is %s", phase))) + } + phaseList := []OpsPhase{OpsSucceedPhase, OpsFailedPhase, OpsCancelledPhase} + for _, phase := range phaseList { + testSpecImmutable(phase) + } + + By("test spec immutable except for cancel") + testSpecImmutableExpectForCancel := func(phase OpsPhase) { + patch := client.MergeFrom(opsRequest.DeepCopy()) + opsRequest.Status.Phase = phase + Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).Should(Succeed()) + + patch = client.MergeFrom(opsRequest.DeepCopy()) + By(fmt.Sprintf("cancel opsRequest when ops phase is %s", phase)) + opsRequest.Spec.Cancel = !opsRequest.Spec.Cancel + Expect(k8sClient.Patch(ctx, opsRequest, patch)).ShouldNot(HaveOccurred()) + + By(fmt.Sprintf("expect an error for updating spec.ClusterRef when ops phase is %s", phase)) + opsRequest.Spec.ClusterRef = newClusterName + Expect(k8sClient.Patch(ctx, opsRequest, patch).Error()).To(ContainSubstring("forbidden to update spec.clusterRef")) + } + + phaseList = []OpsPhase{OpsCreatingPhase, OpsRunningPhase, OpsCancellingPhase} + for _, phase := range phaseList { + testSpecImmutableExpectForCancel(phase) + } } testVolumeExpansion := func(cluster *Cluster) { - volumeExpansionList := []VolumeExpansion{ - { - ComponentOps: ComponentOps{ComponentName: "ve-not-exist"}, - VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ - { - Name: "data", - Storage: resource.MustParse("2Gi"), - }, - }, - }, - { - ComponentOps: ComponentOps{ComponentName: replicaSetComponentName}, - VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ - { - Name: "log", - Storage: resource.MustParse("2Gi"), - }, - { - Name: "data", - Storage: resource.MustParse("2Gi"), - }, - }, - }, - { - ComponentOps: ComponentOps{ComponentName: replicaSetComponentName}, - VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ - { - Name: "data", - Storage: resource.MustParse("2Gi"), - }, - }, - }, - { - ComponentOps: ComponentOps{ComponentName: replicaSetComponentName}, - VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ - { - Name: "log", - Storage: resource.MustParse("2Gi"), - }, - { - Name: "data", - Storage: resource.MustParse("2Gi"), + getSingleVolumeExpansionList := func(compName, vctName, storage string) []VolumeExpansion { + return []VolumeExpansion{ + { + ComponentOps: ComponentOps{ComponentName: compName}, + VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ + { + Name: vctName, + Storage: resource.MustParse(storage), + }, }, }, - }, + } } - + defaultVCTName := "data" + targetStorage := "2Gi" By("By testing volumeExpansion - target component not exist") opsRequest := createTestOpsRequest(clusterName, opsRequestName, VolumeExpansionType) - opsRequest.Spec.VolumeExpansionList = []VolumeExpansion{volumeExpansionList[0]} + opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList("ve-not-exist", defaultVCTName, targetStorage) Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("ve-not-exist"))) By("By testing volumeExpansion - target volume not exist") - opsRequest.Spec.VolumeExpansionList = []VolumeExpansion{volumeExpansionList[1]} - Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("volumeClaimTemplates: [log] not found in component: replicasets")) - - By("By testing volumeExpansion - create a new storage class") - storageClassName := "sc-test-volume-expansion" - storageClass := createStorageClass(testCtx.Ctx, storageClassName, "false", true) - Expect(storageClass != nil).Should(BeTrue()) - - By("By testing volumeExpansion - has no pvc") - for _, compSpec := range cluster.Spec.ComponentSpecs { - for _, vct := range compSpec.VolumeClaimTemplates { - Expect(vct.Spec.StorageClassName == nil).Should(BeTrue()) - } + volumeExpansionList := []VolumeExpansion{{ + ComponentOps: ComponentOps{ComponentName: componentName}, + VolumeClaimTemplates: []OpsRequestVolumeClaimTemplate{ + { + Name: "log", + Storage: resource.MustParse(targetStorage), + }, + { + Name: defaultVCTName, + Storage: resource.MustParse(targetStorage), + }, + }, + }, } - opsRequest.Spec.VolumeExpansionList = []VolumeExpansion{volumeExpansionList[2]} - notSupportMsg := "volumeClaimTemplate: [data] not support volume expansion in component: replicasets, you can view infos by command: kubectl get sc" + opsRequest.Spec.VolumeExpansionList = volumeExpansionList + Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring("volumeClaimTemplates: [log] not found in component: " + componentName)) + + By("By testing volumeExpansion - storageClass do not support volume expansion") + volumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, targetStorage) + opsRequest.Spec.VolumeExpansionList = volumeExpansionList + notSupportMsg := fmt.Sprintf("volumeClaimTemplate: [data] not support volume expansion in component: %s, you can view infos by command: kubectl get sc", componentName) Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notSupportMsg)) - // TODO - By("testing volumeExpansion - pvc exists") - // TODO - By("By testing volumeExpansion - (TODO)use specified storage class") - // Eventually(func() bool { - // opsRequest.Spec.VolumeExpansionList = []VolumeExpansion{volumeExpansionList[3]} - // Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(BeNil()) - // }).Should(BeTrue()) + + By("testing volumeExpansion - storageClass supports volume expansion") + storageClassName := "standard" + storageClass := createStorageClass(testCtx.Ctx, storageClassName, "true", true) + Expect(storageClass).ShouldNot(BeNil()) + // mock to create pvc + createPVC(clusterName, componentName, storageClassName, defaultVCTName, 0) + + By("testing volumeExpansion with smaller storage, expect an error occurs") + opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, "500Mi") + Expect(testCtx.CreateObj(ctx, opsRequest)).Should(HaveOccurred()) + Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(`requested storage size of volumeClaimTemplate "data" can not less than status.capacity.storage "1Gi"`)) + + By("testing other volumeExpansion opsRequest exists") + opsRequest.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, targetStorage) + Expect(testCtx.CreateObj(ctx, opsRequest)).ShouldNot(HaveOccurred()) + // mock ops to running + patch := client.MergeFrom(opsRequest.DeepCopy()) + opsRequest.Status.Phase = OpsRunningPhase + Expect(k8sClient.Status().Patch(ctx, opsRequest, patch)).ShouldNot(HaveOccurred()) + // create another ops + opsRequest1 := createTestOpsRequest(clusterName, opsRequestName+"1", VolumeExpansionType) + opsRequest1.Spec.VolumeExpansionList = getSingleVolumeExpansionList(componentName, defaultVCTName, "3Gi") + Expect(testCtx.CreateObj(ctx, opsRequest1).Error()).Should(ContainSubstring("existing other VolumeExpansion OpsRequest")) } testHorizontalScaling := func(clusterDef *ClusterDefinition, cluster *Cluster) { @@ -321,7 +357,7 @@ var _ = Describe("OpsRequest webhook", func() { Replicas: 2, }, { - ComponentOps: ComponentOps{ComponentName: replicaSetComponentName}, + ComponentOps: ComponentOps{ComponentName: componentName}, Replicas: 2, }, } @@ -335,11 +371,9 @@ var _ = Describe("OpsRequest webhook", func() { clusterDef.Spec.ComponentDefs = clusterDef.Spec.ComponentDefs[:1] } Expect(k8sClient.Patch(ctx, clusterDef, patch)).Should(Succeed()) - Eventually(func() bool { - tmp := &ClusterDefinition{} - _ = k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDef.Name, Namespace: clusterDef.Namespace}, tmp) - return len(tmp.Spec.ComponentDefs) == 1 - }).Should(BeTrue()) + tmp := &ClusterDefinition{} + _ = k8sClient.Get(context.Background(), client.ObjectKey{Name: clusterDef.Name, Namespace: clusterDef.Namespace}, tmp) + Expect(len(tmp.Spec.ComponentDefs)).Should(Equal(1)) By("By testing horizontalScaling - target component not exist") opsRequest := createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType) @@ -353,18 +387,14 @@ var _ = Describe("OpsRequest webhook", func() { By("By testing horizontalScaling. if api is legal, it will create successfully") opsRequest = createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType) - Eventually(func() bool { - opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]} - return testCtx.CheckedCreateObj(ctx, opsRequest) == nil - }).Should(BeTrue()) + opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]} + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed()) By("test min, max is zero") opsRequest = createTestOpsRequest(clusterName, opsRequestName, HorizontalScalingType) - Eventually(func() bool { - opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]} - opsRequest.Spec.HorizontalScalingList[0].Replicas = 5 - return testCtx.CheckedCreateObj(ctx, opsRequest) == nil - }).Should(BeTrue()) + opsRequest.Spec.HorizontalScalingList = []HorizontalScaling{hScalingList[2]} + opsRequest.Spec.HorizontalScalingList[0].Replicas = 5 + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed()) } testWhenClusterDeleted := func(cluster *Cluster, opsRequest *OpsRequest) { @@ -374,10 +404,7 @@ var _ = Describe("OpsRequest webhook", func() { Expect(k8sClient.Delete(ctx, newCluster)).Should(Succeed()) By("test path labels") - Eventually(func() bool { - err := k8sClient.Get(ctx, client.ObjectKey{Name: clusterName, Namespace: cluster.Namespace}, &Cluster{}) - return err != nil - }).Should(BeTrue()) + Eventually(k8sClient.Get(ctx, client.ObjectKey{Name: clusterName, Namespace: cluster.Namespace}, &Cluster{})).Should(HaveOccurred()) patch := client.MergeFrom(opsRequest.DeepCopy()) opsRequest.Labels["test"] = "test-ops" @@ -393,11 +420,8 @@ var _ = Describe("OpsRequest webhook", func() { Expect(testCtx.CreateObj(ctx, opsRequest).Error()).To(ContainSubstring(notFoundComponentsString("replicasets1"))) By("By testing restart. if api is legal, it will create successfully") - Eventually(func() bool { - opsRequest.Spec.RestartList[0].ComponentName = replicaSetComponentName - err := testCtx.CheckedCreateObj(ctx, opsRequest) - return err == nil - }).Should(BeTrue()) + opsRequest.Spec.RestartList[0].ComponentName = componentName + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(Succeed()) return opsRequest } @@ -405,28 +429,21 @@ var _ = Describe("OpsRequest webhook", func() { It("Should webhook validate passed", func() { By("By create a clusterDefinition") - clusterDef := &ClusterDefinition{} // wait until ClusterDefinition and ClusterVersion created - Eventually(func() bool { - clusterDef, _ = createTestClusterDefinitionObj(clusterDefinitionName) - Expect(testCtx.CheckedCreateObj(ctx, clusterDef)).Should(Succeed()) - By("By creating a clusterVersion") - clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName) - err := testCtx.CheckedCreateObj(ctx, clusterVersion) - return err == nil - }).Should(BeTrue()) + clusterDef, _ := createTestClusterDefinitionObj(clusterDefinitionName) + Expect(testCtx.CheckedCreateObj(ctx, clusterDef)).Should(Succeed()) + By("By creating a clusterVersion") + clusterVersion := createTestClusterVersionObj(clusterDefinitionName, clusterVersionName) + Expect(testCtx.CheckedCreateObj(ctx, clusterVersion)).Should(Succeed()) opsRequest := createTestOpsRequest(clusterName, opsRequestName, UpgradeType) - cluster := &Cluster{} - // wait until Cluster created - Eventually(func() bool { - By("By testing spec.clusterDef is legal") - Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).ShouldNot(Succeed()) - By("By create a new cluster ") - cluster, _ = createTestCluster(clusterDefinitionName, clusterVersionName, clusterName) - err := testCtx.CheckedCreateObj(ctx, cluster) - return err == nil - }).Should(BeTrue()) + + // create Cluster + By("By testing spec.clusterDef is legal") + Expect(testCtx.CheckedCreateObj(ctx, opsRequest)).Should(HaveOccurred()) + By("By create a new cluster ") + cluster, _ := createTestCluster(clusterDefinitionName, clusterVersionName, clusterName) + Expect(testCtx.CheckedCreateObj(ctx, cluster)).Should(Succeed()) testUpgrade(cluster) @@ -451,10 +468,13 @@ kind: OpsRequest metadata: name: %s namespace: default + labels: + app.kubernetes.io/instance: %s + ops.kubeblocks.io/ops-type: %s spec: clusterRef: %s type: %s -`, opsRequestName+randomStr, clusterName, opsType) +`, opsRequestName+randomStr, clusterName, opsType, clusterName, opsType) opsRequest := &OpsRequest{} _ = yaml.Unmarshal([]byte(opsRequestYaml), opsRequest) return opsRequest diff --git a/apis/apps/v1alpha1/type.go b/apis/apps/v1alpha1/type.go index 8ff8b48c2..77c388646 100644 --- a/apis/apps/v1alpha1/type.go +++ b/apis/apps/v1alpha1/type.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,6 +18,9 @@ limitations under the License. package v1alpha1 import ( + "errors" + + appsv1 "k8s.io/api/apps/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/manager" ) @@ -30,6 +33,59 @@ const ( OpsRequestKind = "OpsRequestKind" ) +type ComponentTemplateSpec struct { + // Specify the name of configuration template. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + Name string `json:"name"` + + // Specify the name of the referenced the configuration template ConfigMap object. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + TemplateRef string `json:"templateRef"` + + // Specify the namespace of the referenced the configuration template ConfigMap object. + // An empty namespace is equivalent to the "default" namespace. + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:default="default" + // +optional + Namespace string `json:"namespace,omitempty"` + + // volumeName is the volume name of PodTemplate, which the configuration file produced through the configuration template will be mounted to the corresponding volume. + // The volume name must be defined in podSpec.containers[*].volumeMounts. + // +kubebuilder:validation:Required + // +kubebuilder:validation:MaxLength=32 + VolumeName string `json:"volumeName"` + + // defaultMode is optional: mode bits used to set permissions on created files by default. + // Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511. + // YAML accepts both octal and decimal values, JSON requires decimal values for mode bits. + // Defaults to 0644. + // Directories within the path are not affected by this setting. + // This might be in conflict with other options that affect the file + // mode, like fsGroup, and the result can be other mode bits set. + // +optional + DefaultMode *int32 `json:"defaultMode,omitempty" protobuf:"varint,3,opt,name=defaultMode"` +} + +type ComponentConfigSpec struct { + ComponentTemplateSpec `json:",inline"` + + // Specify a list of keys. + // If empty, ConfigConstraint takes effect for all keys in configmap. + // +listType=set + // +optional + Keys []string `json:"keys,omitempty"` + + // Specify the name of the referenced the configuration constraints object. + // +kubebuilder:validation:MaxLength=63 + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + // +optional + ConfigConstraintRef string `json:"constraintRef,omitempty"` +} + // ClusterPhase defines the Cluster CR .status.phase // +enum // +kubebuilder:validation:Enum={Running,Stopped,Failed,Abnormal,Creating,Updating} @@ -59,20 +115,12 @@ const ( SpecReconcilingClusterCompPhase ClusterComponentPhase = "Updating" CreatingClusterCompPhase ClusterComponentPhase = "Creating" // DeletingClusterCompPhase ClusterComponentPhase = "Deleting" // DO REVIEW: may merged with Stopping - - // REVIEW: following are variant of "Updating", why not have "Updating" phase with detail Status.Conditions - // VolumeExpandingClusterCompPhase ClusterComponentPhase = "VolumeExpanding" - // HorizontalScalingClusterCompPhase ClusterComponentPhase = "HorizontalScaling" - // VerticalScalingClusterCompPhase ClusterComponentPhase = "VerticalScaling" - // VersionUpgradingClusterCompPhase ClusterComponentPhase = "Upgrading" - // ReconfiguringClusterCompPhase ClusterComponentPhase = "Reconfiguring" - // ExposingClusterCompPhase ClusterComponentPhase = "Exposing" - // RollingClusterCompPhase ClusterComponentPhase = "Rolling" // REVIEW: original value is Rebooting, and why not having stopping -> stopped -> starting -> running ) const ( // define the cluster condition type ConditionTypeLatestOpsRequestProcessed = "LatestOpsRequestProcessed" // ConditionTypeLatestOpsRequestProcessed describes whether the latest OpsRequest that affect the cluster lifecycle has been processed. + ConditionTypeHaltRecovery = "HaltRecovery" // ConditionTypeHaltRecovery describe Halt recovery processing stage ConditionTypeProvisioningStarted = "ProvisioningStarted" // ConditionTypeProvisioningStarted the operator starts resource provisioning to create or change the cluster ConditionTypeApplyResources = "ApplyResources" // ConditionTypeApplyResources the operator start to apply resources to create or change the cluster ConditionTypeReplicasReady = "ReplicasReady" // ConditionTypeReplicasReady all pods of components are ready @@ -90,17 +138,30 @@ const ( UnavailablePhase Phase = "Unavailable" ) +// ConfigConstraintPhase defines the ConfigConstraint CR .status.phase +// +enum +// +kubebuilder:validation:Enum={Available,Unavailable, Deleting} +type ConfigConstraintPhase string + +const ( + CCAvailablePhase ConfigConstraintPhase = "Available" + CCUnavailablePhase ConfigConstraintPhase = "Unavailable" + CCDeletingPhase ConfigConstraintPhase = "Deleting" +) + // OpsPhase defines opsRequest phase. // +enum -// +kubebuilder:validation:Enum={Pending,Creating,Running,Failed,Succeed} +// +kubebuilder:validation:Enum={Pending,Creating,Running,Cancelling,Cancelled,Failed,Succeed} type OpsPhase string const ( - OpsPendingPhase OpsPhase = "Pending" - OpsCreatingPhase OpsPhase = "Creating" - OpsRunningPhase OpsPhase = "Running" - OpsFailedPhase OpsPhase = "Failed" - OpsSucceedPhase OpsPhase = "Succeed" + OpsPendingPhase OpsPhase = "Pending" + OpsCreatingPhase OpsPhase = "Creating" + OpsRunningPhase OpsPhase = "Running" + OpsCancellingPhase OpsPhase = "Cancelling" + OpsSucceedPhase OpsPhase = "Succeed" + OpsCancelledPhase OpsPhase = "Cancelled" + OpsFailedPhase OpsPhase = "Failed" ) // OpsType defines operation types. @@ -241,7 +302,7 @@ type OpsRecorder struct { type ProvisionPolicyType string const ( - // CreateByStmt will create account w.r.t. deleteion and creation statement given by provider. + // CreateByStmt will create account w.r.t. deletion and creation statement given by provider. CreateByStmt ProvisionPolicyType = "CreateByStmt" // ReferToExisting will not create account, but create a secret by copying data from referred secret file. ReferToExisting ProvisionPolicyType = "ReferToExisting" @@ -442,8 +503,31 @@ const ( VolumeTypeLog VolumeType = "log" ) +// BaseBackupType the base backup type, keep synchronized with the BaseBackupType of the data protection API. +// +enum +// +kubebuilder:validation:Enum={full,snapshot} +type BaseBackupType string + +// BackupStatusUpdateStage defines the stage of backup status update. +// +enum +// +kubebuilder:validation:Enum={pre,post} +type BackupStatusUpdateStage string + func RegisterWebhookManager(mgr manager.Manager) { webhookMgr = &webhookManager{mgr.GetClient()} } type ComponentNameSet map[string]struct{} + +var ( + ErrWorkloadTypeIsUnknown = errors.New("workloadType is unknown") + ErrWorkloadTypeIsStateless = errors.New("workloadType should not be stateless") + ErrNotMatchingCompDef = errors.New("not matching componentDefRef") +) + +// StatefulSetWorkload interface +// +kubebuilder:object:generate=false +type StatefulSetWorkload interface { + FinalStsUpdateStrategy() (appsv1.PodManagementPolicyType, appsv1.StatefulSetUpdateStrategy) + GetUpdateStrategy() UpdateStrategy +} diff --git a/apis/apps/v1alpha1/webhook_suite_test.go b/apis/apps/v1alpha1/webhook_suite_test.go index a9d726bff..757544f6b 100644 --- a/apis/apps/v1alpha1/webhook_suite_test.go +++ b/apis/apps/v1alpha1/webhook_suite_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" admissionv1 "k8s.io/api/admission/v1" admissionregv1 "k8s.io/api/admissionregistration/v1" @@ -109,6 +110,9 @@ var _ = BeforeSuite(func() { err = storagev1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) + err = corev1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + // +kubebuilder:scaffold:scheme k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) diff --git a/apis/dataprotection/v1alpha1/backup_types.go b/apis/dataprotection/v1alpha1/backup_types.go index 7fe18d89d..90ce969bc 100644 --- a/apis/dataprotection/v1alpha1/backup_types.go +++ b/apis/dataprotection/v1alpha1/backup_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,37 +17,34 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" + "fmt" + "sort" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// BackupSpec defines the desired state of Backup +// BackupSpec defines the desired state of Backup. type BackupSpec struct { - // which backupPolicy to perform this backup + // Which backupPolicy is applied to perform this backup // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` BackupPolicyName string `json:"backupPolicyName"` - // Backup Type. full or incremental or snapshot. if unset, default is full. - // +kubebuilder:default=full + // Backup Type. datafile or logfile or snapshot. If not set, datafile is the default type. + // +kubebuilder:default=datafile BackupType BackupType `json:"backupType"` // if backupType is incremental, parentBackupName is required. // +optional ParentBackupName string `json:"parentBackupName,omitempty"` - - // ttl is a time.Duration-parsable string describing how long - // the Backup should be retained for. - // +optional - TTL *metav1.Duration `json:"ttl,omitempty"` } -// BackupStatus defines the observed state of Backup +// BackupStatus defines the observed state of Backup. type BackupStatus struct { // +optional Phase BackupPhase `json:"phase,omitempty"` - // record parentBackupName if backupType is incremental. + // Records parentBackupName if backupType is incremental. // +optional ParentBackupName string `json:"parentBackupName,omitempty"` @@ -69,38 +66,38 @@ type BackupStatus struct { // +optional Duration *metav1.Duration `json:"duration,omitempty"` - // backup total size - // string with capacity units in the form of "1Gi", "1Mi", "1Ki". + // Backup total size. + // A string with capacity units in the form of "1Gi", "1Mi", "1Ki". // +optional TotalSize string `json:"totalSize,omitempty"` - // the reason if backup failed. + // The reason for a backup failure. // +optional FailureReason string `json:"failureReason,omitempty"` // remoteVolume saves the backup data. // +optional - RemoteVolume *corev1.Volume `json:"remoteVolume,omitempty"` + PersistentVolumeClaimName string `json:"persistentVolumeClaimName,omitempty"` - // backupToolName referenced backup tool name. + // backupToolName references the backup tool name. // +optional BackupToolName string `json:"backupToolName,omitempty"` - // manifests determines the backup metadata info + // manifests determines the backup metadata info. // +optional Manifests *ManifestsStatus `json:"manifests,omitempty"` } type ManifestsStatus struct { - // backupLog records startTime and stopTime of data logging + // backupLog records startTime and stopTime of data logging. // +optional BackupLog *BackupLogStatus `json:"backupLog,omitempty"` - // target records the target cluster metadata string, which are in JSON format. + // target records the target cluster metadata string, which is in JSON format. // +optional Target string `json:"target,omitempty"` - // snapshot records the volume snapshot metadata + // snapshot records the volume snapshot metadata. // +optional Snapshot *BackupSnapshotStatus `json:"backupSnapshot,omitempty"` @@ -114,17 +111,17 @@ type ManifestsStatus struct { } type BackupLogStatus struct { - // startTime record start time of data logging + // startTime records the start time of data logging. // +optional StartTime *metav1.Time `json:"startTime,omitempty"` - // stopTime record start time of data logging + // stopTime records the stop time of data logging. // +optional StopTime *metav1.Time `json:"stopTime,omitempty"` } type BackupSnapshotStatus struct { - // volumeSnapshotName record the volumeSnapshot name + // volumeSnapshotName records the volumeSnapshot name. // +optional VolumeSnapshotName string `json:"volumeSnapshotName,omitempty"` @@ -137,28 +134,26 @@ type BackupSnapshotStatus struct { } type BackupToolManifestsStatus struct { - // backupToolName referenced backup tool name. - // +optional - BackupToolName string `json:"backupToolName,omitempty"` - // filePath records the file path of backup. // +optional FilePath string `json:"filePath,omitempty"` - // backup upload total size - // string with capacity units in the form of "1Gi", "1Mi", "1Ki". + // Backup upload total size. + // A string with capacity units in the form of "1Gi", "1Mi", "1Ki". // +optional UploadTotalSize string `json:"uploadTotalSize,omitempty"` - // checksum of backup file, generated by md5 or sha1 or sha256 + // checksum of backup file, generated by md5 or sha1 or sha256. // +optional - CheckSum string `json:"checkSum,omitempty"` + Checksum string `json:"checksum,omitempty"` - // backup check point, for incremental backup. + // backup checkpoint, for incremental backup. // +optional - CheckPoint string `json:"CheckPoint,omitempty"` + Checkpoint string `json:"checkpoint,omitempty"` } +// +genclient +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Namespaced @@ -169,7 +164,7 @@ type BackupToolManifestsStatus struct { // +kubebuilder:printcolumn:name="CREATE-TIME",type=string,JSONPath=".metadata.creationTimestamp" // +kubebuilder:printcolumn:name="COMPLETION-TIME",type=string,JSONPath=`.status.completionTimestamp` -// Backup is the Schema for the backups API (defined by User) +// Backup is the Schema for the backups API (defined by User). type Backup struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -180,7 +175,7 @@ type Backup struct { // +kubebuilder:object:root=true -// BackupList contains a list of Backup +// BackupList contains a list of Backup. type BackupList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -190,3 +185,62 @@ type BackupList struct { func init() { SchemeBuilder.Register(&Backup{}, &BackupList{}) } + +// Validate validates the BackupSpec and returns an error if invalid. +func (r *BackupSpec) Validate(backupPolicy *BackupPolicy) error { + notSupportedMessage := "backupPolicy: %s not supports %s backup in backupPolicy" + switch r.BackupType { + case BackupTypeSnapshot: + if backupPolicy.Spec.Snapshot == nil { + return fmt.Errorf(notSupportedMessage, r.BackupPolicyName, BackupTypeSnapshot) + } + case BackupTypeDataFile: + if backupPolicy.Spec.Datafile == nil { + return fmt.Errorf(notSupportedMessage, r.BackupPolicyName, BackupTypeDataFile) + } + case BackupTypeLogFile: + if backupPolicy.Spec.Logfile == nil { + return fmt.Errorf(notSupportedMessage, r.BackupPolicyName, BackupTypeLogFile) + } + } + return nil +} + +// GetRecoverableTimeRange returns the recoverable time range array. +func GetRecoverableTimeRange(backups []Backup) []BackupLogStatus { + // filter backups with backupLog + baseBackups := make([]Backup, 0) + var incrementalBackup *Backup + for _, b := range backups { + if b.Status.Manifests == nil || b.Status.Manifests.BackupLog == nil || + b.Status.Manifests.BackupLog.StopTime == nil { + continue + } + if b.Spec.BackupType == BackupTypeLogFile { + incrementalBackup = b.DeepCopy() + } else if b.Spec.BackupType != BackupTypeLogFile && b.Status.Phase == BackupCompleted { + baseBackups = append(baseBackups, b) + } + } + if len(baseBackups) == 0 { + return nil + } + sort.Slice(backups, func(i, j int) bool { + if backups[i].Status.StartTimestamp == nil && backups[j].Status.StartTimestamp != nil { + return false + } + if backups[i].Status.StartTimestamp != nil && backups[j].Status.StartTimestamp == nil { + return true + } + if backups[i].Status.StartTimestamp.Equal(backups[j].Status.StartTimestamp) { + return backups[i].Name < backups[j].Name + } + return backups[i].Status.StartTimestamp.Before(backups[j].Status.StartTimestamp) + }) + result := make([]BackupLogStatus, 0) + start, end := baseBackups[0].Status.Manifests.BackupLog.StopTime, baseBackups[0].Status.Manifests.BackupLog.StopTime + if incrementalBackup != nil && start.Before(incrementalBackup.Status.Manifests.BackupLog.StopTime) { + end = incrementalBackup.Status.Manifests.BackupLog.StopTime + } + return append(result, BackupLogStatus{StartTime: start, StopTime: end}) +} diff --git a/apis/dataprotection/v1alpha1/backup_types_test.go b/apis/dataprotection/v1alpha1/backup_types_test.go new file mode 100644 index 000000000..91f9c47dc --- /dev/null +++ b/apis/dataprotection/v1alpha1/backup_types_test.go @@ -0,0 +1,40 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +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 ( + "testing" + "time" +) + +func expectToDuration(t *testing.T, ttl string, baseNum, targetNum int) { + d := ToDuration(&ttl) + if d != time.Hour*time.Duration(baseNum)*time.Duration(targetNum) { + t.Errorf(`Expected duration is "%d*%d*time.Hour"", got %v`, targetNum, baseNum, d) + } +} + +func TestToDuration(t *testing.T) { + d := ToDuration(nil) + if d != time.Duration(0) { + t.Errorf("Expected duration is 0, got %v", d) + } + expectToDuration(t, "7d", 24, 7) + expectToDuration(t, "7D", 24, 7) + expectToDuration(t, "12h", 1, 12) + expectToDuration(t, "12H", 1, 12) +} diff --git a/apis/dataprotection/v1alpha1/backuppolicy_types.go b/apis/dataprotection/v1alpha1/backuppolicy_types.go index e3d2df55b..05bb76af2 100644 --- a/apis/dataprotection/v1alpha1/backuppolicy_types.go +++ b/apis/dataprotection/v1alpha1/backuppolicy_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -17,54 +17,139 @@ limitations under the License. package v1alpha1 import ( - corev1 "k8s.io/api/core/v1" + "strconv" + "strings" + "time" + + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) // BackupPolicySpec defines the desired state of BackupPolicy type BackupPolicySpec struct { - // policy can inherit from backup config and override some fields. - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + // retention describe how long the Backup should be retained. if not set, will be retained forever. // +optional - BackupPolicyTemplateName string `json:"backupPolicyTemplateName,omitempty"` + Retention *RetentionSpec `json:"retention,omitempty"` - // The schedule in Cron format, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron. + // schedule policy for backup. // +optional - Schedule string `json:"schedule,omitempty"` + Schedule Schedule `json:"schedule,omitempty"` - // Backup ComponentDefRef. full or incremental or snapshot. if unset, default is snapshot. - // +kubebuilder:validation:Enum={full,incremental,snapshot} - // +kubebuilder:default=snapshot + // the policy for snapshot backup. // +optional - BackupType string `json:"backupType,omitempty"` + Snapshot *SnapshotPolicy `json:"snapshot,omitempty"` - // The number of automatic backups to retain. Value must be non-negative integer. - // 0 means NO limit on the number of backups. - // +kubebuilder:default=7 + // the policy for datafile backup. // +optional - BackupsHistoryLimit int32 `json:"backupsHistoryLimit,omitempty"` + Datafile *CommonBackupPolicy `json:"datafile,omitempty"` - // which backup tool to perform database backup, only support one tool. - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + // the policy for logfile backup. // +optional - BackupToolName string `json:"backupToolName,omitempty"` + Logfile *CommonBackupPolicy `json:"logfile,omitempty"` +} - // TTL is a time.Duration-parseable string describing how long - // the Backup should be retained for. +type RetentionSpec struct { + // ttl is a time string ending with the 'd'|'D'|'h'|'H' character to describe how long + // the Backup should be retained. if not set, will be retained forever. + // +kubebuilder:validation:Pattern:=`^\d+[d|D|h|H]$` // +optional - TTL *metav1.Duration `json:"ttl,omitempty"` + TTL *string `json:"ttl,omitempty"` +} + +type Schedule struct { + // schedule policy for snapshot backup. + // +optional + Snapshot *SchedulePolicy `json:"snapshot,omitempty"` + + // schedule policy for datafile backup. + // +optional + Datafile *SchedulePolicy `json:"datafile,omitempty"` + + // schedule policy for logfile backup. + // +optional + Logfile *SchedulePolicy `json:"logfile,omitempty"` +} - // database cluster service +type SchedulePolicy struct { + // the cron expression for schedule, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron. // +kubebuilder:validation:Required - Target TargetCluster `json:"target"` + CronExpression string `json:"cronExpression"` + + // enable or disable the schedule. + // +kubebuilder:validation:Required + Enable bool `json:"enable"` +} + +type SnapshotPolicy struct { + BasePolicy `json:",inline"` // execute hook commands for backup. // +optional Hooks *BackupPolicyHook `json:"hooks,omitempty"` +} + +type CommonBackupPolicy struct { + BasePolicy `json:",inline"` + + // refer to PersistentVolumeClaim and the backup data will be stored in the corresponding persistent volume. + // +kubebuilder:validation:Required + PersistentVolumeClaim PersistentVolumeClaim `json:"persistentVolumeClaim"` + + // which backup tool to perform database backup, only support one tool. + // +kubebuilder:validation:Required + // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` + BackupToolName string `json:"backupToolName,omitempty"` +} + +type PersistentVolumeClaim struct { + // the name of the PersistentVolumeClaim. + Name string `json:"name"` + + // storageClassName is the name of the StorageClass required by the claim. + // +optional + StorageClassName *string `json:"storageClassName,omitempty"` + + // initCapacity represents the init storage size of the PersistentVolumeClaim which should be created if not exist. + // and the default value is 100Gi if it is empty. + // +optional + InitCapacity resource.Quantity `json:"initCapacity,omitempty"` + + // createPolicy defines the policy for creating the PersistentVolumeClaim, enum values: + // - Never: do nothing if the PersistentVolumeClaim not exists. + // - IfNotPresent: create the PersistentVolumeClaim if not present and the accessModes only contains 'ReadWriteMany'. + // +kubebuilder:default=IfNotPresent + // +optional + CreatePolicy CreatePVCPolicy `json:"createPolicy"` + + // persistentVolumeConfigMap references the configmap which contains a persistentVolume template. + // key must be "persistentVolume" and value is the "PersistentVolume" struct. + // support the following built-in Objects: + // - $(GENERATE_NAME): generate a specific format "pvcName-pvcNamespace". + // if the PersistentVolumeClaim not exists and CreatePolicy is "IfNotPresent", the controller + // will create it by this template. this is a mutually exclusive setting with "storageClassName". + // +optional + PersistentVolumeConfigMap *PersistentVolumeConfigMap `json:"persistentVolumeConfigMap,omitempty"` +} - // array of remote volumes from CSI driver definition. +type PersistentVolumeConfigMap struct { + // the name of the persistentVolume ConfigMap. // +kubebuilder:validation:Required - RemoteVolume corev1.Volume `json:"remoteVolume"` + Name string `json:"name"` + + // the namespace of the persistentVolume ConfigMap. + // +kubebuilder:validation:Required + Namespace string `json:"namespace"` +} +type BasePolicy struct { + // target database cluster for backup. + // +kubebuilder:validation:Required + Target TargetCluster `json:"target"` + + // the number of automatic backups to retain. Value must be non-negative integer. + // 0 means NO limit on the number of backups. + // +kubebuilder:default=7 + // +optional + BackupsHistoryLimit int32 `json:"backupsHistoryLimit,omitempty"` // count of backup stop retries on fail. // +optional @@ -77,37 +162,39 @@ type BackupPolicySpec struct { // TargetCluster TODO (dsj): target cluster need redefined from Cluster API type TargetCluster struct { - // LabelSelector is used to find matching pods. + // labelsSelector is used to find matching pods. // Pods that match this label selector are counted to determine the number of pods // in their corresponding topology domain. // +kubebuilder:validation:Required // +kubebuilder:pruning:PreserveUnknownFields LabelsSelector *metav1.LabelSelector `json:"labelsSelector"` - // Secret is used to connect to the target database cluster. + // secret is used to connect to the target database cluster. // If not set, secret will be inherited from backup policy template. // if still not set, the controller will check if any system account for dataprotection has been created. // +optional Secret *BackupPolicySecret `json:"secret,omitempty"` } -// BackupPolicySecret defined for the target database secret that backup tool can connect. +// BackupPolicySecret defines for the target database secret that backup tool can connect. type BackupPolicySecret struct { // the secret name // +kubebuilder:validation:Required // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` Name string `json:"name"` - // UserKeyword the map keyword of the user in the connection credential secret - // +optional - UserKeyword string `json:"userKeyword,omitempty"` + // usernameKey the map key of the user in the connection credential secret + // +kubebuilder:validation:Required + // +kubebuilder:default=username + UsernameKey string `json:"usernameKey,omitempty"` - // PasswordKeyword the map keyword of the password in the connection credential secret - // +optional - PasswordKeyword string `json:"passwordKeyword,omitempty"` + // passwordKey the map key of the password in the connection credential secret + // +kubebuilder:validation:Required + // +kubebuilder:default=password + PasswordKey string `json:"passwordKey,omitempty"` } -// BackupPolicyHook defined for the database execute commands before and after backup. +// BackupPolicyHook defines for the database execute commands before and after backup. type BackupPolicyHook struct { // pre backup to perform commands // +optional @@ -159,28 +246,36 @@ type BackupStatusUpdate struct { // BackupPolicyStatus defines the observed state of BackupPolicy type BackupPolicyStatus struct { - // backup policy phase valid value: available, failed, new. + + // observedGeneration is the most recent generation observed for this + // BackupPolicy. It corresponds to the Cluster's generation, which is + // updated on mutation by the API Server. // +optional - Phase BackupPolicyTemplatePhase `json:"phase,omitempty"` + ObservedGeneration int64 `json:"observedGeneration,omitempty"` + + // backup policy phase valid value: Available, Failed. + // +optional + Phase BackupPolicyPhase `json:"phase,omitempty"` // the reason if backup policy check failed. // +optional FailureReason string `json:"failureReason,omitempty"` - // Information when was the last time the job was successfully scheduled. + // information when was the last time the job was successfully scheduled. // +optional LastScheduleTime *metav1.Time `json:"lastScheduleTime,omitempty"` - // Information when was the last time the job successfully completed. + // information when was the last time the job successfully completed. // +optional LastSuccessfulTime *metav1.Time `json:"lastSuccessfulTime,omitempty"` } +// +genclient +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status -// +kubebuilder:resource:categories={kubeblocks},scope=Namespaced +// +kubebuilder:resource:categories={kubeblocks},scope=Namespaced,shortName=bp // +kubebuilder:printcolumn:name="STATUS",type=string,JSONPath=`.status.phase` -// +kubebuilder:printcolumn:name="SCHEDULE",type=string,JSONPath=`.spec.schedule` // +kubebuilder:printcolumn:name="LAST SCHEDULE",type=string,JSONPath=`.status.lastScheduleTime` // +kubebuilder:printcolumn:name="AGE",type=date,JSONPath=`.metadata.creationTimestamp` @@ -205,3 +300,39 @@ type BackupPolicyList struct { func init() { SchemeBuilder.Register(&BackupPolicy{}, &BackupPolicyList{}) } + +func (r *BackupPolicySpec) GetCommonPolicy(backupType BackupType) *CommonBackupPolicy { + switch backupType { + case BackupTypeDataFile: + return r.Datafile + case BackupTypeLogFile: + return r.Logfile + } + return nil +} + +func (r *BackupPolicySpec) GetCommonSchedulePolicy(backupType BackupType) *SchedulePolicy { + switch backupType { + case BackupTypeSnapshot: + return r.Schedule.Snapshot + case BackupTypeDataFile: + return r.Schedule.Datafile + case BackupTypeLogFile: + return r.Schedule.Logfile + } + return nil +} + +// ToDuration converts the ttl string to time.Duration. +func ToDuration(ttl *string) time.Duration { + if ttl == nil { + return time.Duration(0) + } + ttlLower := strings.ToLower(*ttl) + if strings.HasSuffix(ttlLower, "d") { + days, _ := strconv.Atoi(strings.ReplaceAll(ttlLower, "d", "")) + return time.Hour * 24 * time.Duration(days) + } + hours, _ := strconv.Atoi(strings.ReplaceAll(ttlLower, "h", "")) + return time.Hour * time.Duration(hours) +} diff --git a/apis/dataprotection/v1alpha1/backuppolicytemplate_types.go b/apis/dataprotection/v1alpha1/backuppolicytemplate_types.go deleted file mode 100644 index 988a34ac2..000000000 --- a/apis/dataprotection/v1alpha1/backuppolicytemplate_types.go +++ /dev/null @@ -1,104 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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" -) - -// BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate -type BackupPolicyTemplateSpec struct { - // The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - // +optional - Schedule string `json:"schedule,omitempty"` - - // which backup tool to perform database backup, only support one tool. - // +kubebuilder:validation:Required - // +kubebuilder:validation:Pattern:=`^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$` - BackupToolName string `json:"backupToolName"` - - // TTL is a time.Duration-parseable string describing how long - // the Backup should be retained for. - // +optional - TTL *metav1.Duration `json:"ttl,omitempty"` - - // limit count of backup stop retries on fail. - // if unset, retry unlimit attempted. - // +optional - OnFailAttempted int32 `json:"onFailAttempted,omitempty"` - - // execute hook commands for backup. - // +optional - Hooks *BackupPolicyHook `json:"hooks,omitempty"` - - // CredentialKeyword determines backupTool connection credential keyword in secret. - // the backupTool gets the credentials according to the user and password keyword defined by secret - // +optional - CredentialKeyword *BackupPolicyCredentialKeyword `json:"credentialKeyword,omitempty"` - - // define how to update metadata for backup status. - // +optional - BackupStatusUpdates []BackupStatusUpdate `json:"backupStatusUpdates,omitempty"` -} - -// BackupPolicyCredentialKeyword defined for the target database secret that backup tool can connect. -type BackupPolicyCredentialKeyword struct { - // UserKeyword the map keyword of the user in the connection credential secret - // +kubebuilder:default=username - // +optional - UserKeyword string `json:"userKeyword,omitempty"` - - // PasswordKeyword the map keyword of the password in the connection credential secret - // +kubebuilder:default=password - // +optional - PasswordKeyword string `json:"passwordKeyword,omitempty"` -} - -// BackupPolicyTemplateStatus defines the observed state of BackupPolicyTemplate -type BackupPolicyTemplateStatus struct { - // +optional - Phase BackupPolicyTemplatePhase `json:"phase,omitempty"` - - // +optional - FailureReason string `json:"failureReason,omitempty"` -} - -// +kubebuilder:object:root=true -// +kubebuilder:subresource:status -// +kubebuilder:resource:categories={kubeblocks},scope=Cluster - -// BackupPolicyTemplate is the Schema for the BackupPolicyTemplates API (defined by provider) -type BackupPolicyTemplate struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - - Spec BackupPolicyTemplateSpec `json:"spec,omitempty"` - Status BackupPolicyTemplateStatus `json:"status,omitempty"` -} - -// +kubebuilder:object:root=true - -// BackupPolicyTemplateList contains a list of BackupPolicyTemplate -type BackupPolicyTemplateList struct { - metav1.TypeMeta `json:",inline"` - metav1.ListMeta `json:"metadata,omitempty"` - Items []BackupPolicyTemplate `json:"items"` -} - -func init() { - SchemeBuilder.Register(&BackupPolicyTemplate{}, &BackupPolicyTemplateList{}) -} diff --git a/apis/dataprotection/v1alpha1/backuptool_types.go b/apis/dataprotection/v1alpha1/backuptool_types.go index 9e7115d39..ba4aba5ee 100644 --- a/apis/dataprotection/v1alpha1/backuptool_types.go +++ b/apis/dataprotection/v1alpha1/backuptool_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -32,6 +32,11 @@ type BackupToolSpec struct { // +kubebuilder:default=job DeployKind string `json:"deployKind,omitempty"` + // the type of backup tool, file or pitr + // +kubebuilder:validation:Enum={file,pitr} + // +kubebuilder:default=file + Type string `json:"type,omitempty"` + // Compute Resources required by this container. // Cannot be updated. // +kubebuilder:pruning:PreserveUnknownFields @@ -90,6 +95,9 @@ type BackupToolStatus struct { // TODO(dsj): define backup tool status. } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Cluster diff --git a/apis/dataprotection/v1alpha1/doc.go b/apis/dataprotection/v1alpha1/doc.go new file mode 100644 index 000000000..4b38a73a1 --- /dev/null +++ b/apis/dataprotection/v1alpha1/doc.go @@ -0,0 +1,25 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +//go:generate go run ../../../hack/docgen/api/main.go -api-dir . -config ../../../hack/docgen/api/gen-api-doc-config.json -template-dir ../../../hack/docgen/api/template -out-file ../../../docs/user_docs/api-reference/backup.md + +// +k8s:deepcopy-gen=package,register +// +k8s:openapi-gen=true +// +groupName=dataprotection.kubeblocks.io +package v1alpha1 diff --git a/apis/dataprotection/v1alpha1/groupversion_info.go b/apis/dataprotection/v1alpha1/groupversion_info.go index 00eabd849..e1e0f60db 100644 --- a/apis/dataprotection/v1alpha1/groupversion_info.go +++ b/apis/dataprotection/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/dataprotection/v1alpha1/restorejob_types.go b/apis/dataprotection/v1alpha1/restorejob_types.go index 603c2ca4f..a48c3f8af 100644 --- a/apis/dataprotection/v1alpha1/restorejob_types.go +++ b/apis/dataprotection/v1alpha1/restorejob_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -70,6 +70,8 @@ type RestoreJobStatus struct { FailureReason string `json:"failureReason,omitempty"` } +// +genclient +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Namespaced diff --git a/apis/dataprotection/v1alpha1/types.go b/apis/dataprotection/v1alpha1/types.go index 70c784e95..a9b3c8461 100644 --- a/apis/dataprotection/v1alpha1/types.go +++ b/apis/dataprotection/v1alpha1/types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,27 +28,40 @@ const ( BackupFailed BackupPhase = "Failed" ) -// BackupType the backup type, marked backup set is full or incremental or snapshot. +// BackupType the backup type, marked backup set is datafile or logfile or snapshot. // +enum -// +kubebuilder:validation:Enum={full,incremental,snapshot} +// +kubebuilder:validation:Enum={datafile,logfile,snapshot} type BackupType string const ( - BackupTypeFull BackupType = "full" - BackupTypeIncremental BackupType = "incremental" - BackupTypeSnapshot BackupType = "snapshot" + BackupTypeDataFile BackupType = "datafile" + BackupTypeLogFile BackupType = "logfile" + BackupTypeSnapshot BackupType = "snapshot" ) -// BackupPolicyTemplatePhase defines phases for BackupPolicyTemplate CR. +// BaseBackupType the base backup type. // +enum -// +kubebuilder:validation:Enum={New,Available,InProgress,Failed} -type BackupPolicyTemplatePhase string +// +kubebuilder:validation:Enum={full,snapshot} +type BaseBackupType string + +// CreatePVCPolicy the policy how to create the PersistentVolumeClaim for backup. +// +enum +// +kubebuilder:validation:Enum={IfNotPresent,Never} +type CreatePVCPolicy string + +const ( + CreatePVCPolicyNever CreatePVCPolicy = "Never" + CreatePVCPolicyIfNotPresent CreatePVCPolicy = "IfNotPresent" +) + +// BackupPolicyPhase defines phases for BackupPolicy CR. +// +enum +// +kubebuilder:validation:Enum={Available,Failed} +type BackupPolicyPhase string const ( - ConfigNew BackupPolicyTemplatePhase = "New" - ConfigAvailable BackupPolicyTemplatePhase = "Available" - ConfigInProgress BackupPolicyTemplatePhase = "InProgress" - ConfigFailed BackupPolicyTemplatePhase = "Failed" + PolicyAvailable BackupPolicyPhase = "Available" + PolicyFailed BackupPolicyPhase = "Failed" ) // RestoreJobPhase The current phase. Valid values are New, InProgressPhy, InProgressLogic, Completed, Failed. diff --git a/apis/extensions/v1alpha1/addon_types.go b/apis/extensions/v1alpha1/addon_types.go index c23b642d5..477839e60 100644 --- a/apis/extensions/v1alpha1/addon_types.go +++ b/apis/extensions/v1alpha1/addon_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,19 +29,19 @@ import ( "github.com/apecloud/kubeblocks/internal/constant" ) -// AddonSpec defines the desired state of Addon +// AddonSpec defines the desired state of an add-on. // +kubebuilder:validation:XValidation:rule="has(self.type) && self.type == 'Helm' ? has(self.helm) : !has(self.helm)",message="spec.helm is required when spec.type is Helm, and forbidden otherwise" type AddonSpec struct { // Addon description. // +optional Description string `json:"description,omitempty"` - // Addon type, valid value is helm. + // Add-on type. The valid value is helm. // +unionDiscriminator // +kubebuilder:validation:Required Type AddonType `json:"type"` - // Helm installation spec., it's only being processed if type=helm. + // Helm installation spec. It's processed only when type=helm. // +optional Helm *HelmTypeInstallSpec `json:"helm,omitempty"` @@ -54,41 +54,45 @@ type AddonSpec struct { // +optional InstallSpec *AddonInstallSpec `json:"install,omitempty"` - // Addon installable spec., provide selector and auto-install settings. + // Addon installable spec. It provides selector and auto-install settings. // +optional Installable *InstallableSpec `json:"installable,omitempty"` + + // Plugin installation spec. + // +optional + CliPlugins []CliPlugin `json:"cliPlugins,omitempty"` } -// AddonStatus defines the observed state of Addon +// AddonStatus defines the observed state of an add-on. type AddonStatus struct { - // Addon installation phases. Valid values are Disabled, Enabled, Failed, Enabling, Disabling. + // Add-on installation phases. Valid values are Disabled, Enabled, Failed, Enabling, Disabling. // +kubebuilder:validation:Enum={Disabled,Enabled,Failed,Enabling,Disabling} Phase AddonPhase `json:"phase,omitempty"` - // Describe current state of Addon API installation conditions. + // Describes the current state of add-on API installation conditions. // +optional Conditions []metav1.Condition `json:"conditions,omitempty"` // observedGeneration is the most recent generation observed for this - // Addon. It corresponds to the Addon's generation, which is + // add-on. It corresponds to the add-on's generation, which is // updated on mutation by the API Server. // +optional ObservedGeneration int64 `json:"observedGeneration,omitempty"` } type InstallableSpec struct { - // Addon installable selectors. If multiple selectors are provided - // that all selectors must evaluate to true. + // Add-on installable selectors. If multiple selectors are provided, + // all selectors must evaluate to true. // +optional Selectors []SelectorRequirement `json:"selectors,omitempty"` - // autoInstall defines an addon should auto installed + // autoInstall defines an add-on should be installed automatically. // +kubebuilder:default=false AutoInstall bool `json:"autoInstall"` } type SelectorRequirement struct { - // The selector key, valid values are KubeVersion, KubeGitVersion. + // The selector key. Valid values are KubeVersion, KubeGitVersion. // "KubeVersion" the semver expression of Kubernetes versions, i.e., v1.24. // "KubeGitVersion" may contain distro. info., i.e., v1.24.4+eks. // +kubebuilder:validation:Required @@ -98,14 +102,14 @@ type SelectorRequirement struct { // Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex. // // Possible enum values: - // `"Contains"` line contains string - // `"DoesNotContain"` line does not contain string - // `"MatchRegex"` line contains a match to the regular expression - // `"DoesNotMatchRegex"` line does not contain a match to the regular expression + // `"Contains"` line contains a string. + // `"DoesNotContain"` line does not contain a string. + // `"MatchRegex"` line contains a match to the regular expression. + // `"DoesNotMatchRegex"` line does not contain a match to the regular expression. // +kubebuilder:validation:Required Operator LineSelectorOperator `json:"operator"` - // An array of string values. Server as "OR" expression to the operator. + // An array of string values. It serves as an "OR" expression to the operator. // +optional Values []string `json:"values,omitempty" protobuf:"bytes,3,rep,name=values"` } @@ -115,15 +119,15 @@ type HelmTypeInstallSpec struct { // +kubebuilder:validation:Required ChartLocationURL string `json:"chartLocationURL"` - // installOptions defines Helm release install options. + // installOptions defines Helm release installation options. // +optional InstallOptions HelmInstallOptions `json:"installOptions,omitempty"` - // HelmInstallValues defines Helm release install set values. + // HelmInstallValues defines Helm release installation set values. // +optional InstallValues HelmInstallValues `json:"installValues,omitempty"` - // valuesMapping defines addon normalized resources parameters mapped to Helm values' keys. + // valuesMapping defines add-on normalized resources parameters mapped to Helm values' keys. // +optional ValuesMapping HelmValuesMapping `json:"valuesMapping,omitempty"` } @@ -134,23 +138,23 @@ type HelmInstallValues struct { // +optional URLs []string `json:"urls,omitempty"` - // Selects a key of a ConfigMap item list, the value of ConfigMap can be - // a JSON or YAML string content, use key name with ".json" or ".yaml" or ".yml" - // extension name to specify content type. + // Selects a key of a ConfigMap item list. The value of ConfigMap can be + // a JSON or YAML string content. Use a key name with ".json" or ".yaml" or ".yml" + // extension name to specify a content type. // +optional ConfigMapRefs []DataObjectKeySelector `json:"configMapRefs,omitempty"` - // Selects a key of a Secrets item list, the value of Secrets can be - // a JSON or YAML string content, use key name with ".json" or ".yaml" or ".yml" - // extension name to specify content type. + // Selects a key of a Secrets item list. The value of Secrets can be + // a JSON or YAML string content. Use a key name with ".json" or ".yaml" or ".yml" + // extension name to specify a content type. // +optional SecretRefs []DataObjectKeySelector `json:"secretRefs,omitempty"` - // Helm install set values, can specify multiple or separate values with commas(key1=val1,key2=val2). + // Helm install set values. It can specify multiple or separate values with commas(key1=val1,key2=val2). // +optional SetValues []string `json:"setValues,omitempty"` - // Helm install set JSON values, can specify multiple or separate values with commas(key1=jsonval1,key2=jsonval2). + // Helm install set JSON values. It can specify multiple or separate values with commas(key1=jsonval1,key2=jsonval2). // +optional SetJSONValues []string `json:"setJSONValues,omitempty"` } @@ -176,37 +180,37 @@ type HelmValuesMappingExtraItem struct { } type HelmValueMapType struct { - // replicaCount sets replicaCount value mapping key. + // replicaCount sets the replicaCount value mapping key. // +optional ReplicaCount string `json:"replicaCount,omitempty"` - // persistentVolumeEnabled persistent volume enabled mapping key. + // persistentVolumeEnabled sets the persistent volume enabled mapping key. // +optional PVEnabled string `json:"persistentVolumeEnabled,omitempty"` - // storageClass sets storageClass mapping key. + // storageClass sets the storageClass mapping key. // +optional StorageClass string `json:"storageClass,omitempty"` } type HelmJSONValueMapType struct { - // tolerations sets toleration mapping key. + // tolerations sets the toleration mapping key. // +optional Tolerations string `json:"tolerations,omitempty"` } type HelmValuesMappingItem struct { - // valueMap define the "key" mapping values, valid keys are replicaCount, + // valueMap define the "key" mapping values. Valid keys are replicaCount, // persistentVolumeEnabled, and storageClass. Enum values explained: - // `"replicaCount"` sets replicaCount value mapping key - // `"persistentVolumeEnabled"` sets persistent volume enabled mapping key - // `"storageClass"` sets storageClass mapping key + // `"replicaCount"` sets the replicaCount value mapping key. + // `"persistentVolumeEnabled"` sets the persistent volume enabled mapping key. + // `"storageClass"` sets the storageClass mapping key. // +optional HelmValueMap HelmValueMapType `json:"valueMap,omitempty"` - // jsonMap define the "key" mapping values, valid keys are tolerations. + // jsonMap defines the "key" mapping values. The valid key is tolerations. // Enum values explained: - // `"tolerations"` sets toleration mapping key + // `"tolerations"` sets the toleration mapping key. // +optional HelmJSONMap HelmJSONValueMapType `json:"jsonMap,omitempty"` @@ -217,7 +221,7 @@ type HelmValuesMappingItem struct { type ResourceMappingItem struct { - // storage sets storage size value mapping key. + // storage sets the storage size value mapping key. // +optional Storage string `json:"storage,omitempty"` @@ -235,6 +239,40 @@ type ResourceMappingItem struct { Memory *ResourceReqLimItem `json:"memory,omitempty"` } +type CliPlugin struct { + // Name of the plugin. + // +kubebuilder:validation:Required + Name string `json:"name"` + + // The index repository of the plugin. + // +kubebuilder:validation:Required + IndexRepository string `json:"indexRepository"` + + // The description of the plugin. + // +optional + Description string `json:"description,omitempty"` +} + +func (r *ResourceMappingItem) HasStorageMapping() bool { + return !(r == nil || r.Storage == "") +} + +func (r *ResourceMappingItem) HasCPUReqMapping() bool { + return !(r == nil || r.CPU == nil || r.CPU.Requests == "") +} + +func (r *ResourceMappingItem) HasMemReqMapping() bool { + return !(r == nil || r.CPU == nil || r.Memory.Requests == "") +} + +func (r *ResourceMappingItem) HasCPULimMapping() bool { + return !(r == nil || r.CPU == nil || r.CPU.Limits == "") +} + +func (r *ResourceMappingItem) HasMemLimMapping() bool { + return !(r == nil || r.CPU == nil || r.Memory.Limits == "") +} + type ResourceReqLimItem struct { // Requests value mapping key. // +optional @@ -258,8 +296,8 @@ type DataObjectKeySelector struct { type AddonDefaultInstallSpecItem struct { AddonInstallSpec `json:",inline"` - // Addon default install parameters selectors. If multiple selectors are provided - // that all selectors must evaluate to true. + // Addon installs parameters selectors by default. If multiple selectors are provided, + // all selectors must evaluate to true. // +optional Selectors []SelectorRequirement `json:"selectors,omitempty"` } @@ -271,7 +309,7 @@ type AddonInstallSpec struct { // +optional Enabled bool `json:"enabled,omitempty"` - // Install spec. for extra items. + // Installs spec. for extra items. // +patchMergeKey=name // +patchStrategy=merge,retainKeys // +listType=map @@ -280,6 +318,26 @@ type AddonInstallSpec struct { ExtraItems []AddonInstallExtraItem `json:"extras,omitempty"` } +func (r *AddonInstallSpec) IsDisabled() bool { + return r == nil || !r.Enabled +} + +func (r *AddonInstallSpec) HasSetValues() bool { + if r == nil { + return false + } + + if !r.AddonInstallSpecItem.IsEmpty() { + return true + } + for _, i := range r.ExtraItems { + if !i.IsEmpty() { + return true + } + } + return false +} + type AddonInstallExtraItem struct { AddonInstallSpecItem `json:",inline"` @@ -310,19 +368,30 @@ type AddonInstallSpecItem struct { Resources ResourceRequirements `json:"resources,omitempty"` } +func (r AddonInstallSpecItem) IsEmpty() bool { + return r.Replicas == nil && + r.PVEnabled == nil && + r.StorageClass == "" && + r.Tolerations == "" && + len(r.Resources.Requests) == 0 +} + type ResourceRequirements struct { // Limits describes the maximum amount of compute resources allowed. - // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/. // +optional Limits corev1.ResourceList `json:"limits,omitempty"` // Requests describes the minimum amount of compute resources required. - // If Requests is omitted for a container, it defaults to Limits if that is explicitly specified, - // otherwise to an implementation-defined value. - // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ + // If Requests is omitted for a container, it defaults to Limits if that is explicitly specified; + // otherwise, it defaults to an implementation-defined value. + // More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/. // +optional Requests corev1.ResourceList `json:"requests,omitempty"` } +// +genclient +// +genclient:nonNamespaced +// +k8s:openapi-gen=true // +kubebuilder:object:root=true // +kubebuilder:subresource:status // +kubebuilder:resource:categories={kubeblocks},scope=Cluster @@ -330,7 +399,7 @@ type ResourceRequirements struct { // +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.phase",description="status phase" // +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" -// Addon is the Schema for the addons API +// Addon is the Schema for the add-ons API. type Addon struct { metav1.TypeMeta `json:",inline"` metav1.ObjectMeta `json:"metadata,omitempty"` @@ -341,7 +410,7 @@ type Addon struct { // +kubebuilder:object:root=true -// AddonList contains a list of Addon +// AddonList contains a list of add-ons. type AddonList struct { metav1.TypeMeta `json:",inline"` metav1.ListMeta `json:"metadata,omitempty"` @@ -352,7 +421,7 @@ func init() { SchemeBuilder.Register(&Addon{}, &AddonList{}) } -// GetExtraNames exacter extra items' name. +// GetExtraNames extracts extra items' name. func (r *Addon) GetExtraNames() []string { if r == nil { return nil @@ -386,12 +455,12 @@ func buildSelectorStrings(selectors []SelectorRequirement) []string { return sl } -// GetSelectorsStrings extract selectors to string representations. +// GetSelectorsStrings extracts selectors to string representations. func (r AddonDefaultInstallSpecItem) GetSelectorsStrings() []string { return buildSelectorStrings(r.Selectors) } -// GetSelectorsStrings extract selectors to string representations. +// GetSelectorsStrings extracts selectors to string representations. func (r *InstallableSpec) GetSelectorsStrings() []string { if r == nil { return nil @@ -404,7 +473,7 @@ func (r SelectorRequirement) String() string { r.Key, r.Operator, r.Values) } -// MatchesFromConfig matches selector requirement value. +// MatchesFromConfig matches the selector requirement value. func (r SelectorRequirement) MatchesFromConfig() bool { verIf := viper.Get(constant.CfgKeyServerInfo) ver, ok := verIf.(version.Info) @@ -470,7 +539,7 @@ func (r SelectorRequirement) matchesLine(line string) bool { } } -// GetEnabled provides Enabled property getter. +// GetEnabled provides the Enabled property getter. func (r *AddonInstallSpec) GetEnabled() bool { if r == nil { return false @@ -478,13 +547,26 @@ func (r *AddonInstallSpec) GetEnabled() bool { return r.Enabled } -// BuildMergedValues merge values from a AddonInstallSpec and pre-set values. +// BuildMergedValues merges values from a AddonInstallSpec and pre-set values. func (r *HelmTypeInstallSpec) BuildMergedValues(installSpec *AddonInstallSpec) HelmInstallValues { if r == nil { return HelmInstallValues{} } installValues := r.InstallValues processor := func(installSpecItem AddonInstallSpecItem, valueMapping HelmValuesMappingItem) { + var pvEnabled *bool + defer func() { + if v := valueMapping.HelmValueMap.PVEnabled; v != "" && pvEnabled != nil { + installValues.SetValues = append(installValues.SetValues, + fmt.Sprintf("%s=%v", v, *pvEnabled)) + } + }() + + if installSpecItem.PVEnabled != nil { + b := *installSpecItem.PVEnabled + pvEnabled = &b + } + if installSpecItem.Replicas != nil && *installSpecItem.Replicas >= 0 { if v := valueMapping.HelmValueMap.ReplicaCount; v != "" { installValues.SetValues = append(installValues.SetValues, @@ -504,13 +586,6 @@ func (r *HelmTypeInstallSpec) BuildMergedValues(installSpec *AddonInstallSpec) H } } - if installSpecItem.PVEnabled != nil { - if v := valueMapping.HelmValueMap.PVEnabled; v != "" { - installValues.SetValues = append(installValues.SetValues, - fmt.Sprintf("%s=%v", v, *installSpecItem.PVEnabled)) - } - } - if installSpecItem.Tolerations != "" { if v := valueMapping.HelmJSONMap.Tolerations; v != "" { installValues.SetJSONValues = append(installValues.SetJSONValues, @@ -518,20 +593,24 @@ func (r *HelmTypeInstallSpec) BuildMergedValues(installSpec *AddonInstallSpec) H } } + if valueMapping.ResourcesMapping == nil { + return + } + for k, v := range installSpecItem.Resources.Requests { switch k { case corev1.ResourceStorage: - if valueMapping.ResourcesMapping.Storage != "" && len(valueMapping.ResourcesMapping.PVCSelector) == 0 { + if valueMapping.ResourcesMapping.HasStorageMapping() && len(valueMapping.ResourcesMapping.PVCSelector) == 0 { installValues.SetValues = append(installValues.SetValues, fmt.Sprintf("%s=%v", valueMapping.ResourcesMapping.Storage, v.ToUnstructured())) } case corev1.ResourceCPU: - if valueMapping.ResourcesMapping.CPU.Requests != "" { + if valueMapping.ResourcesMapping.HasCPUReqMapping() { installValues.SetValues = append(installValues.SetValues, fmt.Sprintf("%s=%v", valueMapping.ResourcesMapping.CPU.Requests, v.ToUnstructured())) } case corev1.ResourceMemory: - if valueMapping.ResourcesMapping.Memory.Requests != "" { + if valueMapping.ResourcesMapping.HasMemReqMapping() { installValues.SetValues = append(installValues.SetValues, fmt.Sprintf("%s=%v", valueMapping.ResourcesMapping.Memory.Requests, v.ToUnstructured())) } @@ -541,17 +620,18 @@ func (r *HelmTypeInstallSpec) BuildMergedValues(installSpec *AddonInstallSpec) H for k, v := range installSpecItem.Resources.Limits { switch k { case corev1.ResourceCPU: - if valueMapping.ResourcesMapping.CPU.Limits != "" { + if valueMapping.ResourcesMapping.HasCPULimMapping() { installValues.SetValues = append(installValues.SetValues, fmt.Sprintf("%s=%v", valueMapping.ResourcesMapping.CPU.Limits, v.ToUnstructured())) } case corev1.ResourceMemory: - if valueMapping.ResourcesMapping.Memory.Limits != "" { + if valueMapping.ResourcesMapping.HasMemLimMapping() { installValues.SetValues = append(installValues.SetValues, fmt.Sprintf("%s=%v", valueMapping.ResourcesMapping.Memory.Limits, v.ToUnstructured())) } } } + } processor(installSpec.AddonInstallSpecItem, r.ValuesMapping.HelmValuesMappingItem) for _, ei := range installSpec.ExtraItems { @@ -573,8 +653,36 @@ func (r *ResourceMappingItem) HasPVCSelector() bool { return len(r.PVCSelector) > 0 } -// GetSortedDefaultInstallValues return DefaultInstallValues items with items that has -// provided selector first. +// BuildContainerArgs derives helm container args. +func (r *HelmTypeInstallSpec) BuildContainerArgs(helmContainer *corev1.Container, installValues HelmInstallValues) error { + // Add extra helm installation option flags + for k, v := range r.InstallOptions { + helmContainer.Args = append(helmContainer.Args, fmt.Sprintf("--%s", k)) + if v != "" { + helmContainer.Args = append(helmContainer.Args, v) + } + } + + // Sets values from URL. + for _, urlValue := range installValues.URLs { + helmContainer.Args = append(helmContainer.Args, "--values", urlValue) + } + + // Sets key1=val1,key2=val2 value. + if len(installValues.SetValues) > 0 { + helmContainer.Args = append(helmContainer.Args, "--set", + strings.Join(installValues.SetValues, ",")) + } + + // Sets key1=jsonval1,key2=jsonval2 JSON value. It can be applied to multiple. + for _, v := range installValues.SetJSONValues { + helmContainer.Args = append(helmContainer.Args, "--set-json", v) + } + return nil +} + +// GetSortedDefaultInstallValues returns DefaultInstallValues items with items that have +// a provided selector first. func (r AddonSpec) GetSortedDefaultInstallValues() []AddonDefaultInstallSpecItem { values := make([]AddonDefaultInstallSpecItem, 0, len(r.DefaultInstallValues)) nvalues := make([]AddonDefaultInstallSpecItem, 0, len(r.DefaultInstallValues)) @@ -591,7 +699,7 @@ func (r AddonSpec) GetSortedDefaultInstallValues() []AddonDefaultInstallSpecItem return values } -// NewAddonInstallSpecItem creates an initialized AddonInstallSpecItem object +// NewAddonInstallSpecItem creates an initialized AddonInstallSpecItem object. func NewAddonInstallSpecItem() AddonInstallSpecItem { return AddonInstallSpecItem{ Resources: ResourceRequirements{ diff --git a/apis/extensions/v1alpha1/addon_types_test.go b/apis/extensions/v1alpha1/addon_types_test.go index 28c91e78e..56a2a2b1f 100644 --- a/apis/extensions/v1alpha1/addon_types_test.go +++ b/apis/extensions/v1alpha1/addon_types_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -266,7 +266,7 @@ func TestHelmInstallSpecBuildMergedValues(t *testing.T) { }, } - bulidInstallSpecItem := func() AddonInstallSpecItem { + buildInstallSpecItem := func() AddonInstallSpecItem { toleration := []map[string]string{ { "key": "taint-key", @@ -296,11 +296,11 @@ func TestHelmInstallSpecBuildMergedValues(t *testing.T) { } installSpec := AddonInstallSpec{ - AddonInstallSpecItem: bulidInstallSpecItem(), + AddonInstallSpecItem: buildInstallSpecItem(), ExtraItems: []AddonInstallExtraItem{ { Name: "extra", - AddonInstallSpecItem: bulidInstallSpecItem(), + AddonInstallSpecItem: buildInstallSpecItem(), }, }, } @@ -342,18 +342,21 @@ func TestHelmInstallSpecBuildMergedValues(t *testing.T) { mappingName("primary", sc))).Should(BeElementOf(mergedValues.SetValues)) } -func TestAddonSpecMisc(t *testing.T) { +func TestAddonMisc(t *testing.T) { g := NewGomegaWithT(t) - addonSpec := AddonSpec{} - g.Expect(addonSpec.InstallSpec.GetEnabled()).Should(BeFalse()) - g.Expect(addonSpec.Helm.BuildMergedValues(nil)).Should(BeEquivalentTo(HelmInstallValues{})) - addonSpec.InstallSpec = &AddonInstallSpec{ + addon := Addon{} + g.Expect(addon.GetExtraNames()).Should(BeEmpty()) + g.Expect(addon.Spec.Installable.GetSelectorsStrings()).Should(BeEmpty()) + g.Expect(addon.Spec.InstallSpec.GetEnabled()).Should(BeFalse()) + g.Expect(addon.Spec.Helm.BuildMergedValues(nil)).Should(BeEquivalentTo(HelmInstallValues{})) + + addon.Spec.InstallSpec = &AddonInstallSpec{ Enabled: true, AddonInstallSpecItem: NewAddonInstallSpecItem(), } - g.Expect(addonSpec.InstallSpec.GetEnabled()).Should(BeTrue()) + g.Expect(addon.Spec.InstallSpec.GetEnabled()).Should(BeTrue()) - addonSpec.DefaultInstallValues = []AddonDefaultInstallSpecItem{ + addon.Spec.DefaultInstallValues = []AddonDefaultInstallSpecItem{ { AddonInstallSpec: AddonInstallSpec{ Enabled: true, @@ -373,6 +376,32 @@ func TestAddonSpecMisc(t *testing.T) { }, } - di := addonSpec.GetSortedDefaultInstallValues() + di := addon.Spec.GetSortedDefaultInstallValues() g.Expect(di).Should(HaveLen(2)) } + +func TestAddonInstallHasSetValues(t *testing.T) { + g := NewGomegaWithT(t) + + installSpec := &AddonInstallSpec{ + Enabled: true, + ExtraItems: []AddonInstallExtraItem{ + { + Name: "extra", + }, + }, + } + + g.Expect(installSpec.IsDisabled()).Should(BeFalse()) + g.Expect(installSpec.HasSetValues()).Should(BeFalse()) + installSpec.ExtraItems[0].AddonInstallSpecItem = AddonInstallSpecItem{ + StorageClass: "sc", + } + g.Expect(installSpec.HasSetValues()).Should(BeTrue()) + installSpec.ExtraItems = nil + g.Expect(installSpec.HasSetValues()).Should(BeFalse()) + installSpec.AddonInstallSpecItem = AddonInstallSpecItem{ + StorageClass: "sc", + } + g.Expect(installSpec.HasSetValues()).Should(BeTrue()) +} diff --git a/apis/extensions/v1alpha1/doc.go b/apis/extensions/v1alpha1/doc.go new file mode 100644 index 000000000..e064b641a --- /dev/null +++ b/apis/extensions/v1alpha1/doc.go @@ -0,0 +1,25 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +//go:generate go run ../../../hack/docgen/api/main.go -api-dir . -config ../../../hack/docgen/api/gen-api-doc-config.json -template-dir ../../../hack/docgen/api/template -out-file ../../../docs/user_docs/api-reference/add-on.md + +// +k8s:deepcopy-gen=package,register +// +k8s:openapi-gen=true +// +groupName=extensions.kubeblocks.io +package v1alpha1 diff --git a/apis/extensions/v1alpha1/groupversion_info.go b/apis/extensions/v1alpha1/groupversion_info.go index ce8f37e15..ece76aec8 100644 --- a/apis/extensions/v1alpha1/groupversion_info.go +++ b/apis/extensions/v1alpha1/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/extensions/v1alpha1/type.go b/apis/extensions/v1alpha1/type.go index a52106c16..a213763a5 100644 --- a/apis/extensions/v1alpha1/type.go +++ b/apis/extensions/v1alpha1/type.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/apis/workloads/v1alpha1/consensusset_types.go b/apis/workloads/v1alpha1/consensusset_types.go new file mode 100644 index 000000000..b3a9164d4 --- /dev/null +++ b/apis/workloads/v1alpha1/consensusset_types.go @@ -0,0 +1,306 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// ConsensusSetSpec defines the desired state of ConsensusSet +type ConsensusSetSpec struct { + // Replicas defines number of Pods + // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=0 + // +optional + Replicas int32 `json:"replicas,omitempty"` + + // service defines the behavior of a service spec. + // provides read-write service + // https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Required + Service corev1.ServiceSpec `json:"service"` + + Template corev1.PodTemplateSpec `json:"template"` + + // volumeClaimTemplates is a list of claims that pods are allowed to reference. + // The ConsensusSet controller is responsible for mapping network identities to + // claims in a way that maintains the identity of a pod. Every claim in + // this list must have at least one matching (by name) volumeMount in one + // container in the template. A claim in this list takes precedence over + // any volumes in the template, with the same name. + // +optional + VolumeClaimTemplates []corev1.PersistentVolumeClaim `json:"volumeClaimTemplates,omitempty"` + + // Roles, a list of roles defined in this consensus system. + // +kubebuilder:validation:Required + Roles []ConsensusRole `json:"roles"` + + // RoleObservation provides method to observe role. + // +kubebuilder:validation:Required + RoleObservation RoleObservation `json:"roleObservation"` + + // MembershipReconfiguration provides actions to do membership dynamic reconfiguration. + // +optional + MembershipReconfiguration *MembershipReconfiguration `json:"membershipReconfiguration,omitempty"` + + // UpdateStrategy, Pods update strategy. + // serial: update Pods one by one that guarantee minimum component unavailable time. + // Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) -> Follower(with AccessMode=readWrite) -> Leader + // bestEffortParallel: update Pods in parallel that guarantee minimum component un-writable time. + // Learner, Follower(minority) in parallel -> Follower(majority) -> Leader, keep majority online all the time. + // parallel: force parallel + // +kubebuilder:default=Serial + // +kubebuilder:validation:Enum={Serial,BestEffortParallel,Parallel} + // +optional + UpdateStrategy UpdateStrategy `json:"updateStrategy,omitempty"` + + // Credential used to connect to DB engine + // +optional + Credential *Credential `json:"credential,omitempty"` +} + +// ConsensusSetStatus defines the observed state of ConsensusSet +type ConsensusSetStatus struct { + appsv1.StatefulSetStatus `json:",inline"` + + // InitReplicas is the number of pods(members) when cluster first initialized + // it's set to spec.Replicas at object creation time and never changes + InitReplicas int32 `json:"initReplicas"` + + // ReadyInitReplicas is the number of pods(members) already in MembersStatus in the cluster initialization stage + // will never change once equals to InitReplicas + // +optional + ReadyInitReplicas int32 `json:"readyInitReplicas,omitempty"` + + // members' status. + // +optional + MembersStatus []ConsensusMemberStatus `json:"membersStatus,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={kubeblocks,all},shortName=csset +// +kubebuilder:printcolumn:name="LEADER",type="string",JSONPath=".status.membersStatus[?(@.role.isLeader==true)].podName",description="leader pod name." +// +kubebuilder:printcolumn:name="READY",type="string",JSONPath=".status.readyReplicas",description="ready replicas." +// +kubebuilder:printcolumn:name="REPLICAS",type="string",JSONPath=".status.replicas",description="total replicas." +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// ConsensusSet is the Schema for the consensussets API +type ConsensusSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec ConsensusSetSpec `json:"spec,omitempty"` + Status ConsensusSetStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// ConsensusSetList contains a list of ConsensusSet +type ConsensusSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []ConsensusSet `json:"items"` +} + +type ConsensusRole struct { + // Name, role name. + // +kubebuilder:validation:Required + // +kubebuilder:default=leader + Name string `json:"name"` + + // AccessMode, what service this member capable. + // +kubebuilder:validation:Required + // +kubebuilder:default=ReadWrite + // +kubebuilder:validation:Enum={None, Readonly, ReadWrite} + AccessMode AccessMode `json:"accessMode"` + + // CanVote, whether this member has voting rights + // +kubebuilder:default=true + // +optional + CanVote bool `json:"canVote"` + + // IsLeader, whether this member is the leader + // +kubebuilder:default=false + // +optional + IsLeader bool `json:"isLeader"` +} + +// AccessMode defines SVC access mode enums. +// +enum +type AccessMode string + +const ( + ReadWriteMode AccessMode = "ReadWrite" + ReadonlyMode AccessMode = "Readonly" + NoneMode AccessMode = "None" +) + +// UpdateStrategy defines Cluster Component update strategy. +// +enum +type UpdateStrategy string + +const ( + SerialUpdateStrategy UpdateStrategy = "Serial" + BestEffortParallelUpdateStrategy UpdateStrategy = "BestEffortParallel" + ParallelUpdateStrategy UpdateStrategy = "Parallel" +) + +// RoleObservation defines how to observe role +type RoleObservation struct { + // ObservationActions define Actions to be taken in serial. + // after all actions done, the final output should be a single string of the role name defined in spec.Roles + // latest [BusyBox](https://busybox.net/) image will be used if Image not configured + // Environment variables can be used in Command: + // - v_KB_CONSENSUS_SET_LAST_STDOUT stdout from last action, watch 'v_' prefixed + // - KB_CONSENSUS_SET_USERNAME username part of credential + // - KB_CONSENSUS_SET_PASSWORD password part of credential + // +kubebuilder:validation:Required + ObservationActions []Action `json:"observationActions"` + + // Number of seconds after the container has started before role observation has started. + // +kubebuilder:default=0 + // +kubebuilder:validation:Minimum=0 + // +optional + InitialDelaySeconds int32 `json:"initialDelaySeconds,omitempty"` + + // Number of seconds after which the observation times out. + // Defaults to 1 second. Minimum value is 1. + // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=1 + // +optional + TimeoutSeconds int32 `json:"timeoutSeconds,omitempty"` + + // How often (in seconds) to perform the observation. + // Default to 2 seconds. Minimum value is 1. + // +kubebuilder:default=2 + // +kubebuilder:validation:Minimum=1 + // +optional + PeriodSeconds int32 `json:"periodSeconds,omitempty"` + + // Minimum consecutive successes for the observation to be considered successful after having failed. + // Defaults to 1. Minimum value is 1. + // +kubebuilder:default=1 + // +kubebuilder:validation:Minimum=1 + // +optional + SuccessThreshold int32 `json:"successThreshold,omitempty"` + + // Minimum consecutive failures for the observation to be considered failed after having succeeded. + // Defaults to 3. Minimum value is 1. + // +kubebuilder:default=3 + // +kubebuilder:validation:Minimum=1 + // +optional + FailureThreshold int32 `json:"failureThreshold,omitempty"` +} + +type Credential struct { + // Username + // variable name will be KB_CONSENSUS_SET_USERNAME + // +kubebuilder:validation:Required + Username CredentialVar `json:"username"` + + // Password + // variable name will be KB_CONSENSUS_SET_PASSWORD + // +kubebuilder:validation:Required + Password CredentialVar `json:"password"` +} + +type CredentialVar struct { + // Optional: no more than one of the following may be specified. + + // Variable references $(VAR_NAME) are expanded + // using the previously defined environment variables in the container and + // any service environment variables. If a variable cannot be resolved, + // the reference in the input string will be unchanged. Double $$ are reduced + // to a single $, which allows for escaping the $(VAR_NAME) syntax: i.e. + // "$$(VAR_NAME)" will produce the string literal "$(VAR_NAME)". + // Escaped references will never be expanded, regardless of whether the variable + // exists or not. + // Defaults to "". + // +optional + Value string `json:"value,omitempty"` + + // Source for the environment variable's value. Cannot be used if value is not empty. + // +optional + ValueFrom *corev1.EnvVarSource `json:"valueFrom,omitempty"` +} + +type MembershipReconfiguration struct { + // Environment variables can be used in all following Actions: + // - KB_CONSENSUS_SET_USERNAME username part of credential + // - KB_CONSENSUS_SET_PASSWORD password part of credential + // - KB_CONSENSUS_SET_LEADER_HOST leader host + // - KB_CONSENSUS_SET_TARGET_HOST target host + // - KB_CONSENSUS_SET_SERVICE_PORT port + + // SwitchoverAction specifies how to do switchover + // latest [BusyBox](https://busybox.net/) image will be used if Image not configured + // +optional + SwitchoverAction *Action `json:"switchoverAction,omitempty"` + + // MemberJoinAction specifies how to add member + // previous none-nil action's Image wil be used if not configured + // +optional + MemberJoinAction *Action `json:"memberJoinAction,omitempty"` + + // MemberLeaveAction specifies how to remove member + // previous none-nil action's Image wil be used if not configured + // +optional + MemberLeaveAction *Action `json:"memberLeaveAction,omitempty"` + + // LogSyncAction specifies how to trigger the new member to start log syncing + // previous none-nil action's Image wil be used if not configured + // +optional + LogSyncAction *Action `json:"logSyncAction,omitempty"` + + // PromoteAction specifies how to tell the cluster that the new member can join voting now + // previous none-nil action's Image wil be used if not configured + // +optional + PromoteAction *Action `json:"promoteAction,omitempty"` +} + +type Action struct { + // utility image contains command that can be used to retrieve of process role info + // +optional + Image string `json:"image,omitempty"` + + // Command will be executed in Container to retrieve or process role info + // +kubebuilder:validation:Required + Command []string `json:"command"` +} + +type ConsensusMemberStatus struct { + // PodName pod name. + // +kubebuilder:validation:Required + // +kubebuilder:default=Unknown + PodName string `json:"podName"` + + ConsensusRole `json:"role"` +} + +func init() { + SchemeBuilder.Register(&ConsensusSet{}, &ConsensusSetList{}) +} diff --git a/apis/workloads/v1alpha1/consensusset_webhook.go b/apis/workloads/v1alpha1/consensusset_webhook.go new file mode 100644 index 000000000..b7e575e2b --- /dev/null +++ b/apis/workloads/v1alpha1/consensusset_webhook.go @@ -0,0 +1,113 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/util/validation/field" + ctrl "sigs.k8s.io/controller-runtime" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/webhook" +) + +// log is for logging in this package. +var consensussetlog = logf.Log.WithName("consensusset-resource") + +func (r *ConsensusSet) SetupWebhookWithManager(mgr ctrl.Manager) error { + return ctrl.NewWebhookManagedBy(mgr). + For(r). + Complete() +} + +// TODO(user): EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! + +//+kubebuilder:webhook:path=/mutate-workloads-kubeblocks-io-v1alpha1-consensusset,mutating=true,failurePolicy=fail,sideEffects=None,groups=workloads.kubeblocks.io,resources=consensussets,verbs=create;update,versions=v1alpha1,name=mconsensusset.kb.io,admissionReviewVersions=v1 + +var _ webhook.Defaulter = &ConsensusSet{} + +// Default implements webhook.Defaulter so a webhook will be registered for the type +func (r *ConsensusSet) Default() { + consensussetlog.Info("default", "name", r.Name) + + // TODO(user): fill in your defaulting logic. +} + +// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation. +//+kubebuilder:webhook:path=/validate-workloads-kubeblocks-io-v1alpha1-consensusset,mutating=false,failurePolicy=fail,sideEffects=None,groups=workloads.kubeblocks.io,resources=consensussets,verbs=create;update,versions=v1alpha1,name=vconsensusset.kb.io,admissionReviewVersions=v1 + +var _ webhook.Validator = &ConsensusSet{} + +// ValidateCreate implements webhook.Validator so a webhook will be registered for the type +func (r *ConsensusSet) ValidateCreate() error { + consensussetlog.Info("validate create", "name", r.Name) + + return r.validate() +} + +// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type +func (r *ConsensusSet) ValidateUpdate(old runtime.Object) error { + consensussetlog.Info("validate update", "name", r.Name) + + return r.validate() +} + +// ValidateDelete implements webhook.Validator so a webhook will be registered for the type +func (r *ConsensusSet) ValidateDelete() error { + consensussetlog.Info("validate delete", "name", r.Name) + + return r.validate() +} + +func (r *ConsensusSet) validate() error { + var allErrs field.ErrorList + + // Leader is required + hasHeader := false + for _, role := range r.Spec.Roles { + if role.IsLeader && len(role.Name) > 0 { + hasHeader = true + } + } + if !hasHeader { + allErrs = append(allErrs, + field.Required(field.NewPath("spec.roles"), + "leader is required")) + } + + // servicePort must provide + if len(r.Spec.Service.Ports) == 0 { + allErrs = append(allErrs, + field.Required(field.NewPath("spec.service.ports"), + "servicePort must provide")) + } + + if len(allErrs) > 0 { + return apierrors.NewInvalid( + schema.GroupKind{ + Group: "workloads.kubeblocks.io/v1alpha1", + Kind: "ConsensusSet", + }, + r.Name, allErrs) + } + + return nil +} diff --git a/apis/workloads/v1alpha1/consensusset_webhook_test.go b/apis/workloads/v1alpha1/consensusset_webhook_test.go new file mode 100644 index 000000000..7538bea00 --- /dev/null +++ b/apis/workloads/v1alpha1/consensusset_webhook_test.go @@ -0,0 +1,111 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("ConsensusSet Webhook", func() { + Context("spec validation", func() { + const name = "test-consensus-set" + var csSet *ConsensusSet + + BeforeEach(func() { + csSet = &ConsensusSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: testCtx.DefaultNamespace, + }, + Spec: ConsensusSetSpec{ + Replicas: 1, + RoleObservation: RoleObservation{ + ObservationActions: []Action{ + { + Image: "foo", + Command: []string{"bar"}, + }, + }, + }, + Template: corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "foo", + Image: "bar", + }, + }, + }, + }, + }, + } + }) + + It("should return an error if no leader set", func() { + csSet.Spec.Roles = []ConsensusRole{ + { + Name: "leader", + IsLeader: false, + AccessMode: ReadWriteMode, + }, + } + err := k8sClient.Create(ctx, csSet) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(ContainSubstring("leader is required")) + }) + + It("should return an error if servicePort not provided", func() { + csSet.Spec.Roles = []ConsensusRole{ + { + Name: "leader", + IsLeader: true, + AccessMode: ReadWriteMode, + }, + } + err := k8sClient.Create(ctx, csSet) + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(ContainSubstring("servicePort must provide")) + }) + + It("should succeed if spec is well defined", func() { + csSet.Spec.Roles = []ConsensusRole{ + { + Name: "leader", + IsLeader: true, + AccessMode: ReadWriteMode, + }, + } + csSet.Spec.Service.Ports = []corev1.ServicePort{ + { + Name: "foo", + Protocol: "tcp", + Port: 12345, + }, + } + Expect(k8sClient.Create(ctx, csSet)).Should(Succeed()) + Expect(k8sClient.Delete(ctx, csSet)).Should(Succeed()) + }) + }) +}) diff --git a/apis/workloads/v1alpha1/groupversion_info.go b/apis/workloads/v1alpha1/groupversion_info.go new file mode 100644 index 000000000..3fe68a7a9 --- /dev/null +++ b/apis/workloads/v1alpha1/groupversion_info.go @@ -0,0 +1,39 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +// Package v1alpha1 contains API Schema definitions for the workloads v1alpha1 API group +// +kubebuilder:object:generate=true +// +groupName=workloads.kubeblocks.io +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: "workloads.kubeblocks.io", 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 +) diff --git a/apis/workloads/v1alpha1/webhook_suite_test.go b/apis/workloads/v1alpha1/webhook_suite_test.go new file mode 100644 index 000000000..e0f72fdc8 --- /dev/null +++ b/apis/workloads/v1alpha1/webhook_suite_test.go @@ -0,0 +1,140 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "path/filepath" + "testing" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + //+kubebuilder:scaffold:imports + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + "github.com/apecloud/kubeblocks/internal/testutil" +) + +// 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 +var ctx context.Context +var cancel context.CancelFunc +var testCtx testutil.TestContext + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Webhook Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: false, + WebhookInstallOptions: envtest.WebhookInstallOptions{ + Paths: []string{filepath.Join("..", "..", "..", "config", "webhook")}, + }, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + scheme := runtime.NewScheme() + err = AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + err = admissionv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + // start webhook server using Manager + webhookInstallOptions := &testEnv.WebhookInstallOptions + mgr, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme, + Host: webhookInstallOptions.LocalServingHost, + Port: webhookInstallOptions.LocalServingPort, + CertDir: webhookInstallOptions.LocalServingCertDir, + LeaderElection: false, + MetricsBindAddress: "0", + }) + Expect(err).NotTo(HaveOccurred()) + + err = (&ConsensusSet{}).SetupWebhookWithManager(mgr) + Expect(err).NotTo(HaveOccurred()) + + testCtx = testutil.NewDefaultTestContext(ctx, k8sClient, testEnv) + + //+kubebuilder:scaffold:webhook + + go func() { + defer GinkgoRecover() + err = mgr.Start(ctx) + Expect(err).NotTo(HaveOccurred()) + }() + + // wait for the webhook server to get ready + dialer := &net.Dialer{Timeout: time.Second} + addrPort := fmt.Sprintf("%s:%d", webhookInstallOptions.LocalServingHost, webhookInstallOptions.LocalServingPort) + Eventually(func() error { + conn, err := tls.DialWithDialer(dialer, "tcp", addrPort, &tls.Config{InsecureSkipVerify: true}) + if err != nil { + return err + } + conn.Close() + return nil + }).Should(Succeed()) + +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/cmd/cli/main.go b/cmd/cli/main.go index 14ccda619..82f10f6fe 100644 --- a/cmd/cli/main.go +++ b/cmd/cli/main.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main @@ -24,7 +27,7 @@ import ( ) func main() { - cmd := cmd.NewCliCmd() + cmd := cmd.NewDefaultCliCmd() if err := cli.RunNoErrOutput(cmd); err != nil { util.CheckErr(err) } diff --git a/cmd/cmd.mk b/cmd/cmd.mk index 3e70d72c3..9f2564e80 100644 --- a/cmd/cmd.mk +++ b/cmd/cmd.mk @@ -1,14 +1,20 @@ # -# Copyright ApeCloud, Inc. -# 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. +#Copyright (C) 2022-2023 ApeCloud Co., Ltd +# +#This file is part of KubeBlocks project +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . # ##@ Sub-commands @@ -31,7 +37,6 @@ reloader: test-go-generate build-checks ## Build reloader related binaries clean-reloader: ## Clean bin/reloader. rm -f bin/reloader - ## cue-helper cmd CUE_HELPER_LD_FLAGS = "-s -w" diff --git a/cmd/manager/main.go b/cmd/manager/main.go index e13d6ff2a..2d036816f 100644 --- a/cmd/manager/main.go +++ b/cmd/manager/main.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main @@ -26,6 +29,8 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "github.com/fsnotify/fsnotify" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -42,15 +47,18 @@ import ( dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" appscontrollers "github.com/apecloud/kubeblocks/controllers/apps" + "github.com/apecloud/kubeblocks/controllers/apps/components" dataprotectioncontrollers "github.com/apecloud/kubeblocks/controllers/dataprotection" extensionscontrollers "github.com/apecloud/kubeblocks/controllers/extensions" "github.com/apecloud/kubeblocks/internal/constant" + workloadsv1alpha1 "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + workloadscontrollers "github.com/apecloud/kubeblocks/controllers/workloads" + // +kubebuilder:scaffold:imports discoverycli "k8s.io/client-go/discovery" - "github.com/apecloud/kubeblocks/controllers/apps/components" "github.com/apecloud/kubeblocks/controllers/apps/configuration" k8scorecontrollers "github.com/apecloud/kubeblocks/controllers/k8score" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" @@ -75,19 +83,22 @@ func init() { utilruntime.Must(appsv1alpha1.AddToScheme(scheme)) utilruntime.Must(dataprotectionv1alpha1.AddToScheme(scheme)) utilruntime.Must(snapshotv1.AddToScheme(scheme)) + utilruntime.Must(snapshotv1beta1.AddToScheme(scheme)) utilruntime.Must(extensionsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(workloadsv1alpha1.AddToScheme(scheme)) // +kubebuilder:scaffold:scheme viper.SetConfigName("config") // name of config file (without extension) viper.SetConfigType("yaml") // REQUIRED if the config file does not have the extension in the name viper.AddConfigPath(fmt.Sprintf("/etc/%s/", appName)) // path to look for the config file in - viper.AddConfigPath(fmt.Sprintf("$HOME/.%s", appName)) // call multiple times to add many search paths + viper.AddConfigPath(fmt.Sprintf("$HOME/.%s", appName)) // call multiple times to append search path viper.AddConfigPath(".") // optionally look for config in the working directory viper.AutomaticEnv() viper.SetDefault(constant.CfgKeyCtrlrReconcileRetryDurationMS, 100) viper.SetDefault("CERT_DIR", "/tmp/k8s-webhook-server/serving-certs") viper.SetDefault("VOLUMESNAPSHOT", false) + viper.SetDefault("VOLUMESNAPSHOT_API_BETA", false) viper.SetDefault(constant.KBToolsImage, "apecloud/kubeblocks-tools:latest") viper.SetDefault("PROBE_SERVICE_HTTP_PORT", 3501) viper.SetDefault("PROBE_SERVICE_GRPC_PORT", 50001) @@ -115,29 +126,45 @@ func (r flagName) viperName() string { } func validateRequiredToParseConfigs() error { - if jobTTL := viper.GetString(constant.CfgKeyAddonJobTTL); jobTTL != "" { - if _, err := time.ParseDuration(jobTTL); err != nil { - return err + validateTolerations := func(val string) error { + if val == "" { + return nil } + var tolerations []corev1.Toleration + return json.Unmarshal([]byte(val), &tolerations) } - if cmTolerations := viper.GetString(constant.CfgKeyCtrlrMgrTolerations); cmTolerations != "" { - Tolerations := []corev1.Toleration{} - if err := json.Unmarshal([]byte(cmTolerations), &Tolerations); err != nil { - return err + + validateAffinity := func(val string) error { + if val == "" { + return nil } - } - if cmAffinity := viper.GetString(constant.CfgKeyCtrlrMgrAffinity); cmAffinity != "" { affinity := corev1.Affinity{} - if err := json.Unmarshal([]byte(cmAffinity), &affinity); err != nil { + return json.Unmarshal([]byte(val), &affinity) + } + + if jobTTL := viper.GetString(constant.CfgKeyAddonJobTTL); jobTTL != "" { + if _, err := time.ParseDuration(jobTTL); err != nil { return err } } + if err := validateTolerations(viper.GetString(constant.CfgKeyCtrlrMgrTolerations)); err != nil { + return err + } + if err := validateAffinity(viper.GetString(constant.CfgKeyCtrlrMgrAffinity)); err != nil { + return err + } if cmNodeSelector := viper.GetString(constant.CfgKeyCtrlrMgrNodeSelector); cmNodeSelector != "" { nodeSelector := map[string]string{} if err := json.Unmarshal([]byte(cmNodeSelector), &nodeSelector); err != nil { return err } } + if err := validateTolerations(viper.GetString(constant.CfgKeyDataPlaneTolerations)); err != nil { + return err + } + if err := validateAffinity(viper.GetString(constant.CfgKeyDataPlaneAffinity)); err != nil { + return err + } return nil } @@ -167,7 +194,7 @@ func main() { }) if err := viper.BindPFlags(pflag.CommandLine); err != nil { - setupLog.Error(err, "unable able to bind flags") + setupLog.Error(err, "unable to bind flags") os.Exit(1) } @@ -178,9 +205,13 @@ func main() { // Find and read the config file if err := viper.ReadInConfig(); err != nil { // Handle errors reading the config file - setupLog.Info("unable read in config, errors ignored") + setupLog.Info("unable to read in config, errors ignored") } setupLog.Info(fmt.Sprintf("config file: %s", viper.GetViper().ConfigFileUsed())) + viper.OnConfigChange(func(e fsnotify.Event) { + setupLog.Info(fmt.Sprintf("config file changed: %s", e.Name)) + }) + viper.WatchConfig() metricsAddr = viper.GetString(metricsAddrFlagKey.viperName()) probeAddr = viper.GetString(probeAddrFlagKey.viperName()) @@ -208,12 +239,12 @@ func main() { // LeaderElectionReleaseOnCancel defines if the leader should step down voluntarily // when the Manager ends. This requires the binary to immediately end when the // Manager is stopped, otherwise, this setting is unsafe. Setting this significantly - // speeds up voluntary leader transitions as the new leader don't have to wait + // speeds up voluntary leader transitions as the new leader doesn't have to wait // LeaseDuration time first. // // In the default scaffold provided, the program ends immediately after // the manager stops, so would be fine to enable this option. However, - // if you are doing or is intended to do any operation such as perform cleanups + // if you are doing or intending to do any operation such as performing cleanups // after the manager stops then its usage might be unsafe. LeaderElectionReleaseOnCancel: true, @@ -326,6 +357,14 @@ func main() { } } + if err = (&workloadscontrollers.ConsensusSetReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor("consensus-set-controller"), + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "ConsensusSet") + os.Exit(1) + } // +kubebuilder:scaffold:builder if err = (&configuration.ReconfigureRequestReconciler{ @@ -364,30 +403,22 @@ func main() { os.Exit(1) } - if err = (&components.StatefulSetReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("stateful-set-controller"), - }).SetupWithManager(mgr); err != nil { + if err = components.NewStatefulSetReconciler(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "StatefulSet") os.Exit(1) } - if err = (&components.DeploymentReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("deployment-controller"), - }).SetupWithManager(mgr); err != nil { + if err = components.NewDeploymentReconciler(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "Deployment") os.Exit(1) } - if err = (&components.PodReconciler{ + if err = (&appscontrollers.ComponentClassReconciler{ Client: mgr.GetClient(), Scheme: mgr.GetScheme(), - Recorder: mgr.GetEventRecorderFor("pod-controller"), + Recorder: mgr.GetEventRecorderFor("class-controller"), }).SetupWithManager(mgr); err != nil { - setupLog.Error(err, "unable to create controller", "controller", "Pod") + setupLog.Error(err, "unable to create controller", "controller", "Class") os.Exit(1) } @@ -415,6 +446,11 @@ func main() { os.Exit(1) } + if err = (&workloadsv1alpha1.ConsensusSet{}).SetupWebhookWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create webhook", "webhook", "ConsensusSet") + os.Exit(1) + } + if err = webhook.SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to setup webhook") os.Exit(1) diff --git a/cmd/probe/README.md b/cmd/probe/README.md index 88d92a8c4..f845b64f2 100644 --- a/cmd/probe/README.md +++ b/cmd/probe/README.md @@ -17,7 +17,7 @@ You can get started with Probe, by any of the following methods: ## 2.1 Build -Compiler `Go 1.19+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. +Compiler `Go 1.20+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. Use `go build` to build and produce the `probe` binary file. The executable is produced under current directory. diff --git a/cmd/probe/internal/binding/base.go b/cmd/probe/internal/binding/base.go index a61e84a0c..28728ca91 100644 --- a/cmd/probe/internal/binding/base.go +++ b/cmd/probe/internal/binding/base.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package binding @@ -30,14 +33,14 @@ import ( "github.com/pkg/errors" "github.com/spf13/viper" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type Operation func(ctx context.Context, request *bindings.InvokeRequest, response *bindings.InvokeResponse) (OpsResult, error) type OpsResult map[string]interface{} -// AccessMode define SVC access mode enums. +// AccessMode defines SVC access mode enums. // +enum type AccessMode string @@ -137,7 +140,6 @@ func (ops *BaseOperations) Invoke(ctx context.Context, req *bindings.InvokeReque return nil, errors.Errorf("invoke request required") } - ops.Logger.Debugf("request operation: %v", req.Operation) startTime := time.Now() resp := &bindings.InvokeResponse{ Metadata: map[string]string{ @@ -186,9 +188,10 @@ func (ops *BaseOperations) Invoke(ctx context.Context, req *bindings.InvokeReque func (ops *BaseOperations) CheckRoleOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { opsRes := OpsResult{} + opsRes["operation"] = CheckRoleOperation opsRes["originalRole"] = ops.OriRole if ops.GetRole == nil { - message := fmt.Sprintf("roleCheck operation is not implemented for %v", ops.DBType) + message := fmt.Sprintf("checkRole operation is not implemented for %v", ops.DBType) ops.Logger.Errorf(message) opsRes["event"] = OperationNotImplemented opsRes["message"] = message @@ -198,12 +201,12 @@ func (ops *BaseOperations) CheckRoleOps(ctx context.Context, req *bindings.Invok role, err := ops.GetRole(ctx, req, resp) if err != nil { - ops.Logger.Infof("error executing roleCheck: %v", err) + ops.Logger.Infof("error executing checkRole: %v", err) opsRes["event"] = OperationFailed opsRes["message"] = err.Error() if ops.CheckRoleFailedCount%ops.FailedEventReportFrequency == 0 { ops.Logger.Infof("role checks failed %v times continuously", ops.CheckRoleFailedCount) - resp.Metadata[StatusCode] = OperationFailedHTTPCode + SentProbeEvent(ctx, opsRes, ops.Logger) } ops.CheckRoleFailedCount++ return opsRes, nil @@ -220,9 +223,7 @@ func (ops *BaseOperations) CheckRoleOps(ctx context.Context, req *bindings.Invok opsRes["role"] = role if ops.OriRole != role { ops.OriRole = role - ops.RoleUnchangedCount = 0 - } else { - ops.RoleUnchangedCount++ + SentProbeEvent(ctx, opsRes, ops.Logger) } // RoleUnchangedCount is the count of consecutive role unchanged checks. @@ -230,16 +231,16 @@ func (ops *BaseOperations) CheckRoleOps(ctx context.Context, req *bindings.Invok // then the roleCheck event will be reported at roleEventReportFrequency so that the event controller // can always get relevant roleCheck events in order to maintain the pod label accurately, even in cases // of roleChanged events being lost or the pod role label being deleted or updated incorrectly. - if ops.RoleUnchangedCount < ops.RoleDetectionThreshold && ops.RoleUnchangedCount%roleEventReportFrequency == 0 { - resp.Metadata[StatusCode] = OperationFailedHTTPCode - } + // if ops.RoleUnchangedCount < ops.RoleDetectionThreshold && ops.RoleUnchangedCount%roleEventReportFrequency == 0 { + // resp.Metadata[StatusCode] = OperationFailedHTTPCode + // } return opsRes, nil } func (ops *BaseOperations) GetRoleOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { opsRes := OpsResult{} if ops.GetRole == nil { - message := fmt.Sprintf("roleCheck operation is not implemented for %v", ops.DBType) + message := fmt.Sprintf("getRole operation is not implemented for %v", ops.DBType) ops.Logger.Errorf(message) opsRes["event"] = OperationNotImplemented opsRes["message"] = message @@ -249,12 +250,12 @@ func (ops *BaseOperations) GetRoleOps(ctx context.Context, req *bindings.InvokeR role, err := ops.GetRole(ctx, req, resp) if err != nil { - ops.Logger.Infof("error executing roleCheck: %v", err) + ops.Logger.Infof("error executing getRole: %v", err) opsRes["event"] = OperationFailed opsRes["message"] = err.Error() if ops.CheckRoleFailedCount%ops.FailedEventReportFrequency == 0 { - ops.Logger.Infof("role checks failed %v times continuously", ops.CheckRoleFailedCount) - resp.Metadata[StatusCode] = OperationFailedHTTPCode + ops.Logger.Infof("getRole failed %v times continuously", ops.CheckRoleFailedCount) + // resp.Metadata[StatusCode] = OperationFailedHTTPCode } ops.CheckRoleFailedCount++ return opsRes, nil @@ -264,12 +265,12 @@ func (ops *BaseOperations) GetRoleOps(ctx context.Context, req *bindings.InvokeR return opsRes, nil } -// Component may have some internal roles that need not be exposed to end user, +// Component may have some internal roles that needn't be exposed to end user, // and not configured in cluster definition, e.g. ETCD's Candidate. // roleValidate is used to filter the internal roles and decrease the number // of report events to reduce the possibility of event conflicts. func (ops *BaseOperations) roleValidate(role string) (bool, string) { - // do not validate when db roles setting is missing + // do not validate them when db roles setting is missing if len(ops.DBRoles) == 0 { return true, "" } @@ -293,9 +294,10 @@ func (ops *BaseOperations) roleValidate(role string) (bool, string) { func (ops *BaseOperations) CheckRunningOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { var message string opsRes := OpsResult{} + opsRes["operation"] = CheckRunningOperation host := net.JoinHostPort(ops.DBAddress, strconv.Itoa(ops.DBPort)) - // sql exec timeout need to be less than httpget's timeout which default is 1s. + // sql exec timeout needs to be less than httpget's timeout which by default 1s. conn, err := net.DialTimeout("tcp", host, 500*time.Millisecond) if err != nil { message = fmt.Sprintf("running check %s error: %v", host, err) @@ -304,7 +306,8 @@ func (ops *BaseOperations) CheckRunningOps(ctx context.Context, req *bindings.In opsRes["message"] = message if ops.CheckRunningFailedCount%ops.FailedEventReportFrequency == 0 { ops.Logger.Infof("running checks failed %v times continuously", ops.CheckRunningFailedCount) - resp.Metadata[StatusCode] = OperationFailedHTTPCode + // resp.Metadata[StatusCode] = OperationFailedHTTPCode + SentProbeEvent(ctx, opsRes, ops.Logger) } ops.CheckRunningFailedCount++ return opsRes, nil diff --git a/cmd/probe/internal/binding/base_test.go b/cmd/probe/internal/binding/base_test.go index 8f7a499fb..b9e376d10 100644 --- a/cmd/probe/internal/binding/base_test.go +++ b/cmd/probe/internal/binding/base_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package binding @@ -28,7 +31,7 @@ import ( "github.com/dapr/kit/logger" "github.com/spf13/viper" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type fakeOperations struct { diff --git a/cmd/probe/internal/binding/custom/custom.go b/cmd/probe/internal/binding/custom/custom.go new file mode 100644 index 000000000..57ae37036 --- /dev/null +++ b/cmd/probe/internal/binding/custom/custom.go @@ -0,0 +1,142 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package custom + +import ( + "context" + "encoding/json" + "fmt" + "io" + "net" + "net/http" + "net/url" + "time" + + "github.com/dapr/components-contrib/bindings" + "github.com/dapr/kit/logger" + "github.com/spf13/viper" + + . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" +) + +// HTTPCustom is a binding for an http url endpoint invocation +type HTTPCustom struct { + actionSvcPorts *[]int + client *http.Client + BaseOperations +} + +// NewHTTPCustom returns a new HTTPCustom. +func NewHTTPCustom(logger logger.Logger) bindings.OutputBinding { + return &HTTPCustom{ + actionSvcPorts: &[]int{}, + BaseOperations: BaseOperations{Logger: logger}, + } +} + +// Init performs metadata parsing. +func (h *HTTPCustom) Init(metadata bindings.Metadata) error { + actionSvcList := viper.GetString("KB_CONSENSUS_SET_ACTION_SVC_LIST") + if len(actionSvcList) > 0 { + err := json.Unmarshal([]byte(actionSvcList), h.actionSvcPorts) + if err != nil { + return err + } + } + + // See guidance on proper HTTP client settings here: + // https://medium.com/@nate510/don-t-use-go-s-default-http-client-4804cb19f779 + dialer := &net.Dialer{ + Timeout: 5 * time.Second, + } + netTransport := &http.Transport{ + Dial: dialer.Dial, + TLSHandshakeTimeout: 5 * time.Second, + } + h.client = &http.Client{ + Timeout: time.Second * 30, + Transport: netTransport, + } + + h.BaseOperations.Init(metadata) + h.BaseOperations.GetRole = h.GetRole + h.OperationMap[CheckRoleOperation] = h.CheckRoleOps + + return nil +} + +func (h *HTTPCustom) GetRole(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (string, error) { + if h.actionSvcPorts == nil { + return "", nil + } + + var ( + lastOutput string + err error + ) + + for _, port := range *h.actionSvcPorts { + u := fmt.Sprintf("http://127.0.0.1:%d/role?KB_CONSENSUS_SET_LAST_STDOUT=%s", port, url.QueryEscape(lastOutput)) + lastOutput, err = h.callAction(ctx, u) + if err != nil { + return "", err + } + } + + return lastOutput, nil +} + +func (h *HTTPCustom) GetRoleOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { + role, err := h.GetRole(ctx, req, resp) + if err != nil { + return nil, err + } + opsRes := OpsResult{} + opsRes["role"] = role + return opsRes, nil +} + +// callAction performs an HTTP request to local HTTP endpoint specified by actionSvcPort +func (h *HTTPCustom) callAction(ctx context.Context, url string) (string, error) { + // compose http request + request, err := http.NewRequestWithContext(ctx, "GET", url, nil) + if err != nil { + return "", err + } + + // send http request + resp, err := h.client.Do(request) + if err != nil { + return "", err + } + defer resp.Body.Close() + + // parse http response + if resp.StatusCode/100 != 2 { + return "", fmt.Errorf("received status code %d", resp.StatusCode) + } + b, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + + return string(b), err +} diff --git a/cmd/probe/internal/binding/custom/custom_test.go b/cmd/probe/internal/binding/custom/custom_test.go new file mode 100644 index 000000000..fe6f3e50c --- /dev/null +++ b/cmd/probe/internal/binding/custom/custom_test.go @@ -0,0 +1,85 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package custom + +import ( + "context" + "net/http" + "net/http/httptest" + "strings" + "testing" + + "github.com/dapr/components-contrib/bindings" + "github.com/dapr/kit/logger" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInit(t *testing.T) { + s := httptest.NewServer( + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + _, _ = w.Write([]byte("leader")) + }), + ) + defer s.Close() + + addr := s.Listener.Addr().String() + index := strings.LastIndex(addr, ":") + portStr := addr[index+1:] + viper.Set("KB_CONSENSUS_SET_ACTION_SVC_LIST", "["+portStr+"]") + m := bindings.Metadata{} + hs := NewHTTPCustom(logger.NewLogger("test")) + err := hs.Init(m) + require.NoError(t, err) + + tests := map[string]struct { + input string + operation string + metadata map[string]string + path string + err string + }{ + "get": { + input: `{"event":"Success","operation":"checkRole","originalRole":"","role":"leader"}`, + operation: "checkRole", + metadata: nil, + path: "/", + err: "", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + response, err := hs.Invoke(context.TODO(), &bindings.InvokeRequest{ + Data: []byte(tc.input), + Metadata: tc.metadata, + Operation: bindings.OperationKind(tc.operation), + }) + if tc.err == "" { + require.NoError(t, err) + assert.Equal(t, strings.ToUpper(tc.input), strings.ToUpper(string(response.Data))) + } else { + require.Error(t, err) + assert.Equal(t, tc.err, err.Error()) + } + }) + } +} diff --git a/cmd/probe/internal/binding/etcd/etcd.go b/cmd/probe/internal/binding/etcd/etcd.go index 7c672e108..4fc77a8b3 100644 --- a/cmd/probe/internal/binding/etcd/etcd.go +++ b/cmd/probe/internal/binding/etcd/etcd.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package etcd @@ -28,7 +31,7 @@ import ( v3 "go.etcd.io/etcd/client/v3" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type Etcd struct { @@ -64,7 +67,7 @@ func (e *Etcd) initIfNeed() bool { if e.etcd == nil { go func() { err := e.InitDelay() - e.Logger.Errorf("MongoDB connection init failed: %v", err) + e.Logger.Errorf("Etcd connection init failed: %v", err) }() return true } diff --git a/cmd/probe/internal/binding/etcd/etcd_test.go b/cmd/probe/internal/binding/etcd/etcd_test.go index 98b9c9a39..bb03d8b6e 100644 --- a/cmd/probe/internal/binding/etcd/etcd_test.go +++ b/cmd/probe/internal/binding/etcd/etcd_test.go @@ -1,28 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package etcd import ( "context" + "fmt" "io/ioutil" - "math/rand" + "net" "net/url" "os" - "strconv" "testing" "time" @@ -38,15 +41,14 @@ const ( etcdStartTimeout = 30 ) -// randomize the port to avoid conflicting -var testEndpoint = "http://localhost:" + strconv.Itoa(52600+rand.Intn(1000)) - func TestETCD(t *testing.T) { - etcdServer, err := startEtcdServer(testEndpoint) - defer stopEtcdServer(etcdServer) + etcdServer, err := startEtcdServer("http://localhost:0") if err != nil { t.Errorf("start embedded etcd server error: %s", err) } + defer stopEtcdServer(etcdServer) + testEndpoint := fmt.Sprintf("http://%s", etcdServer.ETCD.Clients[0].Addr().(*net.TCPAddr).String()) + t.Run("Invoke GetRole", func(t *testing.T) { e := mockEtcd(etcdServer) role, err := e.GetRole(context.Background(), &bindings.InvokeRequest{}, &bindings.InvokeResponse{}) diff --git a/cmd/probe/internal/binding/kafka/kafka.go b/cmd/probe/internal/binding/kafka/kafka.go new file mode 100644 index 000000000..258173fc5 --- /dev/null +++ b/cmd/probe/internal/binding/kafka/kafka.go @@ -0,0 +1,135 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package kafka + +import ( + "context" + "strings" + "sync" + + "github.com/dapr/components-contrib/bindings" + "github.com/dapr/kit/logger" + + . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" + "github.com/apecloud/kubeblocks/cmd/probe/internal/component/kafka" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" +) + +const ( + publishTopic = "publishTopic" + topics = "topics" +) + +type KafkaOperations struct { + kafka *kafka.Kafka + publishTopic string + topics []string + closeCh chan struct{} + mu sync.Mutex + BaseOperations +} + +// NewKafka returns a new kafka binding instance. +func NewKafka(logger logger.Logger) bindings.OutputBinding { + k := kafka.NewKafka(logger) + // in kafka binding component, disable consumer retry by default + k.DefaultConsumeRetryEnabled = false + return &KafkaOperations{ + kafka: k, + closeCh: make(chan struct{}), + BaseOperations: BaseOperations{Logger: logger}, + } +} + +func (kafkaOps *KafkaOperations) Init(metadata bindings.Metadata) error { + kafkaOps.BaseOperations.Init(metadata) + kafkaOps.Logger.Debug("Initializing kafka binding") + kafkaOps.DBType = "kafka" + kafkaOps.InitIfNeed = kafkaOps.initIfNeed + // kafkaOps.BaseOperations.GetRole = kafkaOps.GetRole + // kafkaOps.DBPort = kafkaOps.GetRunningPort() + // kafkaOps.RegisterOperation(GetRoleOperation, kafkaOps.GetRoleOps) + // kafkaOps.RegisterOperation(GetLagOperation, kafkaOps.GetLagOps) + kafkaOps.RegisterOperation(CheckStatusOperation, kafkaOps.CheckStatusOps) + // kafkaOps.RegisterOperation(ExecOperation, kafkaOps.ExecOps) + // kafkaOps.RegisterOperation(QueryOperation, kafkaOps.QueryOps) + return nil +} + +func (kafkaOps *KafkaOperations) initIfNeed() bool { + if kafkaOps.kafka.Producer == nil { + go func() { + err := kafkaOps.InitDelay() + kafkaOps.Logger.Errorf("Kafka connection init failed: %v", err) + }() + return true + } + return false +} + +func (kafkaOps *KafkaOperations) InitDelay() error { + kafkaOps.mu.Lock() + defer kafkaOps.mu.Unlock() + if kafkaOps.kafka.Producer != nil { + return nil + } + + err := kafkaOps.kafka.Init(context.TODO(), kafkaOps.Metadata.Properties) + if err != nil { + return err + } + + val, ok := kafkaOps.Metadata.Properties[publishTopic] + if ok && val != "" { + kafkaOps.publishTopic = val + } + + val, ok = kafkaOps.Metadata.Properties[topics] + if ok && val != "" { + kafkaOps.topics = strings.Split(val, ",") + } + + return nil +} + +// CheckStatusOps design details: https://infracreate.feishu.cn/wiki/wikcndch7lMZJneMnRqaTvhQpwb#doxcnOUyQ4Mu0KiUo232dOr5aad +func (kafkaOps *KafkaOperations) CheckStatusOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { + result := OpsResult{} + topic := "kb_health_check" + + err := kafkaOps.kafka.BrokerOpen() + if err != nil { + result["event"] = OperationFailed + result["message"] = err.Error() + return result, nil + } + defer kafkaOps.kafka.BrokerClose() + + err = kafkaOps.kafka.BrokerCreateTopics(topic) + if err != nil { + result["event"] = OperationFailed + result["message"] = err.Error() + } else { + result["event"] = OperationSuccess + result["message"] = "topic validateOnly success" + } + + return result, nil +} diff --git a/cmd/probe/internal/binding/kafka/kafka_test.go b/cmd/probe/internal/binding/kafka/kafka_test.go new file mode 100644 index 000000000..67442c11b --- /dev/null +++ b/cmd/probe/internal/binding/kafka/kafka_test.go @@ -0,0 +1,62 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package kafka + +import ( + "testing" + + "github.com/dapr/components-contrib/bindings" + "github.com/dapr/components-contrib/metadata" + "github.com/dapr/kit/logger" + "github.com/stretchr/testify/assert" + + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" +) + +// Test case for Init() function +func TestInit(t *testing.T) { + kafkaOps := mockKafkaOps(t) + + err := kafkaOps.Init(kafkaOps.Metadata) + if err != nil { + t.Errorf("Error during Init(): %s", err) + } + + assert.Equal(t, "kafka", kafkaOps.DBType) + assert.NotNil(t, kafkaOps.InitIfNeed) + assert.NotNil(t, kafkaOps.OperationMap[CheckStatusOperation]) +} + +func TestCheckStatusOps(t *testing.T) { + // TODO: find mock way +} + +func mockKafkaOps(t *testing.T) *KafkaOperations { + m := bindings.Metadata{ + Base: metadata.Base{ + Properties: map[string]string{}, + }, + } + + kafkaOps := NewKafka(logger.NewLogger("test")).(*KafkaOperations) + _ = kafkaOps.Init(m) + + return kafkaOps +} diff --git a/cmd/probe/internal/binding/mongodb/mongodb.go b/cmd/probe/internal/binding/mongodb/mongodb.go index 79a992162..06f838305 100644 --- a/cmd/probe/internal/binding/mongodb/mongodb.go +++ b/cmd/probe/internal/binding/mongodb/mongodb.go @@ -1,15 +1,20 @@ /* -Copyright ApeCloud, Inc. - -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. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mongodb @@ -25,23 +30,23 @@ import ( "github.com/dapr/components-contrib/bindings" "github.com/dapr/kit/logger" + "github.com/spf13/viper" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo" "go.mongodb.org/mongo-driver/mongo/options" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) -// MongoDB is a binding implementation for MongoDB. -type MongoDB struct { +// MongoDBOperations is a binding implementation for MongoDB. +type MongoDBOperations struct { mongoDBMetadata mu sync.Mutex client *mongo.Client database *mongo.Database operationTimeout time.Duration - logger logger.Logger BaseOperations } @@ -105,7 +110,7 @@ const ( defaultTimeout = 5 * time.Second - defaultDBPort = 27018 + defaultDBPort = 27017 // mongodb://:/ connectionURIFormatWithAuthentication = "mongodb://%s:%s@%s/%s%s" @@ -124,62 +129,62 @@ const ( // NewMongoDB returns a new MongoDB Binding func NewMongoDB(logger logger.Logger) bindings.OutputBinding { - return &MongoDB{BaseOperations: BaseOperations{Logger: logger}} + return &MongoDBOperations{BaseOperations: BaseOperations{Logger: logger}} } // Init initializes the MongoDB Binding. -func (m *MongoDB) Init(metadata bindings.Metadata) error { - m.Logger.Debug("Initializing MongoDB binding") - m.BaseOperations.Init(metadata) +func (mongoOps *MongoDBOperations) Init(metadata bindings.Metadata) error { + mongoOps.Logger.Debug("Initializing MongoDB binding") + mongoOps.BaseOperations.Init(metadata) meta, err := getMongoDBMetaData(metadata) if err != nil { return err } - m.mongoDBMetadata = *meta + mongoOps.mongoDBMetadata = *meta - m.DBType = "mongodb" - m.InitIfNeed = m.initIfNeed - m.DBPort = m.GetRunningPort() - m.OperationMap[GetRoleOperation] = m.GetRoleOps + mongoOps.DBType = "mongodb" + mongoOps.InitIfNeed = mongoOps.initIfNeed + mongoOps.DBPort = mongoOps.GetRunningPort() + mongoOps.BaseOperations.GetRole = mongoOps.GetRole + mongoOps.OperationMap[GetRoleOperation] = mongoOps.GetRoleOps return nil } -func (m *MongoDB) Ping() error { - if err := m.client.Ping(context.Background(), nil); err != nil { - return fmt.Errorf("mongoDB store: error connecting to mongoDB at %s: %s", m.mongoDBMetadata.host, err) +func (mongoOps *MongoDBOperations) Ping() error { + if err := mongoOps.client.Ping(context.Background(), nil); err != nil { + return fmt.Errorf("MongoDB binding: error connecting to MongoDB at %s: %s", mongoOps.mongoDBMetadata.host, err) } return nil } -// InitIfNeed do the real init -func (m *MongoDB) initIfNeed() bool { - if m.database == nil { +func (mongoOps *MongoDBOperations) initIfNeed() bool { + if mongoOps.database == nil { go func() { - err := m.InitDelay() - m.Logger.Errorf("MongoDB connection init failed: %v", err) + err := mongoOps.InitDelay() + mongoOps.Logger.Errorf("MongoDB connection init failed: %v", err) }() return true } return false } -func (m *MongoDB) InitDelay() error { - m.mu.Lock() - defer m.mu.Unlock() - if m.database != nil { +func (mongoOps *MongoDBOperations) InitDelay() error { + mongoOps.mu.Lock() + defer mongoOps.mu.Unlock() + if mongoOps.database != nil { return nil } - m.operationTimeout = m.mongoDBMetadata.operationTimeout + mongoOps.operationTimeout = mongoOps.mongoDBMetadata.operationTimeout - client, err := getMongoDBClient(&m.mongoDBMetadata) + client, err := getMongoDBClient(&mongoOps.mongoDBMetadata) if err != nil { - m.Logger.Errorf("error in creating mongodb client: %s", err) + mongoOps.Logger.Errorf("error in creating MongoDB client: %s", err) return err } if err = client.Ping(context.Background(), nil); err != nil { _ = client.Disconnect(context.Background()) - m.Logger.Errorf("error in connecting to mongodb, host: %s error: %s", m.mongoDBMetadata.host, err) + mongoOps.Logger.Errorf("error in connecting to MongoDB, host: %s error: %s", mongoOps.mongoDBMetadata.host, err) return err } @@ -187,18 +192,22 @@ func (m *MongoDB) InitDelay() error { _, err = getReplSetStatus(context.Background(), db) if err != nil { _ = client.Disconnect(context.Background()) - m.Logger.Errorf("error in getting repl status from mongodb, error: %s", err) + mongoOps.Logger.Errorf("error in getting repl status from mongodb, error: %s", err) return err } - m.client = client - m.database = db + mongoOps.client = client + mongoOps.database = db return nil } -func (m *MongoDB) GetRunningPort() int { - uri := getMongoURI(&m.mongoDBMetadata) +func (mongoOps *MongoDBOperations) GetRunningPort() int { + if viper.IsSet("KB_SERVICE_PORT") { + return viper.GetInt("KB_SERVICE_PORT") + } + + uri := getMongoURI(&mongoOps.mongoDBMetadata) index := strings.Index(uri, "://") if index < 0 { return defaultDBPort @@ -213,12 +222,17 @@ func (m *MongoDB) GetRunningPort() int { if index < 0 { return defaultDBPort } - uri = uri[:index] + uri = uri[index:] index = strings.Index(uri, ":") if index < 0 { return defaultDBPort } - port, err := strconv.Atoi(uri[index+1:]) + portStr := uri[index+1:] + if viper.IsSet("KB_SERVICE_PORT") { + portStr = viper.GetString("KB_SERVICE_PORT") + } + + port, err := strconv.Atoi(portStr) if err != nil { return defaultDBPort } @@ -226,10 +240,10 @@ func (m *MongoDB) GetRunningPort() int { return port } -func (m *MongoDB) GetRole(ctx context.Context, request *bindings.InvokeRequest, response *bindings.InvokeResponse) (string, error) { - status, err := getReplSetStatus(ctx, m.database) +func (mongoOps *MongoDBOperations) GetRole(ctx context.Context, request *bindings.InvokeRequest, response *bindings.InvokeResponse) (string, error) { + status, err := getReplSetStatus(ctx, mongoOps.database) if err != nil { - m.Logger.Errorf("rs.status() error: %", err) + mongoOps.Logger.Errorf("rs.status() error: %", err) return "", err } for _, member := range status.Members { @@ -240,8 +254,8 @@ func (m *MongoDB) GetRole(ctx context.Context, request *bindings.InvokeRequest, return "", errors.New("role not found") } -func (m *MongoDB) GetRoleOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { - role, err := m.GetRole(ctx, req, resp) +func (mongoOps *MongoDBOperations) GetRoleOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { + role, err := mongoOps.GetRole(ctx, req, resp) if err != nil { return nil, err } @@ -250,7 +264,7 @@ func (m *MongoDB) GetRoleOps(ctx context.Context, req *bindings.InvokeRequest, r return opsRes, nil } -func (m *MongoDB) StatusCheck(ctx context.Context, cmd string, response *bindings.InvokeResponse) (OpsResult, error) { +func (mongoOps *MongoDBOperations) StatusCheck(ctx context.Context, cmd string, response *bindings.InvokeResponse) (OpsResult, error) { // TODO implement me when proposal is passed // proposal: https://infracreate.feishu.cn/wiki/wikcndch7lMZJneMnRqaTvhQpwb#doxcnOUyQ4Mu0KiUo232dOr5aad return nil, nil @@ -306,6 +320,10 @@ func getMongoDBMetaData(metadata bindings.Metadata) (*mongoDBMetadata, error) { meta.host = val } + if viper.IsSet("KB_SERVICE_PORT") { + meta.host = "localhost:" + viper.GetString("KB_SERVICE_PORT") + } + if val, ok := metadata.Properties[server]; ok && val != "" { meta.server = val } @@ -326,6 +344,14 @@ func getMongoDBMetaData(metadata bindings.Metadata) (*mongoDBMetadata, error) { meta.password = val } + if viper.IsSet("KB_SERVICE_USER") { + meta.username = viper.GetString("KB_SERVICE_USER") + } + + if viper.IsSet("KB_SERVICE_PASSWORD") { + meta.password = viper.GetString("KB_SERVICE_PASSWORD") + } + meta.databaseName = adminDatabase if val, ok := metadata.Properties[databaseName]; ok && val != "" { meta.databaseName = val diff --git a/cmd/probe/internal/binding/mongodb/mongodb_test.go b/cmd/probe/internal/binding/mongodb/mongodb_test.go index c9defa5da..be84403e4 100644 --- a/cmd/probe/internal/binding/mongodb/mongodb_test.go +++ b/cmd/probe/internal/binding/mongodb/mongodb_test.go @@ -1,15 +1,20 @@ /* -Copyright ApeCloud, Inc. - -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. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mongodb @@ -25,6 +30,8 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" "go.mongodb.org/mongo-driver/mongo/integration/mtest" + + . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" ) func TestGetMongoDBMetadata(t *testing.T) { @@ -200,9 +207,9 @@ func TestGetRole(t *testing.T) { }, }}, }) - m := &MongoDB{ - database: mt.Client.Database(adminDatabase), - logger: logger.NewLogger("mongodb-test"), + m := &MongoDBOperations{ + database: mt.Client.Database(adminDatabase), + BaseOperations: BaseOperations{Logger: logger.NewLogger("mongodb-test")}, } role, err := m.GetRole(context.Background(), &bindings.InvokeRequest{}, &bindings.InvokeResponse{}) if err != nil { diff --git a/cmd/probe/internal/binding/mysql/mysql.go b/cmd/probe/internal/binding/mysql/mysql.go index a52c5fb91..8ef890f08 100644 --- a/cmd/probe/internal/binding/mysql/mysql.go +++ b/cmd/probe/internal/binding/mysql/mysql.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mysql @@ -36,9 +39,10 @@ import ( "github.com/go-sql-driver/mysql" "github.com/pkg/errors" "github.com/spf13/viper" + "golang.org/x/exp/slices" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) // MysqlOperations represents MySQL output bindings. @@ -51,12 +55,12 @@ type MysqlOperations struct { var _ BaseInternalOps = &MysqlOperations{} const ( - // configurations to connect to Mysql, either a data source name represent by URL. + // configurations to connect to MySQL, either a data source name represent by URL. connectionURLKey = "url" - // To connect to MySQL running in Azure over SSL you have to download a + // To connect to MySQL running over SSL you have to download a // SSL certificate. If this is provided the driver will connect using - // SSL. If you have disable SSL you can leave this empty. + // SSL. If you have disabled SSL you can leave this empty. // When the user provides a pem path their connection string must end with // &tls=custom // The connection string should be in the following format @@ -79,14 +83,15 @@ const ( listUserTpl = "SELECT user AS userName, CASE password_expired WHEN 'N' THEN 'F' ELSE 'T' END as expired FROM mysql.user WHERE host = '%' and user <> 'root' and user not like 'kb%';" showGrantTpl = "SHOW GRANTS FOR '%s'@'%%';" getUserTpl = ` - SELECT user AS userName, CASE password_expired WHEN 'N' THEN 'F' ELSE 'T' END as expired - FROM mysql.user + SELECT user AS userName, CASE password_expired WHEN 'N' THEN 'F' ELSE 'T' END as expired + FROM mysql.user WHERE host = '%%' and user <> 'root' and user not like 'kb%%' and user ='%s';" ` - createUserTpl = "CREATE USER '%s'@'%%' IDENTIFIED BY '%s';" - deleteUserTpl = "DROP USER IF EXISTS '%s'@'%%';" - grantTpl = "GRANT %s TO '%s'@'%%';" - revokeTpl = "REVOKE %s FROM '%s'@'%%';" + createUserTpl = "CREATE USER '%s'@'%%' IDENTIFIED BY '%s';" + deleteUserTpl = "DROP USER IF EXISTS '%s'@'%%';" + grantTpl = "GRANT %s TO '%s'@'%%';" + revokeTpl = "REVOKE %s FROM '%s'@'%%';" + listSystemAccountsTpl = "SELECT user AS userName FROM mysql.user WHERE host = '%' and user like 'kb%';" ) var ( @@ -129,6 +134,7 @@ func (mysqlOps *MysqlOperations) Init(metadata bindings.Metadata) error { mysqlOps.RegisterOperation(DescribeUserOp, mysqlOps.describeUserOps) mysqlOps.RegisterOperation(GrantUserRoleOp, mysqlOps.grantUserRoleOps) mysqlOps.RegisterOperation(RevokeUserRoleOp, mysqlOps.revokeUserRoleOps) + mysqlOps.RegisterOperation(ListSystemAccountsOp, mysqlOps.listSystemAccountsOps) return nil } @@ -139,7 +145,7 @@ func (mysqlOps *MysqlOperations) initIfNeed() bool { if err != nil { mysqlOps.Logger.Errorf("MySQL connection init failed: %v", err) } else { - mysqlOps.Logger.Info("MySQL connection init success.") + mysqlOps.Logger.Info("MySQL connection init succeeded.") } }() return true @@ -222,7 +228,7 @@ func (mysqlOps *MysqlOperations) GetRunningPort() int { func (mysqlOps *MysqlOperations) GetRole(ctx context.Context, request *bindings.InvokeRequest, response *bindings.InvokeResponse) (string, error) { sql := "select CURRENT_LEADER, ROLE, SERVER_ID from information_schema.wesql_cluster_local" - // sql exec timeout need to be less than httpget's timeout which default is 1s. + // sql exec timeout needs to be less than httpget's timeout which by default 1s. ctx1, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) defer cancel() rows, err := mysqlOps.db.QueryContext(ctx1, sql) @@ -270,15 +276,39 @@ func (mysqlOps *MysqlOperations) ExecOps(ctx context.Context, req *bindings.Invo func (mysqlOps *MysqlOperations) GetLagOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { result := OpsResult{} + slaveStatus := make([]SlaveStatus, 0) + var err error + + if mysqlOps.OriRole == "" { + mysqlOps.OriRole, err = mysqlOps.GetRole(ctx, req, resp) + if err != nil { + result["event"] = OperationFailed + result["message"] = err.Error() + return result, nil + } + } + if mysqlOps.OriRole == LEADER { + result["event"] = OperationSuccess + result["lag"] = 0 + result["message"] = "This is leader instance, leader has no lag" + return result, nil + } + sql := "show slave status" - _, err := mysqlOps.query(ctx, sql) + data, err := mysqlOps.query(ctx, sql) if err != nil { mysqlOps.Logger.Infof("GetLagOps error: %v", err) result["event"] = OperationFailed result["message"] = err.Error() } else { - result["event"] = OperationSuccess - result["lag"] = 0 + err = json.Unmarshal(data, &slaveStatus) + if err != nil { + result["event"] = OperationFailed + result["message"] = err.Error() + } else { + result["event"] = OperationSuccess + result["lag"] = slaveStatus[0].SecondsBehindMaster + } } return result, nil } @@ -332,7 +362,7 @@ func (mysqlOps *MysqlOperations) CheckStatusOps(ctx context.Context, req *bindin result["event"] = OperationFailed result["message"] = err.Error() if mysqlOps.CheckStatusFailedCount%mysqlOps.FailedEventReportFrequency == 0 { - mysqlOps.Logger.Infof("status checks failed %v times continuously", mysqlOps.CheckStatusFailedCount) + mysqlOps.Logger.Infof("status check failed %v times continuously", mysqlOps.CheckStatusFailedCount) resp.Metadata[StatusCode] = OperationFailedHTTPCode } mysqlOps.CheckStatusFailedCount++ @@ -349,7 +379,7 @@ func propertyToInt(props map[string]string, key string, setter func(int)) error if i, err := strconv.Atoi(v); err == nil { setter(i) } else { - return errors.Wrapf(err, "error converitng %s:%s to int", key, v) + return errors.Wrapf(err, "error converting %s:%s to int", key, v) } } @@ -361,7 +391,7 @@ func propertyToDuration(props map[string]string, key string, setter func(time.Du if d, err := time.ParseDuration(v); err == nil { setter(d) } else { - return errors.Wrapf(err, "error converitng %s:%s to time duration", key, v) + return errors.Wrapf(err, "error converting %s:%s to time duration", key, v) } } @@ -453,7 +483,22 @@ func prepareValues(columnTypes []*sql.ColumnType) []interface{} { } values := make([]interface{}, len(columnTypes)) for i := range values { - values[i] = reflect.New(types[i]).Interface() + switch types[i].Kind() { + case reflect.String, reflect.Interface: + values[i] = &sql.NullString{} + case reflect.Bool: + values[i] = &sql.NullBool{} + case reflect.Float64: + values[i] = &sql.NullFloat64{} + case reflect.Int16, reflect.Uint16: + values[i] = &sql.NullInt16{} + case reflect.Int32, reflect.Uint32: + values[i] = &sql.NullInt32{} + case reflect.Int64, reflect.Uint64: + values[i] = &sql.NullInt64{} + default: + values[i] = reflect.New(types[i]).Interface() + } } return values } @@ -483,17 +528,17 @@ func (mysqlOps *MysqlOperations) convert(columnTypes []*sql.ColumnType, values [ return r } -// InternalQuery is used for internal query, implement BaseInternalOps interface +// InternalQuery is used for internal query, implements BaseInternalOps interface func (mysqlOps *MysqlOperations) InternalQuery(ctx context.Context, sql string) ([]byte, error) { return mysqlOps.query(ctx, sql) } -// InternalExec is used for internal execution, implement BaseInternalOps interface +// InternalExec is used for internal execution, implements BaseInternalOps interface func (mysqlOps *MysqlOperations) InternalExec(ctx context.Context, sql string) (int64, error) { return mysqlOps.exec(ctx, sql) } -// GetLogger is used for getting logger, implement BaseInternalOps interface +// GetLogger is used for getting logger, implements BaseInternalOps interface func (mysqlOps *MysqlOperations) GetLogger() logger.Logger { return mysqlOps.Logger } @@ -506,6 +551,28 @@ func (mysqlOps *MysqlOperations) listUsersOps(ctx context.Context, req *bindings return QueryObject(ctx, mysqlOps, req, ListUsersOp, sqlTplRend, nil, UserInfo{}) } +func (mysqlOps *MysqlOperations) listSystemAccountsOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { + sqlTplRend := func(user UserInfo) string { + return listSystemAccountsTpl + } + dataProcessor := func(data interface{}) (interface{}, error) { + var users []UserInfo + if err := json.Unmarshal(data.([]byte), &users); err != nil { + return nil, err + } + userNames := make([]string, 0) + for _, user := range users { + userNames = append(userNames, user.UserName) + } + if jsonData, err := json.Marshal(userNames); err != nil { + return nil, err + } else { + return string(jsonData), nil + } + } + return QueryObject(ctx, mysqlOps, req, ListSystemAccountsOp, sqlTplRend, dataProcessor, UserInfo{}) +} + func (mysqlOps *MysqlOperations) describeUserOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { var ( object = UserInfo{} @@ -522,16 +589,22 @@ func (mysqlOps *MysqlOperations) describeUserOps(ctx context.Context, req *bindi return nil, err } user := UserInfo{} - userRoles := make([]string, 0) + // only keep one role name of the highest privilege + userRoles := make([]RoleType, 0) for _, roleMap := range roles { for k, v := range roleMap { if len(user.UserName) == 0 { user.UserName = strings.TrimPrefix(strings.TrimSuffix(k, "@%"), "Grants for ") } - userRoles = append(userRoles, mysqlOps.inferRoleFromPriv(strings.TrimPrefix(v, "GRANT "))) + mysqlRoleType := mysqlOps.priv2Role(strings.TrimPrefix(v, "GRANT ")) + userRoles = append(userRoles, mysqlRoleType) } } - user.RoleName = strings.Join(userRoles, ",") + // sort roles by weight + slices.SortFunc(userRoles, SortRoleByWeight) + if len(userRoles) > 0 { + user.RoleName = (string)(userRoles[0]) + } if jsonData, err := json.Marshal([]UserInfo{user}); err != nil { return nil, err } else { @@ -618,7 +691,7 @@ func (mysqlOps *MysqlOperations) managePrivillege(ctx context.Context, req *bind object = UserInfo{} sqlTplRend = func(user UserInfo) string { // render sql stmts - roleDesc, _ := mysqlOps.renderRoleByName(user.RoleName) + roleDesc, _ := mysqlOps.role2Priv(user.RoleName) // update privilege sql := fmt.Sprintf(sqlTpl, roleDesc, user.UserName) return sql @@ -636,20 +709,20 @@ func (mysqlOps *MysqlOperations) managePrivillege(ctx context.Context, req *bind return ExecuteObject(ctx, mysqlOps, req, op, sqlTplRend, msgTplRend, object) } -func (mysqlOps *MysqlOperations) renderRoleByName(roleName string) (string, error) { - switch strings.ToLower(roleName) { +func (mysqlOps *MysqlOperations) role2Priv(roleName string) (string, error) { + roleType := String2RoleType(roleName) + switch roleType { case SuperUserRole: return superUserPriv, nil case ReadWriteRole: return readWritePriv, nil case ReadOnlyRole: return readOnlyRPriv, nil - default: - return "", fmt.Errorf("role name: %s is not supported", roleName) } + return "", fmt.Errorf("role name: %s is not supported", roleName) } -func (mysqlOps *MysqlOperations) inferRoleFromPriv(priv string) string { +func (mysqlOps *MysqlOperations) priv2Role(priv string) RoleType { if strings.HasPrefix(priv, readOnlyRPriv) { return ReadOnlyRole } diff --git a/cmd/probe/internal/binding/mysql/mysql_test.go b/cmd/probe/internal/binding/mysql/mysql_test.go index dd42e44b2..2225cf0ee 100644 --- a/cmd/probe/internal/binding/mysql/mysql_test.go +++ b/cmd/probe/internal/binding/mysql/mysql_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mysql @@ -35,7 +38,7 @@ import ( "github.com/stretchr/testify/assert" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) const ( @@ -81,7 +84,7 @@ func TestInit(t *testing.T) { func TestInitDelay(t *testing.T) { // Initialize a new instance of MysqlOperations. mysqlOps, _, _ := mockDatabase(t) - mysqlOps.initIfNeed() + // mysqlOps.initIfNeed() t.Run("Invalid url", func(t *testing.T) { mysqlOps.db = nil mysqlOps.initIfNeed() @@ -163,6 +166,10 @@ func TestGetLagOps(t *testing.T) { col2 := sqlmock.NewColumn("ROLE").OfType("VARCHAR", "") col3 := sqlmock.NewColumn("SERVER_ID").OfType("INT", 0) rows := sqlmock.NewRowsWithColumnDefinition(col1, col2, col3).AddRow("wesql-main-1.wesql-main-headless:13306", "Follower", 1) + getRoleRows := sqlmock.NewRowsWithColumnDefinition(col1, col2, col3).AddRow("wesql-main-1.wesql-main-headless:13306", "Follower", 1) + if mysqlOps.OriRole == "" { + mock.ExpectQuery("select .* from information_schema.wesql_cluster_local").WillReturnRows(getRoleRows) + } mock.ExpectQuery("show slave status").WillReturnRows(rows) result, err := mysqlOps.GetLagOps(context.Background(), req, &bindings.InvokeResponse{}) @@ -360,7 +367,7 @@ func TestQuery(t *testing.T) { ret, err := mysqlOps.query(context.Background(), `SELECT * FROM foo WHERE id < 4`) assert.Nil(t, err) t.Logf("query result: %s", ret) - assert.Contains(t, string(ret), "\"id\":1") + assert.Contains(t, string(ret), "\"id\":\"1") var result []interface{} err = json.Unmarshal(ret, &result) assert.Nil(t, err) @@ -499,7 +506,7 @@ func TestMySQLAccounts(t *testing.T) { assert.Equal(t, 1, len(users)) assert.Equal(t, userName, users[0].UserName) assert.NotEmpty(t, users[0].RoleName) - assert.Equal(t, users[0].RoleName, ReadOnlyRole) + assert.True(t, ReadOnlyRole.EqualTo(users[0].RoleName)) }) t.Run("List accounts", func(t *testing.T) { @@ -550,7 +557,7 @@ func TestMySQLAccounts(t *testing.T) { assert.Equal(t, ErrNoRoleName.Error(), result[RespTypMsg]) req.Metadata["roleName"] = roleName - roleDesc, err := mysqlOps.renderRoleByName(req.Metadata["roleName"]) + roleDesc, err := mysqlOps.role2Priv(req.Metadata["roleName"]) assert.Nil(t, err) grantRoleCmd := fmt.Sprintf("GRANT %s TO '%s'@'%%';", roleDesc, req.Metadata["userName"]) @@ -580,7 +587,7 @@ func TestMySQLAccounts(t *testing.T) { assert.Equal(t, ErrNoRoleName.Error(), result[RespTypMsg]) req.Metadata["roleName"] = roleName - roleDesc, err := mysqlOps.renderRoleByName(req.Metadata["roleName"]) + roleDesc, err := mysqlOps.role2Priv(req.Metadata["roleName"]) assert.Nil(t, err) revokeRoleCmd := fmt.Sprintf("REVOKE %s FROM '%s'@'%%';", roleDesc, req.Metadata["userName"]) @@ -589,6 +596,32 @@ func TestMySQLAccounts(t *testing.T) { assert.Nil(t, err) assert.Equal(t, RespEveSucc, result[RespTypEve], result[RespTypMsg]) }) + t.Run("List System Accounts", func(t *testing.T) { + var err error + var result OpsResult + + req := &bindings.InvokeRequest{} + req.Operation = CreateUserOp + req.Metadata = map[string]string{} + + col1 := sqlmock.NewColumn("userName").OfType("STRING", "turning") + + rows := sqlmock.NewRowsWithColumnDefinition(col1). + AddRow("kbadmin") + + stmt := "SELECT user AS userName FROM mysql.user WHERE host = '%' and user like 'kb%';" + mock.ExpectQuery(regexp.QuoteMeta(stmt)).WillReturnRows(rows) + + result, err = mysqlOps.listSystemAccountsOps(ctx, req, resp) + assert.Nil(t, err) + assert.Equal(t, RespEveSucc, result[RespTypEve], result[RespTypMsg]) + data := result[RespTypMsg].(string) + users := []string{} + err = json.Unmarshal([]byte(data), &users) + assert.Nil(t, err) + assert.Equal(t, 1, len(users)) + assert.Equal(t, "kbadmin", users[0]) + }) } func mockDatabase(t *testing.T) (*MysqlOperations, sqlmock.Sqlmock, error) { viper.SetDefault("KB_SERVICE_ROLES", "{\"follower\":\"Readonly\",\"leader\":\"ReadWrite\"}") diff --git a/cmd/probe/internal/binding/postgres/postgres.go b/cmd/probe/internal/binding/postgres/postgres.go index 14b55a7ff..1b36d6cc3 100644 --- a/cmd/probe/internal/binding/postgres/postgres.go +++ b/cmd/probe/internal/binding/postgres/postgres.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package postgres @@ -21,7 +24,6 @@ import ( "encoding/json" "fmt" "strconv" - "strings" "sync" "time" @@ -30,26 +32,25 @@ import ( "github.com/jackc/pgx/v5/pgxpool" "github.com/pkg/errors" "github.com/spf13/viper" + "golang.org/x/exp/slices" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) // List of operations. const ( connectionURLKey = "url" commandSQLKey = "sql" - PRIMARY = "primary" - SECONDARY = "secondary" listUserTpl = ` SELECT usename AS userName, valuntil 0 { + users[i].RoleName = string(roleTypes[0]) + } } if jsonData, err := json.Marshal(users); err != nil { return nil, err @@ -524,13 +567,13 @@ func pgUserRolesProcessor(data interface{}) (interface{}, error) { } } -func (pgOps *PostgresOperations) renderRoleByName(roleName string) (string, error) { - switch strings.ToLower(roleName) { +func (pgOps *PostgresOperations) role2PGRole(roleName string) (string, error) { + roleType := String2RoleType(roleName) + switch roleType { case ReadWriteRole: return "pg_write_all_data", nil case ReadOnlyRole: return "pg_read_all_data", nil - default: - return "", fmt.Errorf("role name: %s is not supported", roleName) } + return "", fmt.Errorf("role name: %s is not supported", roleName) } diff --git a/cmd/probe/internal/binding/postgres/postgres_test.go b/cmd/probe/internal/binding/postgres/postgres_test.go index f99b7c19a..19a1b136c 100644 --- a/cmd/probe/internal/binding/postgres/postgres_test.go +++ b/cmd/probe/internal/binding/postgres/postgres_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package postgres @@ -31,7 +34,7 @@ import ( "github.com/stretchr/testify/assert" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) const ( @@ -193,11 +196,11 @@ func TestPostgresIntegration(t *testing.T) { }) } -// SETUP TESTS, run as `postgre` to manage accounts -// 1. exprot PGUSER=potgres -// 2. exprot PGPASSWORD= +// SETUP TESTS, run as `postgres` to manage accounts +// 1. export PGUSER=potgres +// 2. export PGPASSWORD= // 4. export POSTGRES_TEST_CONN_URL="postgres://${PGUSER}:${PGPASSWORD}@localhost:5432/postgres" -// 5. `go test -v -count=1 ./bindings/postgres -run ^TestPostgresIntegrationAccounts` +// 5. `go test -v -count=1 ./cmd/probe/internal/binding/postgres -run ^TestPostgresIntegrationAccounts` func TestPostgresIntegrationAccounts(t *testing.T) { url := os.Getenv("POSTGRES_TEST_CONN_URL") if url == "" { @@ -264,6 +267,14 @@ func TestPostgresIntegrationAccounts(t *testing.T) { res, err = b.Invoke(ctx, req) assertResponse(t, res, err, RespEveSucc) + // list system users + req = &bindings.InvokeRequest{ + Operation: ListSystemAccountsOp, + Metadata: map[string]string{}, + } + res, err = b.Invoke(ctx, req) + assertResponse(t, res, err, RespEveSucc) + // grant role req = &bindings.InvokeRequest{ Operation: GrantUserRoleOp, @@ -280,9 +291,16 @@ func TestPostgresIntegrationAccounts(t *testing.T) { res, err = b.Invoke(ctx, req) assertResponse(t, res, err, RespEveFail) - req.Metadata["roleName"] = roleName - res, err = b.Invoke(ctx, req) - assertResponse(t, res, err, RespEveSucc) + for _, roleType := range []RoleType{ReadOnlyRole, ReadWriteRole, SuperUserRole} { + roleStr := (string)(roleType) + req.Metadata["roleName"] = roleStr + res, err = b.Invoke(ctx, req) + assertResponse(t, res, err, RespEveSucc) + + req.Metadata["roleName"] = strings.ToUpper(roleStr) + res, err = b.Invoke(ctx, req) + assertResponse(t, res, err, RespEveSucc) + } // revoke role req = &bindings.InvokeRequest{ @@ -300,10 +318,16 @@ func TestPostgresIntegrationAccounts(t *testing.T) { res, err = b.Invoke(ctx, req) assertResponse(t, res, err, RespEveFail) - req.Metadata["roleName"] = roleName - res, err = b.Invoke(ctx, req) - assertResponse(t, res, err, RespEveSucc) + for _, roleType := range []RoleType{ReadOnlyRole, ReadWriteRole, SuperUserRole} { + roleStr := (string)(roleType) + req.Metadata["roleName"] = roleStr + res, err = b.Invoke(ctx, req) + assertResponse(t, res, err, RespEveSucc) + req.Metadata["roleName"] = strings.ToUpper(roleStr) + res, err = b.Invoke(ctx, req) + assertResponse(t, res, err, RespEveSucc) + } // delete user req = &bindings.InvokeRequest{ Operation: DeleteUserOp, diff --git a/cmd/probe/internal/binding/redis/redis.go b/cmd/probe/internal/binding/redis/redis.go index 16bac29e0..764f53c58 100644 --- a/cmd/probe/internal/binding/redis/redis.go +++ b/cmd/probe/internal/binding/redis/redis.go @@ -1,24 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis import ( "context" - "encoding/json" "fmt" "strconv" "strings" @@ -27,12 +29,16 @@ import ( "github.com/redis/go-redis/v9" "golang.org/x/exp/slices" - bindings "github.com/dapr/components-contrib/bindings" + "github.com/dapr/components-contrib/bindings" "github.com/dapr/kit/logger" + // import this json-iterator package to replace the default + // to avoid the error: 'json: unsupported type: map[interface {}]interface {}' + json "github.com/json-iterator/go" + . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" rediscomponent "github.com/apecloud/kubeblocks/cmd/probe/internal/component/redis" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var ( @@ -86,6 +92,7 @@ func (r *Redis) Init(meta bindings.Metadata) (err error) { r.RegisterOperation(DescribeUserOp, r.describeUserOps) r.RegisterOperation(GrantUserRoleOp, r.grantUserRoleOps) r.RegisterOperation(RevokeUserRoleOp, r.revokeUserRoleOps) + r.RegisterOperation(ListSystemAccountsOp, r.listSystemAccountsOps) return nil } @@ -111,7 +118,7 @@ func (r *Redis) initIfNeed() bool { if err := r.initDelay(); err != nil { r.Logger.Errorf("redis connection init failed: %v", err) } else { - r.Logger.Info("redis connection init succeed.") + r.Logger.Info("redis connection init succeeded.") } }() return true @@ -153,9 +160,12 @@ func (r *Redis) GetLogger() logger.Logger { return r.Logger } -// InternalQuery is used for internal query, implement BaseInternalOps interface. +// InternalQuery is used for internal query, implements BaseInternalOps interface. func (r *Redis) InternalQuery(ctx context.Context, cmd string) ([]byte, error) { redisArgs := tokenizeCmd2Args(cmd) + // Be aware of the result type. + // type of result could be string, []string, []interface{}, map[interface]interface{} + // it is determined by the combination of command and redis version. result, err := r.query(ctx, redisArgs...) if err != nil { return nil, err @@ -163,7 +173,7 @@ func (r *Redis) InternalQuery(ctx context.Context, cmd string) ([]byte, error) { return json.Marshal(result) } -// InternalExec is used for internal execution, implement BaseInternalOps interface. +// InternalExec is used for internal execution, implements BaseInternalOps interface. func (r *Redis) InternalExec(ctx context.Context, cmd string) (int64, error) { // split command into array of args redisArgs := tokenizeCmd2Args(cmd) @@ -175,7 +185,7 @@ func (r *Redis) exec(ctx context.Context, args ...interface{}) error { } func (r *Redis) query(ctx context.Context, args ...interface{}) (interface{}, error) { - // parse result into an slice of string + // parse result into a slice of string return r.client.Do(ctx, args...).Result() } @@ -268,51 +278,54 @@ func (r *Redis) listUsersOps(ctx context.Context, req *bindings.InvokeRequest, r return QueryObject(ctx, r, req, ListUsersOp, cmdRender, dataProcessor, UserInfo{}) } +func (r *Redis) listSystemAccountsOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { + dataProcessor := func(data interface{}) (interface{}, error) { + // data is an array of interface{} of string + results := make([]string, 0) + err := json.Unmarshal(data.([]byte), &results) + if err != nil { + return nil, err + } + sysetmUsers := make([]string, 0) + for _, user := range results { + if slices.Contains(redisPreDefinedUsers, user) { + sysetmUsers = append(sysetmUsers, user) + } + } + if jsonData, err := json.Marshal(sysetmUsers); err != nil { + return nil, err + } else { + return string(jsonData), nil + } + } + cmdRender := func(user UserInfo) string { + return "ACL USERS" + } + + return QueryObject(ctx, r, req, ListUsersOp, cmdRender, dataProcessor, UserInfo{}) +} + func (r *Redis) describeUserOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { var ( object = UserInfo{} + profile map[string]string + err error dataProcessor = func(data interface{}) (interface{}, error) { - redisUserPrivContxt := []string{"commands", "keys", "channels", "selectors"} - redisUserInfoContext := []string{"flags", "passwords"} - - profile := make(map[string]string, 0) - results := make([]interface{}, 0) - err := json.Unmarshal(data.([]byte), &results) + // parse it to a map or an []interface + // try map first + profile, err = parseCommandAndKeyFromMap(data) if err != nil { - return nil, err - } - - var context string - for i := 0; i < len(results); i++ { - result := results[i] - switch result := result.(type) { - case string: - strVal := strings.TrimSpace(result) - if len(strVal) == 0 { - continue - } - if slices.Contains(redisUserInfoContext, strVal) { - i++ - continue - } - if slices.Contains(redisUserPrivContxt, strVal) { - context = strVal - } else { - profile[context] = strVal - } - case []interface{}: - selectors := make([]string, 0) - for _, sel := range result { - selectors = append(selectors, sel.(string)) - } - profile[context] = strings.Join(selectors, " ") + // try list + profile, err = parseCommandAndKeyFromList(data) + if err != nil { + return nil, err } } users := make([]UserInfo, 0) user := UserInfo{ UserName: object.UserName, - RoleName: redisPriv2RoleName(profile["commands"] + " " + profile["keys"]), + RoleName: (string)(r.priv2Role(profile["commands"] + " " + profile["keys"])), } users = append(users, user) if jsonData, err := json.Marshal(users); err != nil { @@ -336,6 +349,83 @@ func (r *Redis) describeUserOps(ctx context.Context, req *bindings.InvokeRequest return QueryObject(ctx, r, req, DescribeUserOp, cmdRender, dataProcessor, object) } +func parseCommandAndKeyFromList(data interface{}) (map[string]string, error) { + var ( + redisUserPrivContxt = []string{"commands", "keys", "channels", "selectors"} + redisUserInfoContext = []string{"flags", "passwords"} + ) + + profile := make(map[string]string, 0) + results := make([]interface{}, 0) + + err := json.Unmarshal(data.([]byte), &results) + if err != nil { + return nil, err + } + // parse line by line + var context string + for i := 0; i < len(results); i++ { + result := results[i] + switch result := result.(type) { + case string: + strVal := strings.TrimSpace(result) + if len(strVal) == 0 { + continue + } + if slices.Contains(redisUserInfoContext, strVal) { + i++ + continue + } + if slices.Contains(redisUserPrivContxt, strVal) { + context = strVal + } else { + profile[context] = strVal + } + case []interface{}: + selectors := make([]string, 0) + for _, sel := range result { + selectors = append(selectors, sel.(string)) + } + profile[context] = strings.Join(selectors, " ") + } + } + return profile, nil +} + +func parseCommandAndKeyFromMap(data interface{}) (map[string]string, error) { + var ( + redisUserPrivContxt = []string{"commands", "keys", "channels", "selectors"} + ) + + profile := make(map[string]string, 0) + results := make(map[string]interface{}, 0) + + err := json.Unmarshal(data.([]byte), &results) + if err != nil { + return nil, err + } + for k, v := range results { + // each key is string, and each v is string or list of string + if !slices.Contains(redisUserPrivContxt, k) { + continue + } + + switch v := v.(type) { + case string: + profile[k] = v + case []interface{}: + selectors := make([]string, 0) + for _, sel := range v { + selectors = append(selectors, sel.(string)) + } + profile[k] = strings.Join(selectors, " ") + default: + return nil, fmt.Errorf("unknown data type: %v", v) + } + } + return profile, nil +} + func (r *Redis) createUserOps(ctx context.Context, req *bindings.InvokeRequest, resp *bindings.InvokeResponse) (OpsResult, error) { var ( object = UserInfo{} @@ -399,7 +489,7 @@ func (r *Redis) managePrivillege(ctx context.Context, req *bindings.InvokeReques object = UserInfo{} cmdRend = func(user UserInfo) string { - command := roleName2RedisPriv(op, user.RoleName) + command := r.role2Priv(op, user.RoleName) return fmt.Sprintf("ACL SETUSER %s %s", user.UserName, command) } @@ -418,7 +508,7 @@ func (r *Redis) managePrivillege(ctx context.Context, req *bindings.InvokeReques return ExecuteObject(ctx, r, req, op, cmdRend, msgTplRend, object) } -func roleName2RedisPriv(op bindings.OperationKind, roleName string) string { +func (r *Redis) role2Priv(op bindings.OperationKind, roleName string) string { const ( grantPrefix = "+" revokePrefix = "-" @@ -430,22 +520,23 @@ func roleName2RedisPriv(op bindings.OperationKind, roleName string) string { prefix = revokePrefix } var command string - switch roleName { - case ReadOnlyRole: - command = fmt.Sprintf("-@all %s@read allkeys", prefix) - case ReadWriteRole: - command = fmt.Sprintf("-@all %s@write %s@read allkeys", prefix, prefix) + + roleType := String2RoleType(roleName) + switch roleType { case SuperUserRole: command = fmt.Sprintf("%s@all allkeys", prefix) + case ReadWriteRole: + command = fmt.Sprintf("-@all %s@write %s@read allkeys", prefix, prefix) + case ReadOnlyRole: + command = fmt.Sprintf("-@all %s@read allkeys", prefix) } return command } -func redisPriv2RoleName(commands string) string { +func (r *Redis) priv2Role(commands string) RoleType { if commands == "-@all" { return NoPrivileges } - switch commands { case "-@all +@read ~*": return ReadOnlyRole @@ -459,13 +550,15 @@ func redisPriv2RoleName(commands string) string { } func (r *Redis) Close() error { + if r.cancel == nil { + return nil + } r.cancel() - return r.client.Close() } func (r *Redis) GetRole(ctx context.Context, request *bindings.InvokeRequest, response *bindings.InvokeResponse) (string, error) { - // sql exec timeout need to be less than httpget's timeout which default is 1s. + // sql exec timeout needs to be less than httpget's timeout which by default 1s. // ctx1, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond) // defer cancel() ctx1 := ctx @@ -487,6 +580,12 @@ func (r *Redis) GetRole(ctx context.Context, request *bindings.InvokeRequest, re } } } + if role == MASTER { + return PRIMARY, nil + } + if role == SLAVE { + return SECONDARY, nil + } return role, nil } diff --git a/cmd/probe/internal/binding/redis/redis_test.go b/cmd/probe/internal/binding/redis/redis_test.go index 5280295ad..49e21a3f6 100644 --- a/cmd/probe/internal/binding/redis/redis_test.go +++ b/cmd/probe/internal/binding/redis/redis_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis @@ -28,10 +31,10 @@ import ( "github.com/dapr/components-contrib/bindings" "github.com/dapr/kit/logger" - redismock "github.com/go-redis/redismock/v9" + "github.com/go-redis/redismock/v9" . "github.com/apecloud/kubeblocks/cmd/probe/internal/binding" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) const ( @@ -163,7 +166,7 @@ func TestRedisGetRoles(t *testing.T) { err = json.Unmarshal(bindingRes.Data, &opsResult) assert.Nil(t, err) assert.Equal(t, RespEveSucc, opsResult[RespTypEve]) - assert.Equal(t, "master", opsResult["role"]) + assert.Equal(t, PRIMARY, opsResult["role"]) // invoke one more time bindingRes, err = r.Invoke(context.TODO(), request) @@ -171,7 +174,7 @@ func TestRedisGetRoles(t *testing.T) { err = json.Unmarshal(bindingRes.Data, &opsResult) assert.Nil(t, err) assert.Equal(t, RespEveSucc, opsResult[RespTypEve]) - assert.Equal(t, "slave", opsResult["role"]) + assert.Equal(t, SECONDARY, opsResult["role"]) } func TestRedisAccounts(t *testing.T) { @@ -299,7 +302,7 @@ func TestRedisAccounts(t *testing.T) { testName: "validInput", testMetaData: map[string]string{ "userName": userName, - "roleName": roleName, + "roleName": (string)(roleName), }, expectEveType: RespEveSucc, }, @@ -307,7 +310,7 @@ func TestRedisAccounts(t *testing.T) { for _, ops := range []bindings.OperationKind{GrantUserRoleOp, RevokeUserRoleOp} { // mock exepctation - args := tokenizeCmd2Args(fmt.Sprintf("ACL SETUSER %s %s", userName, roleName2RedisPriv(ops, roleName))) + args := tokenizeCmd2Args(fmt.Sprintf("ACL SETUSER %s %s", userName, r.role2Priv(ops, (string)(roleName)))) mock.ExpectDo(args...).SetVal("ok") request := &bindings.InvokeRequest{ @@ -353,6 +356,15 @@ func TestRedisAccounts(t *testing.T) { "selectors", []interface{}{}, } + + userInfoMap = map[string]interface{}{ + "flags": []interface{}{"on"}, + "passwords": []interface{}{"mock-password"}, + "commands": "+@all", + "keys": "~*", + "channels": "", + "selectors": []interface{}{}, + } ) testCases := []redisTestCase{ @@ -383,10 +395,18 @@ func TestRedisAccounts(t *testing.T) { }, expectEveType: RespEveSucc, }, + { + testName: "validInputAsMap", + testMetaData: map[string]string{ + "userName": userName, + }, + expectEveType: RespEveSucc, + }, } mock.ExpectDo("ACL", "GETUSER", userName).RedisNil() mock.ExpectDo("ACL", "GETUSER", userName).SetVal(userInfo) + mock.ExpectDo("ACL", "GETUSER", userName).SetVal(userInfoMap) for _, accTest := range testCases { request.Metadata = accTest.testMetaData @@ -407,7 +427,7 @@ func TestRedisAccounts(t *testing.T) { assert.Len(t, users, 1) user := users[0] assert.Equal(t, userName, user.UserName) - assert.Equal(t, SuperUserRole, user.RoleName) + assert.True(t, SuperUserRole.EqualTo(user.RoleName)) } } mock.ClearExpect() @@ -464,7 +484,7 @@ func TestRedisAccounts(t *testing.T) { t.Run("RoleName Conversion", func(t *testing.T) { type roleTestCase struct { - roleName string + roleName RoleType redisPrivs string } grantTestCases := []roleTestCase{ @@ -482,12 +502,12 @@ func TestRedisAccounts(t *testing.T) { }, } for _, test := range grantTestCases { - cmd := roleName2RedisPriv(GrantUserRoleOp, test.roleName) + cmd := r.role2Priv(GrantUserRoleOp, (string)(test.roleName)) assert.Equal(t, test.redisPrivs, cmd) // allkeys -> ~* cmd = strings.Replace(cmd, "allkeys", "~*", 1) - inferredRole := redisPriv2RoleName(cmd) + inferredRole := r.priv2Role(cmd) assert.Equal(t, test.roleName, inferredRole) } @@ -506,10 +526,35 @@ func TestRedisAccounts(t *testing.T) { }, } for _, test := range revokeTestCases { - cmd := roleName2RedisPriv(RevokeUserRoleOp, test.roleName) + cmd := r.role2Priv(RevokeUserRoleOp, (string)(test.roleName)) assert.Equal(t, test.redisPrivs, cmd) } }) + // list accounts + t.Run("List System Accounts", func(t *testing.T) { + mock.ExpectDo("ACL", "USERS").SetVal([]string{"ape", "default", "kbadmin"}) + + response, err := r.Invoke(ctx, &bindings.InvokeRequest{ + Operation: ListSystemAccountsOp, + }) + + assert.Nil(t, err) + assert.NotNil(t, response) + assert.NotNil(t, response.Data) + // parse result + opsResult := OpsResult{} + _ = json.Unmarshal(response.Data, &opsResult) + assert.Equal(t, RespEveSucc, opsResult[RespTypEve], opsResult[RespTypMsg]) + + users := []string{} + err = json.Unmarshal([]byte(opsResult[RespTypMsg].(string)), &users) + assert.Nil(t, err) + assert.NotEmpty(t, users) + assert.Len(t, users, 2) + assert.Contains(t, users, "kbadmin") + assert.Contains(t, users, "default") + mock.ClearExpect() + }) } func mockRedisOps(t *testing.T) (*Redis, redismock.ClientMock) { diff --git a/cmd/probe/internal/binding/types.go b/cmd/probe/internal/binding/types.go index dd03725f9..455362551 100644 --- a/cmd/probe/internal/binding/types.go +++ b/cmd/probe/internal/binding/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package binding @@ -54,19 +57,14 @@ const ( ) const ( - RespTypEve = "event" - RespTypMsg = "message" - RespTypMeta = "metadata" - - RespEveSucc = "Success" - RespEveFail = "Failed" - - SuperUserRole string = "superuser" - ReadWriteRole string = "readwrite" - ReadOnlyRole string = "readonly" - NoPrivileges string = "" - CustomizedRole string = "customized" - InvalidRole string = "invalid" + PRIMARY = "primary" + SECONDARY = "secondary" + MASTER = "master" + SLAVE = "slave" + LEADER = "Leader" + FOLLOWER = "Follower" + LEARNER = "Learner" + CANDIDATE = "Candidate" ) const ( @@ -86,3 +84,7 @@ var ( ErrInvalidRoleName = fmt.Errorf(errMsgInvalidRoleName) ErrNoSuchUser = fmt.Errorf(errMsgNoSuchUser) ) + +type SlaveStatus struct { + SecondsBehindMaster int64 `json:"Seconds_Behind_Master"` +} diff --git a/cmd/probe/internal/binding/utils.go b/cmd/probe/internal/binding/utils.go index 118928fa6..ed5f9e56d 100644 --- a/cmd/probe/internal/binding/utils.go +++ b/cmd/probe/internal/binding/utils.go @@ -1,39 +1,46 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package binding import ( + "bytes" "context" "encoding/json" "fmt" - "strings" + "os" + "text/template" "time" "github.com/dapr/components-contrib/bindings" - "golang.org/x/exp/slices" + "github.com/dapr/kit/logger" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) -type UserInfo struct { - UserName string `json:"userName"` - Password string `json:"password,omitempty"` - Expired string `json:"expired,omitempty"` - RoleName string `json:"roleName,omitempty"` -} - type RedisEntry struct { Key string `json:"key"` Data []byte `json:"data,omitempty"` @@ -163,11 +170,13 @@ func UserNameAndRoleValidator(user UserInfo) error { if len(user.RoleName) == 0 { return ErrNoRoleName } - roles := []string{ReadOnlyRole, ReadWriteRole, SuperUserRole} - if !slices.Contains(roles, strings.ToLower(user.RoleName)) { - return ErrInvalidRoleName + roles := []RoleType{ReadOnlyRole, ReadWriteRole, SuperUserRole} + for _, role := range roles { + if role.EqualTo(user.RoleName) { + return nil + } } - return nil + return ErrInvalidRoleName } func getAndFormatNow() string { @@ -189,3 +198,108 @@ func opsTerminateOnErr(result OpsResult, metadata opsMetadata, err error) (OpsRe result[RespTypMeta] = metadata return result, nil } + +func SortRoleByWeight(r1, r2 RoleType) bool { + return int(r1.GetWeight()) > int(r2.GetWeight()) +} + +func String2RoleType(roleName string) RoleType { + if SuperUserRole.EqualTo(roleName) { + return SuperUserRole + } + if ReadWriteRole.EqualTo(roleName) { + return ReadWriteRole + } + if ReadOnlyRole.EqualTo(roleName) { + return ReadOnlyRole + } + if NoPrivileges.EqualTo(roleName) { + return NoPrivileges + } + return CustomizedRole +} + +func SentProbeEvent(ctx context.Context, opsResult OpsResult, log logger.Logger) { + log.Infof("send event: %v", opsResult) + event, err := createProbeEvent(opsResult) + if err != nil { + log.Infof("generate event failed: %v", err) + return + } + + config, err := rest.InClusterConfig() + if err != nil { + log.Infof("get k8s client config failed: %v", err) + return + } + + clientset, err := kubernetes.NewForConfig(config) + if err != nil { + log.Infof("k8s client create failed: %v", err) + return + } + namespace := os.Getenv("KB_NAMESPACE") + for i := 0; i < 3; i++ { + _, err = clientset.CoreV1().Events(namespace).Create(ctx, event, metav1.CreateOptions{}) + if err == nil { + break + } + log.Infof("send event failed: %v", err) + } +} + +func createProbeEvent(opsResult OpsResult) (*corev1.Event, error) { + eventTmpl := ` +apiVersion: v1 +kind: Event +metadata: + name: {{ .PodName }}.{{ .EventSeq }} + namespace: {{ .Namespace }} +involvedObject: + apiVersion: v1 + fieldPath: spec.containers{sqlchannel} + kind: Pod + name: {{ .PodName }} + namespace: {{ .Namespace }} +reason: RoleChanged +type: Normal +source: + component: sqlchannel +` + + // get pod object + podName := os.Getenv("KB_POD_NAME") + podUID := os.Getenv("KB_POD_UID") + nodeName := os.Getenv("KB_NODENAME") + namespace := os.Getenv("KB_NAMESPACE") + msg, _ := json.Marshal(opsResult) + seq := rand.String(16) + roleValue := map[string]string{ + "PodName": podName, + "Namespace": namespace, + "EventSeq": seq, + } + tmpl, err := template.New("event-tmpl").Parse(eventTmpl) + if err != nil { + return nil, err + } + buf := new(bytes.Buffer) + err = tmpl.Execute(buf, roleValue) + if err != nil { + return nil, err + } + + event := &corev1.Event{} + _, _, err = scheme.Codecs.UniversalDeserializer().Decode(buf.Bytes(), nil, event) + if err != nil { + return nil, err + } + event.Message = string(msg) + event.InvolvedObject.UID = types.UID(podUID) + event.Source.Host = nodeName + event.Reason = string(opsResult["operation"].(bindings.OperationKind)) + event.FirstTimestamp = metav1.Now() + event.LastTimestamp = metav1.Now() + + return event, nil +} diff --git a/cmd/probe/internal/component/kafka/auth.go b/cmd/probe/internal/component/kafka/auth.go new file mode 100644 index 000000000..31bd72c00 --- /dev/null +++ b/cmd/probe/internal/component/kafka/auth.go @@ -0,0 +1,93 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "fmt" + + "github.com/Shopify/sarama" +) + +func updatePasswordAuthInfo(config *sarama.Config, metadata *kafkaMetadata, saslUsername, saslPassword string) { + config.Net.SASL.Enable = true + config.Net.SASL.User = saslUsername + config.Net.SASL.Password = saslPassword + switch metadata.SaslMechanism { + case "SHA-256": + config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA256} } + config.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA256 + case "SHA-512": + config.Net.SASL.SCRAMClientGeneratorFunc = func() sarama.SCRAMClient { return &XDGSCRAMClient{HashGeneratorFcn: SHA512} } + config.Net.SASL.Mechanism = sarama.SASLTypeSCRAMSHA512 + default: + config.Net.SASL.Mechanism = sarama.SASLTypePlaintext + } +} + +func updateMTLSAuthInfo(config *sarama.Config, metadata *kafkaMetadata) error { + if metadata.TLSDisable { + return fmt.Errorf("kafka: cannot configure mTLS authentication when TLSDisable is 'true'") + } + cert, err := tls.X509KeyPair([]byte(metadata.TLSClientCert), []byte(metadata.TLSClientKey)) + if err != nil { + return fmt.Errorf("unable to load client certificate and key pair. Err: %w", err) + } + config.Net.TLS.Config.Certificates = []tls.Certificate{cert} + return nil +} + +func updateTLSConfig(config *sarama.Config, metadata *kafkaMetadata) error { + if metadata.TLSDisable || metadata.AuthType == noAuthType { + config.Net.TLS.Enable = false + return nil + } + config.Net.TLS.Enable = true + + if !metadata.TLSSkipVerify && metadata.TLSCaCert == "" { + return nil + } + //nolint:gosec + config.Net.TLS.Config = &tls.Config{InsecureSkipVerify: metadata.TLSSkipVerify, MinVersion: tls.VersionTLS12} + if metadata.TLSCaCert != "" { + caCertPool := x509.NewCertPool() + if ok := caCertPool.AppendCertsFromPEM([]byte(metadata.TLSCaCert)); !ok { + return errors.New("kafka error: unable to load ca certificate") + } + config.Net.TLS.Config.RootCAs = caCertPool + } + + return nil +} + +func updateOidcAuthInfo(config *sarama.Config, metadata *kafkaMetadata) error { + tokenProvider := newOAuthTokenSource(metadata.OidcTokenEndpoint, metadata.OidcClientID, metadata.OidcClientSecret, metadata.OidcScopes) + + if metadata.TLSCaCert != "" { + err := tokenProvider.addCa(metadata.TLSCaCert) + if err != nil { + return fmt.Errorf("kafka: error setting oauth client trusted CA: %w", err) + } + } + + tokenProvider.skipCaVerify = metadata.TLSSkipVerify + + config.Net.SASL.Enable = true + config.Net.SASL.Mechanism = sarama.SASLTypeOAuth + config.Net.SASL.TokenProvider = &tokenProvider + + return nil +} diff --git a/cmd/probe/internal/component/kafka/consumer.go b/cmd/probe/internal/component/kafka/consumer.go new file mode 100644 index 000000000..9164dff1a --- /dev/null +++ b/cmd/probe/internal/component/kafka/consumer.go @@ -0,0 +1,229 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "context" + "errors" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/Shopify/sarama" + "github.com/cenkalti/backoff/v4" + + "github.com/dapr/kit/retry" +) + +type consumer struct { + k *Kafka + ready chan bool + running chan struct{} + stopped atomic.Bool + once sync.Once +} + +func (consumer *consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error { + b := consumer.k.backOffConfig.NewBackOffWithContext(session.Context()) + + for { + select { + case message, ok := <-claim.Messages(): + if !ok { + return nil + } + + if consumer.k.consumeRetryEnabled { + if err := retry.NotifyRecover(func() error { + return consumer.doCallback(session, message) + }, b, func(err error, d time.Duration) { + consumer.k.logger.Warnf("Error processing Kafka message: %s/%d/%d [key=%s]. Error: %v. Retrying...", message.Topic, message.Partition, message.Offset, asBase64String(message.Key), err) + }, func() { + consumer.k.logger.Infof("Successfully processed Kafka message after it previously failed: %s/%d/%d [key=%s]", message.Topic, message.Partition, message.Offset, asBase64String(message.Key)) + }); err != nil { + consumer.k.logger.Errorf("Too many failed attempts at processing Kafka message: %s/%d/%d [key=%s]. Error: %v.", message.Topic, message.Partition, message.Offset, asBase64String(message.Key), err) + } + } else { + err := consumer.doCallback(session, message) + if err != nil { + consumer.k.logger.Errorf("Error processing Kafka message: %s/%d/%d [key=%s]. Error: %v.", message.Topic, message.Partition, message.Offset, asBase64String(message.Key), err) + } + } + // Should return when `session.Context()` is done. + // If not, will raise `ErrRebalanceInProgress` or `read tcp :: i/o timeout` when kafka rebalance. see: + // https://github.com/Shopify/sarama/issues/1192 + case <-session.Context().Done(): + return nil + } + } +} + +func (consumer *consumer) doCallback(session sarama.ConsumerGroupSession, message *sarama.ConsumerMessage) error { + consumer.k.logger.Debugf("Processing Kafka message: %s/%d/%d [key=%s]", message.Topic, message.Partition, message.Offset, asBase64String(message.Key)) + handlerConfig, err := consumer.k.GetTopicHandlerConfig(message.Topic) + if err != nil { + return err + } + if !handlerConfig.IsBulkSubscribe && handlerConfig.Handler == nil { + return errors.New("invalid handler config for subscribe call") + } + event := NewEvent{ + Topic: message.Topic, + Data: message.Value, + } + // This is true only when headers are set (Kafka > 0.11) + if len(message.Headers) > 0 { + event.Metadata = make(map[string]string, len(message.Headers)) + for _, header := range message.Headers { + event.Metadata[string(header.Key)] = string(header.Value) + } + } + err = handlerConfig.Handler(session.Context(), &event) + if err == nil { + session.MarkMessage(message, "") + } + return err +} + +func (consumer *consumer) Cleanup(sarama.ConsumerGroupSession) error { + return nil +} + +func (consumer *consumer) Setup(sarama.ConsumerGroupSession) error { + consumer.once.Do(func() { + close(consumer.ready) + }) + + return nil +} + +// AddTopicHandler adds a handler and configuration for a topic +func (k *Kafka) AddTopicHandler(topic string, handlerConfig SubscriptionHandlerConfig) { + k.subscribeLock.Lock() + k.subscribeTopics[topic] = handlerConfig + k.subscribeLock.Unlock() +} + +// RemoveTopicHandler removes a topic handler +func (k *Kafka) RemoveTopicHandler(topic string) { + k.subscribeLock.Lock() + delete(k.subscribeTopics, topic) + k.subscribeLock.Unlock() +} + +// GetTopicHandlerConfig returns the handlerConfig for a topic +func (k *Kafka) GetTopicHandlerConfig(topic string) (SubscriptionHandlerConfig, error) { + handlerConfig, ok := k.subscribeTopics[topic] + if ok && (!handlerConfig.IsBulkSubscribe && handlerConfig.Handler != nil) { + return handlerConfig, nil + } + return SubscriptionHandlerConfig{}, + fmt.Errorf("any handler for messages of topic %s not found", topic) +} + +// Subscribe to topic in the Kafka cluster, in a background goroutine +func (k *Kafka) Subscribe(ctx context.Context) error { + if k.consumerGroup == "" { + return errors.New("kafka: consumerGroup must be set to subscribe") + } + + k.subscribeLock.Lock() + defer k.subscribeLock.Unlock() + + // Close resources and reset synchronization primitives + k.closeSubscriptionResources() + + topics := k.subscribeTopics.TopicList() + if len(topics) == 0 { + // Nothing to subscribe to + return nil + } + + cg, err := sarama.NewConsumerGroup(k.brokers, k.consumerGroup, k.config) + if err != nil { + return err + } + + k.cg = cg + + ready := make(chan bool) + k.consumer = consumer{ + k: k, + ready: ready, + running: make(chan struct{}), + } + + go func() { + k.logger.Debugf("Subscribed and listening to topics: %s", topics) + + for { + // If the context was cancelled, as is the case when handling SIGINT and SIGTERM below, then this pops + // us out of the consume loop + if ctx.Err() != nil { + break + } + + k.logger.Debugf("Starting loop to consume.") + + // Consume the requested topics + bo := backoff.WithContext(backoff.NewConstantBackOff(k.consumeRetryInterval), ctx) + innerErr := retry.NotifyRecover(func() error { + if ctxErr := ctx.Err(); ctxErr != nil { + return backoff.Permanent(ctxErr) + } + return k.cg.Consume(ctx, topics, &(k.consumer)) + }, bo, func(err error, t time.Duration) { + k.logger.Errorf("Error consuming %v. Retrying...: %v", topics, err) + }, func() { + k.logger.Infof("Recovered consuming %v", topics) + }) + if innerErr != nil && !errors.Is(innerErr, context.Canceled) { + k.logger.Errorf("Permanent error consuming %v: %v", topics, innerErr) + } + } + + k.logger.Debugf("Closing ConsumerGroup for topics: %v", topics) + err := k.cg.Close() + if err != nil { + k.logger.Errorf("Error closing consumer group: %v", err) + } + + // Ensure running channel is only closed once. + if k.consumer.stopped.CompareAndSwap(false, true) { + close(k.consumer.running) + } + }() + + <-ready + + return nil +} + +// Close down consumer group resources, refresh once. +func (k *Kafka) closeSubscriptionResources() { + if k.cg != nil { + err := k.cg.Close() + if err != nil { + k.logger.Errorf("Error closing consumer group: %v", err) + } + + k.consumer.once.Do(func() { + // Wait for shutdown to be complete + <-k.consumer.running + close(k.consumer.ready) + k.consumer.once = sync.Once{} + }) + } +} diff --git a/cmd/probe/internal/component/kafka/kafka.go b/cmd/probe/internal/component/kafka/kafka.go new file mode 100644 index 000000000..659982b5f --- /dev/null +++ b/cmd/probe/internal/component/kafka/kafka.go @@ -0,0 +1,230 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "context" + "sync" + "time" + + "github.com/Shopify/sarama" + + "github.com/dapr/kit/logger" + "github.com/dapr/kit/retry" +) + +// Kafka allows reading/writing to a Kafka consumer group. +type Kafka struct { + Producer sarama.SyncProducer + broker *sarama.Broker + consumerGroup string + brokers []string + logger logger.Logger + authType string + saslUsername string + saslPassword string + initialOffset int64 + cg sarama.ConsumerGroup + consumer consumer + config *sarama.Config + subscribeTopics TopicHandlerConfig + subscribeLock sync.Mutex + + backOffConfig retry.Config + + // The default value should be true for kafka pubsub component and false for kafka binding component + // This default value can be overridden by metadata consumeRetryEnabled + DefaultConsumeRetryEnabled bool + consumeRetryEnabled bool + consumeRetryInterval time.Duration +} + +func NewKafka(logger logger.Logger) *Kafka { + return &Kafka{ + logger: logger, + subscribeTopics: make(TopicHandlerConfig), + subscribeLock: sync.Mutex{}, + } +} + +// Init does metadata parsing and connection establishment. +func (k *Kafka) Init(_ context.Context, metadata map[string]string) error { + upgradedMetadata, err := k.upgradeMetadata(metadata) + if err != nil { + return err + } + + meta, err := k.getKafkaMetadata(upgradedMetadata) + if err != nil { + return err + } + + k.brokers = meta.Brokers + k.consumerGroup = meta.ConsumerGroup + k.initialOffset = meta.InitialOffset + k.authType = meta.AuthType + + k.broker = sarama.NewBroker(k.brokers[0]) + + config := sarama.NewConfig() + config.Version = meta.Version + config.Consumer.Offsets.Initial = k.initialOffset + + if meta.ClientID != "" { + config.ClientID = meta.ClientID + } + + err = updateTLSConfig(config, meta) + if err != nil { + return err + } + + switch k.authType { + case oidcAuthType: + k.logger.Info("Configuring SASL OAuth2/OIDC authentication") + err = updateOidcAuthInfo(config, meta) + if err != nil { + return err + } + case passwordAuthType: + k.logger.Info("Configuring SASL Password authentication") + k.saslUsername = meta.SaslUsername + k.saslPassword = meta.SaslPassword + updatePasswordAuthInfo(config, meta, k.saslUsername, k.saslPassword) + case mtlsAuthType: + k.logger.Info("Configuring mTLS authentcation") + err = updateMTLSAuthInfo(config, meta) + if err != nil { + return err + } + } + + k.config = config + sarama.Logger = SaramaLogBridge{daprLogger: k.logger} + + k.Producer, err = getSyncProducer(*k.config, k.brokers, meta.MaxMessageBytes) + if err != nil { + return err + } + + // Default retry configuration is used if no + // backOff properties are set. + if err := retry.DecodeConfigWithPrefix( + &k.backOffConfig, + metadata, + "backOff"); err != nil { + return err + } + k.consumeRetryEnabled = meta.ConsumeRetryEnabled + k.consumeRetryInterval = meta.ConsumeRetryInterval + + k.logger.Debug("Kafka message bus initialization complete") + + return nil +} + +func (k *Kafka) Close() (err error) { + k.closeSubscriptionResources() + + if k.Producer != nil { + err = k.Producer.Close() + k.Producer = nil + } + + return err +} + +// EventHandler is the handler used to handle the subscribed event. +type EventHandler func(ctx context.Context, msg *NewEvent) error + +// BulkEventHandler is the handler used to handle the subscribed bulk event. +// type BulkEventHandler func(ctx context.Context, msg *KafkaBulkMessage) ([]pubsub.BulkSubscribeResponseEntry, error) + +// SubscriptionHandlerConfig is the handler and configuration for subscription. +type SubscriptionHandlerConfig struct { + IsBulkSubscribe bool + Handler EventHandler +} + +// NewEvent is an event arriving from a message bus instance. +type NewEvent struct { + Data []byte `json:"data"` + Topic string `json:"topic"` + Metadata map[string]string `json:"metadata"` + ContentType *string `json:"contentType,omitempty"` +} + +// KafkaBulkMessage is a bulk event arriving from a message bus instance. +type KafkaBulkMessage struct { + Entries []KafkaBulkMessageEntry `json:"entries"` + Topic string `json:"topic"` + Metadata map[string]string `json:"metadata"` +} + +// KafkaBulkMessageEntry is an item contained inside bulk event arriving from a message bus instance. +type KafkaBulkMessageEntry struct { + EntryID string `json:"entryId"` //nolint:stylecheck + Event []byte `json:"event"` + ContentType string `json:"contentType,omitempty"` + Metadata map[string]string `json:"metadata"` +} + +func (k *Kafka) BrokerOpen() error { + connected, err := k.broker.Connected() + if err != nil { + k.logger.Info("broker connected err:%v", err) + return err + } + if !connected { + err = k.broker.Open(k.config) + if err != nil { + k.logger.Info("broker connected err:%v", err) + return err + } + } + + return nil +} + +func (k *Kafka) BrokerClose() { + _ = k.broker.Close() +} + +func (k *Kafka) BrokerCreateTopics(topic string) error { + req := &sarama.CreateTopicsRequest{ + Version: 1, + TopicDetails: map[string]*sarama.TopicDetail{ + topic: { + NumPartitions: -1, + ReplicationFactor: -1, + }, + }, + Timeout: time.Second, + ValidateOnly: false, + } + + resp, err := k.broker.CreateTopics(req) + if err != nil { + k.logger.Infof("CheckStatus error: %v", err) + return err + } else { + respErr := resp.TopicErrors[topic] + // ErrNo details: https://cwiki.apache.org/confluence/display/KAFKA/A+Guide+To+The+Kafka+Protocol#AGuideToTheKafkaProtocol-ErrorCodes + if respErr.Err != 0 { + k.logger.Infof("CheckStatus error, errMsg: %s errNo: %d", respErr.Error(), int16(respErr.Err)) + return respErr + } + return nil + } +} diff --git a/cmd/probe/internal/component/kafka/metadata.go b/cmd/probe/internal/component/kafka/metadata.go new file mode 100644 index 000000000..9e8785222 --- /dev/null +++ b/cmd/probe/internal/component/kafka/metadata.go @@ -0,0 +1,283 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/Shopify/sarama" +) + +const ( + key = "partitionKey" + skipVerify = "skipVerify" + caCert = "caCert" + clientCert = "clientCert" + clientKey = "clientKey" + consumeRetryEnabled = "consumeRetryEnabled" + consumeRetryInterval = "consumeRetryInterval" + authType = "authType" + passwordAuthType = "password" + oidcAuthType = "oidc" + mtlsAuthType = "mtls" + noAuthType = "none" +) + +type kafkaMetadata struct { + Brokers []string + ConsumerGroup string + ClientID string + AuthType string + SaslUsername string + SaslPassword string + SaslMechanism string + InitialOffset int64 + MaxMessageBytes int + OidcTokenEndpoint string + OidcClientID string + OidcClientSecret string + OidcScopes []string + TLSDisable bool + TLSSkipVerify bool + TLSCaCert string + TLSClientCert string + TLSClientKey string + ConsumeRetryEnabled bool + ConsumeRetryInterval time.Duration + Version sarama.KafkaVersion +} + +// upgradeMetadata updates metadata properties based on deprecated usage. +func (k *Kafka) upgradeMetadata(metadata map[string]string) (map[string]string, error) { + authTypeVal, authTypePres := metadata[authType] + authReqVal, authReqPres := metadata["authRequired"] + saslPassVal, saslPassPres := metadata["saslPassword"] + + // If authType is not set, derive it from authRequired. + if (!authTypePres || authTypeVal == "") && authReqPres && authReqVal != "" { + k.logger.Warn("AuthRequired is deprecated, use AuthType instead.") + validAuthRequired, err := strconv.ParseBool(authReqVal) + if err == nil { + if validAuthRequired { + // If legacy authRequired was used, either SASL username or mtls is the method. + if saslPassPres && saslPassVal != "" { + // User has specified saslPassword, so intend for password auth. + metadata[authType] = passwordAuthType + } else { + metadata[authType] = mtlsAuthType + } + } else { + metadata[authType] = noAuthType + } + } else { + return metadata, errors.New("kafka error: invalid value for 'authRequired' attribute") + } + } + + // if consumeRetryEnabled is not present, use component default value + consumeRetryEnabledVal, consumeRetryEnabledPres := metadata[consumeRetryEnabled] + if !consumeRetryEnabledPres || consumeRetryEnabledVal == "" { + metadata[consumeRetryEnabled] = strconv.FormatBool(k.DefaultConsumeRetryEnabled) + } + + return metadata, nil +} + +// getKafkaMetadata returns new Kafka metadata. +func (k *Kafka) getKafkaMetadata(metadata map[string]string) (*kafkaMetadata, error) { + meta := kafkaMetadata{ + ConsumeRetryInterval: 100 * time.Millisecond, + } + // use the runtimeConfig.ID as the consumer group so that each dapr runtime creates its own consumergroup + if val, ok := metadata["consumerID"]; ok && val != "" { + meta.ConsumerGroup = val + k.logger.Debugf("Using %s as ConsumerGroup", meta.ConsumerGroup) + } + + if val, ok := metadata["consumerGroup"]; ok && val != "" { + meta.ConsumerGroup = val + k.logger.Debugf("Using %s as ConsumerGroup", meta.ConsumerGroup) + } + + if val, ok := metadata["clientID"]; ok && val != "" { + meta.ClientID = val + k.logger.Debugf("Using %s as ClientID", meta.ClientID) + } + + if val, ok := metadata["saslMechanism"]; ok && val != "" { + meta.SaslMechanism = val + k.logger.Debugf("Using %s as saslMechanism", meta.SaslMechanism) + } + + initialOffset, err := parseInitialOffset(metadata["initialOffset"]) + if err != nil { + return nil, err + } + meta.InitialOffset = initialOffset + + if val, ok := metadata["brokers"]; ok && val != "" { + meta.Brokers = strings.Split(val, ",") + } else { + return nil, errors.New("kafka error: missing 'brokers' attribute") + } + + k.logger.Debugf("Found brokers: %v", meta.Brokers) + + val, ok := metadata["authType"] + if !ok { + return nil, errors.New("kafka error: missing 'authType' attribute") + } + if val == "" { + return nil, errors.New("kafka error: 'authType' attribute was empty") + } + + switch strings.ToLower(val) { + case passwordAuthType: + meta.AuthType = val + if val, ok = metadata["saslUsername"]; ok && val != "" { + meta.SaslUsername = val + } else { + return nil, errors.New("kafka error: missing SASL Username for authType 'password'") + } + + if val, ok = metadata["saslPassword"]; ok && val != "" { + meta.SaslPassword = val + } else { + return nil, errors.New("kafka error: missing SASL Password for authType 'password'") + } + k.logger.Debug("Configuring SASL password authentication.") + case oidcAuthType: + meta.AuthType = val + if val, ok = metadata["oidcTokenEndpoint"]; ok && val != "" { + meta.OidcTokenEndpoint = val + } else { + return nil, errors.New("kafka error: missing OIDC Token Endpoint for authType 'oidc'") + } + if val, ok = metadata["oidcClientID"]; ok && val != "" { + meta.OidcClientID = val + } else { + return nil, errors.New("kafka error: missing OIDC Client ID for authType 'oidc'") + } + if val, ok = metadata["oidcClientSecret"]; ok && val != "" { + meta.OidcClientSecret = val + } else { + return nil, errors.New("kafka error: missing OIDC Client Secret for authType 'oidc'") + } + if val, ok = metadata["oidcScopes"]; ok && val != "" { + meta.OidcScopes = strings.Split(val, ",") + } else { + k.logger.Warn("Warning: no OIDC scopes specified, using default 'openid' scope only. This is a security risk for token reuse.") + meta.OidcScopes = []string{"openid"} + } + k.logger.Debug("Configuring SASL token authentication via OIDC.") + case mtlsAuthType: + meta.AuthType = val + if val, ok = metadata[clientCert]; ok && val != "" { + if !isValidPEM(val) { + return nil, errors.New("kafka error: invalid client certificate") + } + meta.TLSClientCert = val + } + if val, ok = metadata[clientKey]; ok && val != "" { + if !isValidPEM(val) { + return nil, errors.New("kafka error: invalid client key") + } + meta.TLSClientKey = val + } + // clientKey and clientCert need to be all specified or all not specified. + if (meta.TLSClientKey == "") != (meta.TLSClientCert == "") { + return nil, errors.New("kafka error: clientKey or clientCert is missing") + } + k.logger.Debug("Configuring mTLS authentication.") + case noAuthType: + meta.AuthType = val + k.logger.Debug("No authentication configured.") + default: + return nil, errors.New("kafka error: invalid value for 'authType' attribute") + } + + if val, ok := metadata["maxMessageBytes"]; ok && val != "" { + maxBytes, err := strconv.Atoi(val) + if err != nil { + return nil, fmt.Errorf("kafka error: cannot parse maxMessageBytes: %w", err) + } + + meta.MaxMessageBytes = maxBytes + } + + if val, ok := metadata[caCert]; ok && val != "" { + if !isValidPEM(val) { + return nil, errors.New("kafka error: invalid ca certificate") + } + meta.TLSCaCert = val + } + + if val, ok := metadata["disableTls"]; ok && val != "" { + boolVal, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf("kafka: invalid value for 'tlsDisable' attribute: %w", err) + } + meta.TLSDisable = boolVal + if meta.TLSDisable { + k.logger.Info("kafka: TLS connectivity to broker disabled") + } + } + + if val, ok := metadata[skipVerify]; ok && val != "" { + boolVal, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf("kafka error: invalid value for '%s' attribute: %w", skipVerify, err) + } + meta.TLSSkipVerify = boolVal + if boolVal { + k.logger.Infof("kafka: you are using 'skipVerify' to skip server config verify which is unsafe!") + } + } + + if val, ok := metadata[consumeRetryEnabled]; ok && val != "" { + boolVal, err := strconv.ParseBool(val) + if err != nil { + return nil, fmt.Errorf("kafka error: invalid value for '%s' attribute: %w", consumeRetryEnabled, err) + } + meta.ConsumeRetryEnabled = boolVal + } + + if val, ok := metadata[consumeRetryInterval]; ok && val != "" { + durationVal, err := time.ParseDuration(val) + if err != nil { + intVal, err := strconv.ParseUint(val, 10, 32) + if err != nil { + return nil, fmt.Errorf("kafka error: invalid value for '%s' attribute: %w", consumeRetryInterval, err) + } + durationVal = time.Duration(intVal) * time.Millisecond + } + meta.ConsumeRetryInterval = durationVal + } + + if val, ok := metadata["version"]; ok && val != "" { + version, err := sarama.ParseKafkaVersion(val) + if err != nil { + return nil, errors.New("kafka error: invalid kafka version") + } + meta.Version = version + } else { + meta.Version = sarama.V2_0_0_0 //nolint:nosnakecase + } + + return &meta, nil +} diff --git a/cmd/probe/internal/component/kafka/metadata_test.go b/cmd/probe/internal/component/kafka/metadata_test.go new file mode 100644 index 000000000..8b8cd18a9 --- /dev/null +++ b/cmd/probe/internal/component/kafka/metadata_test.go @@ -0,0 +1,323 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "fmt" + "testing" + "time" + + "github.com/Shopify/sarama" + "github.com/stretchr/testify/require" + + "github.com/dapr/kit/logger" +) + +var ( + clientCertPemMock = `-----BEGIN CERTIFICATE----- +Y2xpZW50Q2VydA== +-----END CERTIFICATE-----` + clientKeyMock = `-----BEGIN RSA PRIVATE KEY----- +Y2xpZW50S2V5 +-----END RSA PRIVATE KEY-----` + caCertMock = `-----BEGIN CERTIFICATE----- +Y2FDZXJ0 +-----END CERTIFICATE-----` +) + +func getKafka() *Kafka { + return &Kafka{logger: logger.NewLogger("kafka_test")} +} + +func getBaseMetadata() map[string]string { + return map[string]string{"consumerGroup": "a", "clientID": "a", "brokers": "a", "disableTls": "true", "authType": mtlsAuthType, "maxMessageBytes": "2048", "initialOffset": "newest"} +} + +func getCompleteMetadata() map[string]string { + return map[string]string{ + "consumerGroup": "a", "clientID": "a", "brokers": "a", "authType": mtlsAuthType, "maxMessageBytes": "2048", + skipVerify: "true", clientCert: clientCertPemMock, clientKey: clientKeyMock, caCert: caCertMock, + "consumeRetryInterval": "200", "initialOffset": "newest", + } +} + +func TestParseMetadata(t *testing.T) { + k := getKafka() + t.Run("default kafka version", func(t *testing.T) { + m := getCompleteMetadata() + meta, err := k.getKafkaMetadata(m) + require.NoError(t, err) + require.NotNil(t, meta) + assertMetadata(t, meta) + require.Equal(t, sarama.V2_0_0_0, meta.Version) //nolint:nosnakecase + }) + + t.Run("specific kafka version", func(t *testing.T) { + m := getCompleteMetadata() + m["version"] = "0.10.2.0" + meta, err := k.getKafkaMetadata(m) + require.NoError(t, err) + require.NotNil(t, meta) + assertMetadata(t, meta) + require.Equal(t, sarama.V0_10_2_0, meta.Version) //nolint:nosnakecase + }) + + t.Run("invalid kafka version", func(t *testing.T) { + m := getCompleteMetadata() + m["version"] = "not_valid_version" + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, "kafka error: invalid kafka version", err.Error()) + }) +} + +func assertMetadata(t *testing.T, meta *kafkaMetadata) { + require.Equal(t, "a", meta.Brokers[0]) + require.Equal(t, "a", meta.ConsumerGroup) + require.Equal(t, "a", meta.ClientID) + require.Equal(t, 2048, meta.MaxMessageBytes) + require.Equal(t, true, meta.TLSSkipVerify) + require.Equal(t, clientCertPemMock, meta.TLSClientCert) + require.Equal(t, clientKeyMock, meta.TLSClientKey) + require.Equal(t, caCertMock, meta.TLSCaCert) + require.Equal(t, 200*time.Millisecond, meta.ConsumeRetryInterval) +} + +func TestMissingBrokers(t *testing.T) { + m := map[string]string{"initialOffset": "newest"} + k := getKafka() + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: missing 'brokers' attribute", err.Error()) +} + +func TestMissingAuthType(t *testing.T) { + m := map[string]string{"brokers": "kafka.com:9092", "initialOffset": "newest"} + k := getKafka() + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: missing 'authType' attribute", err.Error()) +} + +func TestMetadataUpgradeNoAuth(t *testing.T) { + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authRequired": "false"} + k := getKafka() + upgraded, err := k.upgradeMetadata(m) + require.Nil(t, err) + require.Equal(t, noAuthType, upgraded["authType"]) +} + +func TestMetadataUpgradePasswordAuth(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authRequired": "true", "saslPassword": "sassapass"} + upgraded, err := k.upgradeMetadata(m) + require.Nil(t, err) + require.Equal(t, passwordAuthType, upgraded["authType"]) +} + +func TestMetadataUpgradePasswordMTLSAuth(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authRequired": "true"} + upgraded, err := k.upgradeMetadata(m) + require.Nil(t, err) + require.Equal(t, mtlsAuthType, upgraded["authType"]) +} + +func TestMissingSaslValues(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authType": "password"} + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, fmt.Sprintf("kafka error: missing SASL Username for authType '%s'", passwordAuthType), err.Error()) + + m["saslUsername"] = "sassafras" + + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, fmt.Sprintf("kafka error: missing SASL Password for authType '%s'", passwordAuthType), err.Error()) +} + +func TestMissingSaslValuesOnUpgrade(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authRequired": "true", "saslPassword": "sassapass"} + upgraded, err := k.upgradeMetadata(m) + require.Nil(t, err) + meta, err := k.getKafkaMetadata(upgraded) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, fmt.Sprintf("kafka error: missing SASL Username for authType '%s'", passwordAuthType), err.Error()) +} + +func TestMissingOidcValues(t *testing.T) { + k := getKafka() + m := map[string]string{"brokers": "akfak.com:9092", "initialOffset": "newest", "authType": oidcAuthType} + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, fmt.Sprintf("kafka error: missing OIDC Token Endpoint for authType '%s'", oidcAuthType), err.Error()) + + m["oidcTokenEndpoint"] = "https://sassa.fra/" + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, fmt.Sprintf("kafka error: missing OIDC Client ID for authType '%s'", oidcAuthType), err.Error()) + + m["oidcClientID"] = "sassafras" + meta, err = k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + require.Equal(t, fmt.Sprintf("kafka error: missing OIDC Client Secret for authType '%s'", oidcAuthType), err.Error()) + + // Check if missing scopes causes the default 'openid' to be used. + m["oidcClientSecret"] = "sassapass" + meta, err = k.getKafkaMetadata(m) + require.Nil(t, err) + require.Contains(t, meta.OidcScopes, "openid") +} + +func TestPresentSaslValues(t *testing.T) { + k := getKafka() + m := map[string]string{ + "brokers": "akfak.com:9092", + "authType": passwordAuthType, + "saslUsername": "sassafras", + "saslPassword": "sassapass", + "initialOffset": "newest", + } + meta, err := k.getKafkaMetadata(m) + require.NoError(t, err) + require.NotNil(t, meta) + + require.Equal(t, "sassafras", meta.SaslUsername) + require.Equal(t, "sassapass", meta.SaslPassword) +} + +func TestPresentOidcValues(t *testing.T) { + k := getKafka() + m := map[string]string{ + "brokers": "akfak.com:9092", + "authType": oidcAuthType, + "oidcTokenEndpoint": "https://sassa.fras", + "oidcClientID": "sassafras", + "oidcClientSecret": "sassapass", + "oidcScopes": "akfak", + "initialOffset": "newest", + } + meta, err := k.getKafkaMetadata(m) + require.NoError(t, err) + require.NotNil(t, meta) + + require.Equal(t, "https://sassa.fras", meta.OidcTokenEndpoint) + require.Equal(t, "sassafras", meta.OidcClientID) + require.Equal(t, "sassapass", meta.OidcClientSecret) + require.Contains(t, meta.OidcScopes, "akfak") +} + +func TestInvalidAuthRequiredFlag(t *testing.T) { + m := map[string]string{"brokers": "akfak.com:9092", "authRequired": "maybe?????????????"} + k := getKafka() + _, err := k.upgradeMetadata(m) + require.Error(t, err) + + require.Equal(t, "kafka error: invalid value for 'authRequired' attribute", err.Error()) +} + +func TestInitialOffset(t *testing.T) { + m := map[string]string{"consumerGroup": "a", "brokers": "a", "authRequired": "false", "initialOffset": "oldest"} + k := getKafka() + upgraded, err := k.upgradeMetadata(m) + require.NoError(t, err) + meta, err := k.getKafkaMetadata(upgraded) + require.NoError(t, err) + require.Equal(t, sarama.OffsetOldest, meta.InitialOffset) + m["initialOffset"] = "newest" + meta, err = k.getKafkaMetadata(m) + require.NoError(t, err) + require.Equal(t, sarama.OffsetNewest, meta.InitialOffset) +} + +func TestTls(t *testing.T) { + k := getKafka() + + t.Run("disable tls", func(t *testing.T) { + m := getBaseMetadata() + meta, err := k.getKafkaMetadata(m) + require.NoError(t, err) + require.NotNil(t, meta) + c := &sarama.Config{} + err = updateTLSConfig(c, meta) + require.NoError(t, err) + require.Equal(t, false, c.Net.TLS.Enable) + }) + + t.Run("wrong client cert format", func(t *testing.T) { + m := getBaseMetadata() + m[clientCert] = "clientCert" + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: invalid client certificate", err.Error()) + }) + + t.Run("wrong client key format", func(t *testing.T) { + m := getBaseMetadata() + m[clientKey] = "clientKey" + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: invalid client key", err.Error()) + }) + + t.Run("miss client key", func(t *testing.T) { + m := getBaseMetadata() + m[clientCert] = clientCertPemMock + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: clientKey or clientCert is missing", err.Error()) + }) + + t.Run("miss client cert", func(t *testing.T) { + m := getBaseMetadata() + m[clientKey] = clientKeyMock + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: clientKey or clientCert is missing", err.Error()) + }) + + t.Run("wrong ca cert format", func(t *testing.T) { + m := getBaseMetadata() + m[caCert] = "caCert" + meta, err := k.getKafkaMetadata(m) + require.Error(t, err) + require.Nil(t, meta) + + require.Equal(t, "kafka error: invalid ca certificate", err.Error()) + }) +} diff --git a/cmd/probe/internal/component/kafka/producer.go b/cmd/probe/internal/component/kafka/producer.go new file mode 100644 index 000000000..780456eab --- /dev/null +++ b/cmd/probe/internal/component/kafka/producer.go @@ -0,0 +1,77 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "context" + "errors" + + "github.com/Shopify/sarama" +) + +func getSyncProducer(config sarama.Config, brokers []string, maxMessageBytes int) (sarama.SyncProducer, error) { + // Add SyncProducer specific properties to copy of base config + config.Producer.RequiredAcks = sarama.WaitForAll + config.Producer.Retry.Max = 5 + config.Producer.Return.Successes = true + + if maxMessageBytes > 0 { + config.Producer.MaxMessageBytes = maxMessageBytes + } + + producer, err := sarama.NewSyncProducer(brokers, &config) + if err != nil { + return nil, err + } + + return producer, nil +} + +// Publish message to Kafka cluster. +func (k *Kafka) Publish(_ context.Context, topic string, data []byte, metadata map[string]string) error { + if k.Producer == nil { + return errors.New("component is closed") + } + // k.logger.Debugf("Publishing topic %v with data: %v", topic, string(data)) + k.logger.Debugf("Publishing on topic %v", topic) + + msg := &sarama.ProducerMessage{ + Topic: topic, + Value: sarama.ByteEncoder(data), + } + + for name, value := range metadata { + if name == key { + msg.Key = sarama.StringEncoder(value) + } else { + if msg.Headers == nil { + msg.Headers = make([]sarama.RecordHeader, 0, len(metadata)) + } + msg.Headers = append(msg.Headers, sarama.RecordHeader{ + Key: []byte(name), + Value: []byte(value), + }) + } + } + + partition, offset, err := k.Producer.SendMessage(msg) + + k.logger.Debugf("Partition: %v, offset: %v", partition, offset) + + if err != nil { + return err + } + + return nil +} diff --git a/internal/controller/lifecycle/validator_enable_logs.go b/cmd/probe/internal/component/kafka/sarama_log_bridge.go similarity index 58% rename from internal/controller/lifecycle/validator_enable_logs.go rename to cmd/probe/internal/component/kafka/sarama_log_bridge.go index 52c59f50f..be98deab5 100644 --- a/internal/controller/lifecycle/validator_enable_logs.go +++ b/cmd/probe/internal/component/kafka/sarama_log_bridge.go @@ -1,12 +1,9 @@ /* -Copyright ApeCloud, Inc. - +Copyright 2021 The Dapr 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. @@ -14,18 +11,24 @@ See the License for the specific language governing permissions and limitations under the License. */ -package lifecycle +package kafka import ( - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/dapr/kit/logger" ) -type enableLogsValidator struct { - cluster *appsv1alpha1.Cluster - clusterDef *appsv1alpha1.ClusterDefinition +type SaramaLogBridge struct { + daprLogger logger.Logger +} + +func (b SaramaLogBridge) Print(v ...interface{}) { + b.daprLogger.Debug(v...) +} + +func (b SaramaLogBridge) Printf(format string, v ...interface{}) { + b.daprLogger.Debugf(format, v...) } -func (e *enableLogsValidator) Validate() error { - // validate config and send warning event log necessarily - return e.cluster.Spec.ValidateEnabledLogs(e.clusterDef) +func (b SaramaLogBridge) Println(v ...interface{}) { + b.daprLogger.Debug(v...) } diff --git a/cmd/probe/internal/component/kafka/sasl_oauthbearer.go b/cmd/probe/internal/component/kafka/sasl_oauthbearer.go new file mode 100644 index 000000000..df70e5df5 --- /dev/null +++ b/cmd/probe/internal/component/kafka/sasl_oauthbearer.go @@ -0,0 +1,128 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + ctx "context" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "net/http" + "time" + + "github.com/Shopify/sarama" + "golang.org/x/oauth2" + ccred "golang.org/x/oauth2/clientcredentials" +) + +type OAuthTokenSource struct { + CachedToken oauth2.Token + Extensions map[string]string + TokenEndpoint oauth2.Endpoint + ClientID string + ClientSecret string + Scopes []string + httpClient *http.Client + trustedCas []*x509.Certificate + skipCaVerify bool +} + +func newOAuthTokenSource(oidcTokenEndpoint, oidcClientID, oidcClientSecret string, oidcScopes []string) OAuthTokenSource { + return OAuthTokenSource{TokenEndpoint: oauth2.Endpoint{TokenURL: oidcTokenEndpoint}, ClientID: oidcClientID, ClientSecret: oidcClientSecret, Scopes: oidcScopes} +} + +var tokenRequestTimeout, _ = time.ParseDuration("30s") + +func (ts *OAuthTokenSource) addCa(caPem string) error { + pemBytes := []byte(caPem) + + block, _ := pem.Decode(pemBytes) + + if block == nil || block.Type != "CERTIFICATE" { + return fmt.Errorf("PEM data not valid or not of a valid type (CERTIFICATE)") + } + + caCert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("error parsing PEM certificate: %w", err) + } + + if ts.trustedCas == nil { + ts.trustedCas = make([]*x509.Certificate, 0) + } + ts.trustedCas = append(ts.trustedCas, caCert) + + return nil +} + +func (ts *OAuthTokenSource) configureClient() { + if ts.httpClient != nil { + return + } + + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: ts.skipCaVerify, //nolint:gosec + } + + if ts.trustedCas != nil { + caPool, err := x509.SystemCertPool() + if err != nil { + caPool = x509.NewCertPool() + } + + for _, c := range ts.trustedCas { + caPool.AddCert(c) + } + tlsConfig.RootCAs = caPool + } + + ts.httpClient = &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: tlsConfig, + }, + } +} + +func (ts *OAuthTokenSource) Token() (*sarama.AccessToken, error) { + if ts.CachedToken.Valid() { + return ts.asSaramaToken(), nil + } + + if ts.TokenEndpoint.TokenURL == "" || ts.ClientID == "" || ts.ClientSecret == "" { + return nil, fmt.Errorf("cannot generate token, OAuthTokenSource not fully configured") + } + + oidcCfg := ccred.Config{ClientID: ts.ClientID, ClientSecret: ts.ClientSecret, Scopes: ts.Scopes, TokenURL: ts.TokenEndpoint.TokenURL, AuthStyle: ts.TokenEndpoint.AuthStyle} + + timeoutCtx, cancel := ctx.WithTimeout(ctx.TODO(), tokenRequestTimeout) + defer cancel() + + ts.configureClient() + + timeoutCtx = ctx.WithValue(timeoutCtx, oauth2.HTTPClient, ts.httpClient) + + token, err := oidcCfg.Token(timeoutCtx) + if err != nil { + return nil, fmt.Errorf("error generating oauth2 token: %w", err) + } + + ts.CachedToken = *token + return ts.asSaramaToken(), nil +} + +func (ts *OAuthTokenSource) asSaramaToken() *sarama.AccessToken { + return &(sarama.AccessToken{Token: ts.CachedToken.AccessToken, Extensions: ts.Extensions}) +} diff --git a/cmd/probe/internal/component/kafka/scram_client.go b/cmd/probe/internal/component/kafka/scram_client.go new file mode 100644 index 000000000..49d0cdde1 --- /dev/null +++ b/cmd/probe/internal/component/kafka/scram_client.go @@ -0,0 +1,50 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "crypto/sha256" + "crypto/sha512" + + "github.com/xdg-go/scram" +) + +var ( + SHA256 scram.HashGeneratorFcn = sha256.New + SHA512 scram.HashGeneratorFcn = sha512.New +) + +type XDGSCRAMClient struct { + *scram.Client + *scram.ClientConversation + scram.HashGeneratorFcn +} + +func (x *XDGSCRAMClient) Begin(userName, password, authzID string) (err error) { + x.Client, err = x.HashGeneratorFcn.NewClient(userName, password, authzID) + if err != nil { + return err + } + x.ClientConversation = x.Client.NewConversation() + return nil +} + +func (x *XDGSCRAMClient) Step(challenge string) (response string, err error) { + response, err = x.ClientConversation.Step(challenge) + return +} + +func (x *XDGSCRAMClient) Done() bool { + return x.ClientConversation.Done() +} diff --git a/cmd/probe/internal/component/kafka/utils.go b/cmd/probe/internal/component/kafka/utils.go new file mode 100644 index 000000000..8c014487e --- /dev/null +++ b/cmd/probe/internal/component/kafka/utils.go @@ -0,0 +1,75 @@ +/* +Copyright 2021 The Dapr 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 kafka + +import ( + "encoding/base64" + "encoding/pem" + "fmt" + "strings" + + "github.com/Shopify/sarama" +) + +const ( + // DefaultMaxBulkSubCount is the default max bulk count for kafka pubsub component + // if the MaxBulkCountKey is not set in the metadata. + DefaultMaxBulkSubCount = 80 + // DefaultMaxBulkSubAwaitDurationMs is the default max bulk await duration for kafka pubsub component + // if the MaxBulkAwaitDurationKey is not set in the metadata. + DefaultMaxBulkSubAwaitDurationMs = 10000 +) + +// asBase64String implements the `fmt.Stringer` interface in order to print +// `[]byte` as a base 64 encoded string. +// It is used above to log the message key. The call to `EncodeToString` +// only occurs for logs that are written based on the logging level. +type asBase64String []byte + +func (s asBase64String) String() string { + return base64.StdEncoding.EncodeToString(s) +} + +func parseInitialOffset(value string) (initialOffset int64, err error) { + switch strings.ToLower(value) { + case "oldest": + initialOffset = sarama.OffsetOldest + case "newest": + initialOffset = sarama.OffsetNewest + default: + return 0, fmt.Errorf("kafka error: invalid initialOffset: %s", value) + } + return initialOffset, err +} + +// isValidPEM validates the provided input has PEM formatted block. +func isValidPEM(val string) bool { + block, _ := pem.Decode([]byte(val)) + + return block != nil +} + +// TopicHandlerConfig is the map of topics and sruct containing handler and their config. +type TopicHandlerConfig map[string]SubscriptionHandlerConfig + +// TopicList returns the list of topics +func (tbh TopicHandlerConfig) TopicList() []string { + topics := make([]string, len(tbh)) + i := 0 + for topic := range tbh { + topics[i] = topic + i++ + } + return topics +} diff --git a/cmd/probe/internal/component/redis/metadata.go b/cmd/probe/internal/component/redis/metadata.go index 0d8d8237a..5c4fd02a8 100644 --- a/cmd/probe/internal/component/redis/metadata.go +++ b/cmd/probe/internal/component/redis/metadata.go @@ -1,12 +1,9 @@ /* -Copyright ApeCloud, Inc. - +Copyright 2021 The Dapr 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. diff --git a/cmd/probe/internal/component/redis/redis.go b/cmd/probe/internal/component/redis/redis.go index 4c69272fe..946dfa58c 100644 --- a/cmd/probe/internal/component/redis/redis.go +++ b/cmd/probe/internal/component/redis/redis.go @@ -1,12 +1,9 @@ /* -Copyright ApeCloud, Inc. - +Copyright 2021 The Dapr 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. diff --git a/cmd/probe/internal/component/redis/redis_test.go b/cmd/probe/internal/component/redis/redis_test.go index 02a1429f3..ee0a78cda 100644 --- a/cmd/probe/internal/component/redis/redis_test.go +++ b/cmd/probe/internal/component/redis/redis_test.go @@ -1,12 +1,9 @@ /* -Copyright ApeCloud, Inc. - +Copyright 2021 The Dapr 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. diff --git a/cmd/probe/internal/component/redis/settings.go b/cmd/probe/internal/component/redis/settings.go index 355c9132b..27ff63406 100644 --- a/cmd/probe/internal/component/redis/settings.go +++ b/cmd/probe/internal/component/redis/settings.go @@ -1,12 +1,9 @@ /* -Copyright ApeCloud, Inc. - +Copyright 2021 The Dapr 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. diff --git a/cmd/probe/internal/middleware/http/probe/checks_middleware.go b/cmd/probe/internal/middleware/http/probe/checks_middleware.go index 940b913ae..1a1abb4d1 100644 --- a/cmd/probe/internal/middleware/http/probe/checks_middleware.go +++ b/cmd/probe/internal/middleware/http/probe/checks_middleware.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package probe @@ -27,14 +30,14 @@ import ( "github.com/dapr/kit/logger" "github.com/valyala/fasthttp" - . "github.com/apecloud/kubeblocks/cmd/probe/util" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) const ( bindingPath = "/v1.0/bindings" // the key is used to bypass the dapr framework and set http status code. - // "status-code" is the key defined by probe, but this will changed like this + // "status-code" is the key defined by probe, but this will be changed // by dapr framework and http framework in the end. statusCodeHeader = "Metadata.status-Code" bodyFmt = `{"operation": "%s", "metadata": {"sql" : ""}}` @@ -45,7 +48,7 @@ func NewProbeMiddleware(log logger.Logger) middleware.Middleware { return &Middleware{logger: log} } -// Middleware is an probe middleware. +// Middleware is a probe middleware. type Middleware struct { logger logger.Logger } @@ -96,10 +99,13 @@ func (m *Middleware) GetHandler(metadata middleware.Metadata) (func(next fasthtt code := ctx.Response.Header.Peek(statusCodeHeader) statusCode, err := strconv.Atoi(string(code)) if err == nil { + // header has a statusCodeHeader ctx.Response.Header.SetStatusCode(statusCode) m.logger.Infof("response abnormal: %v", ctx.Response.String()) + } else { + // header has no statusCodeHeader + m.logger.Infof("response: %v", ctx.Response.String()) } - m.logger.Infof("response: %v", ctx.Response.String()) } }, nil } diff --git a/cmd/probe/internal/middleware/http/probe/checks_middleware_test.go b/cmd/probe/internal/middleware/http/probe/checks_middleware_test.go index 123c3f7a4..56fb87c5f 100644 --- a/cmd/probe/internal/middleware/http/probe/checks_middleware_test.go +++ b/cmd/probe/internal/middleware/http/probe/checks_middleware_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package probe @@ -29,7 +32,7 @@ import ( const checkFailedHTTPCode = "451" -// mockedRequestHandler acts like an upstream service returns success status code 200 and a fixed response body. +// mockedRequestHandler acts like an upstream service, returns success status code 200 and a fixed response body. func mockedRequestHandler(ctx *fasthttp.RequestCtx) { ctx.Response.Header.SetStatusCode(http.StatusOK) ctx.Response.SetBodyString("mock response") diff --git a/cmd/probe/main.go b/cmd/probe/main.go index 577001c07..dc456b64e 100644 --- a/cmd/probe/main.go +++ b/cmd/probe/main.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main @@ -46,7 +49,9 @@ import ( "go.uber.org/automaxprocs/maxprocs" + "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/custom" "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/etcd" + "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/kafka" "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/mongodb" "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/mysql" "github.com/apecloud/kubeblocks/cmd/probe/internal/binding/postgres" @@ -66,8 +71,10 @@ func init() { bindingsLoader.DefaultRegistry.RegisterOutputBinding(mongodb.NewMongoDB, "mongodb") bindingsLoader.DefaultRegistry.RegisterOutputBinding(redis.NewRedis, "redis") bindingsLoader.DefaultRegistry.RegisterOutputBinding(postgres.NewPostgres, "postgres") + bindingsLoader.DefaultRegistry.RegisterOutputBinding(custom.NewHTTPCustom, "custom") bindingsLoader.DefaultRegistry.RegisterOutputBinding(dhttp.NewHTTP, "http") bindingsLoader.DefaultRegistry.RegisterOutputBinding(localstorage.NewLocalStorage, "localstorage") + bindingsLoader.DefaultRegistry.RegisterOutputBinding(kafka.NewKafka, "kafka") nrLoader.DefaultRegistry.RegisterComponent(mdns.NewResolver, "mdns") httpMiddlewareLoader.DefaultRegistry.RegisterComponent(func(log logger.Logger) httpMiddlewareLoader.FactoryMethod { return func(metadata middleware.Metadata) (httpMiddleware.Middleware, error) { diff --git a/cmd/probe/probe.proto b/cmd/probe/probe.proto index e26fa2c6f..b272e85a0 100644 --- a/cmd/probe/probe.proto +++ b/cmd/probe/probe.proto @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ syntax = "proto3"; diff --git a/cmd/probe/util/const.go b/cmd/probe/util/const.go deleted file mode 100644 index b4629dae1..000000000 --- a/cmd/probe/util/const.go +++ /dev/null @@ -1,44 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 util - -import "github.com/dapr/components-contrib/bindings" - -const ( - CheckRunningOperation bindings.OperationKind = "checkRunning" - CheckStatusOperation bindings.OperationKind = "checkStatus" - CheckRoleOperation bindings.OperationKind = "checkRole" - GetRoleOperation bindings.OperationKind = "getRole" - GetLagOperation bindings.OperationKind = "getLag" - ExecOperation bindings.OperationKind = "exec" - QueryOperation bindings.OperationKind = "query" - CloseOperation bindings.OperationKind = "close" - - // actions for cluster accounts management - ListUsersOp bindings.OperationKind = "listUsers" - CreateUserOp bindings.OperationKind = "createUser" - DeleteUserOp bindings.OperationKind = "deleteUser" - DescribeUserOp bindings.OperationKind = "describeUser" - GrantUserRoleOp bindings.OperationKind = "grantUserRole" - RevokeUserRoleOp bindings.OperationKind = "revokeUserRole" - // actions for cluster roles management - - OperationNotImplemented = "NotImplemented" - OperationInvalid = "Invalid" - OperationSuccess = "Success" - OperationFailed = "Failed" -) diff --git a/cmd/reloader/README.md b/cmd/reloader/README.md index 1ce1b4e52..3b7c1214f 100644 --- a/cmd/reloader/README.md +++ b/cmd/reloader/README.md @@ -15,7 +15,7 @@ You can get started with Reloader, by any of the following methods: ## 2.1 Build -Compiler `Go 1.19+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. +Compiler `Go 1.20+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. Use `make reloader` to build and produce the `reloader` binary file. The executable is produced under current directory. diff --git a/cmd/reloader/app/cmd.go b/cmd/reloader/app/cmd.go index 6947a6a2a..d02200d14 100644 --- a/cmd/reloader/app/cmd.go +++ b/cmd/reloader/app/cmd.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package app @@ -39,7 +42,7 @@ import ( var logger *zap.SugaredLogger -// NewConfigManagerCommand This command is used to reload configuration +// NewConfigManagerCommand is used to reload configuration func NewConfigManagerCommand(ctx context.Context, name string) *cobra.Command { opt := NewVolumeWatcherOpts() cmd := &cobra.Command{ @@ -156,7 +159,7 @@ func logUnaryServerInterceptor(ctx context.Context, req interface{}, info *grpc. func checkOptions(opt *VolumeWatcherOpts) error { if len(opt.VolumeDirs) == 0 && opt.NotifyHandType != TPLScript { - return cfgutil.MakeError("require volume directory is null.") + return cfgutil.MakeError("required volume directory is null.") } if opt.NotifyHandType == TPLScript { @@ -164,11 +167,11 @@ func checkOptions(opt *VolumeWatcherOpts) error { } if opt.NotifyHandType == ShellTool && opt.Command == "" { - return cfgutil.MakeError("require command is null.") + return cfgutil.MakeError("required command is null.") } if len(opt.ProcessName) == 0 { - return cfgutil.MakeError("require process name is null.") + return cfgutil.MakeError("required process name is null.") } return nil } @@ -183,7 +186,7 @@ type TplScriptConfig struct { func checkTPLScriptOptions(opt *VolumeWatcherOpts) error { if opt.TPLConfig == "" { - return cfgutil.MakeError("require tpl config is not null") + return cfgutil.MakeError("required tpl config is null") } if _, err := os.Stat(opt.TPLConfig); err != nil { @@ -219,7 +222,7 @@ func initLog(level string) *zap.Logger { } if _, ok := levelStrings[level]; !ok { - fmt.Printf("not support log level[%s], set default info", level) + fmt.Printf("not supported log level[%s], set default info", level) level = "info" } @@ -245,8 +248,8 @@ func createHandlerWithVolumeWatch(opt *VolumeWatcherOpts) (cfgcore.WatchEventHan case TPLScript: return cfgcore.CreateTPLScriptHandler(opt.TPLScriptPath, opt.VolumeDirs, opt.FileRegex, opt.BackupPath, opt.FormatterConfig, opt.DataType, opt.DSN) case SQL, WebHook: - return nil, cfgutil.MakeError("event type[%s]: not yet, but in the future", opt.NotifyHandType.String()) + return nil, cfgutil.MakeError("event type[%s]: not supported", opt.NotifyHandType.String()) default: - return nil, cfgutil.MakeError("not support event type.") + return nil, cfgutil.MakeError("not supported event type.") } } diff --git a/cmd/reloader/app/flags.go b/cmd/reloader/app/flags.go index 4217c11f0..7b1fd9182 100644 --- a/cmd/reloader/app/flags.go +++ b/cmd/reloader/app/flags.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package app @@ -68,7 +71,7 @@ func (f *NotifyEventType) Set(val string) error { return nil } } - return cfgcore.MakeError("not support type[%s], require list: [%v]", val, allNotifyType) + return cfgcore.MakeError("not supported type[%s], required list: [%v]", val, allNotifyType) } func (f *NotifyEventType) String() string { @@ -83,9 +86,9 @@ type ReconfigureServiceOptions struct { GrpcPort int PodIP string - // EnableRemoteOnlineUpdate enable remote online update + // EnableRemoteOnlineUpdate enables remote online update RemoteOnlineUpdateEnable bool - // EnableContainerRuntime enable container runtime + // EnableContainerRuntime enables container runtime ContainerRuntimeEnable bool DebugMode bool @@ -96,7 +99,7 @@ type ReconfigureServiceOptions struct { type VolumeWatcherOpts struct { VolumeDirs []string - // fileRegex watch file regex + // fileRegex watches file regex FileRegex string // ProcessName: program name @@ -144,75 +147,75 @@ func InstallFlags(flags *pflag.FlagSet, opt *VolumeWatcherOpts) { flags.StringArrayVar(&opt.VolumeDirs, "volume-dir", opt.VolumeDirs, - "the config map volume directory to watch for updates; may be used multiple times.") + "the config map volume directory to be watched for updates; may be used multiple times.") flags.Var(&opt.NotifyHandType, "notify-type", - "the config describe how to process notification messages.", + "the config describes how to process notification messages.", ) // for signal handle flags.StringVar(&opt.ProcessName, "process", opt.ProcessName, - "the config describe what is db program.") + "the config describes what db program is.") flags.StringVar((*string)(&opt.Signal), "signal", string(opt.Signal), - "the config describe reload unix signal.") + "the config describes the reload unix signal.") // for exec handle flags.StringVar(&opt.Command, "command", opt.Command, - "the config describe reload command. ") + "the config describes reload command. ") // for exec tpl scripts flags.StringVar(&opt.TPLConfig, "tpl-config", opt.TPLConfig, - "the config describe reload by tpl script.") + "the config describes reload behaviors by tpl script.") flags.StringVar(&opt.BackupPath, "backup-path", opt.BackupPath, - "the config describe.") + "the config describes backup path.") flags.StringVar(&opt.LogLevel, "log-level", opt.LogLevel, - "the config set log level. enum: [error, info, debug]") + "the config sets log level. enum: [error, info, debug]") flags.StringVar(&opt.FileRegex, "regex", opt.FileRegex, - "the config set filter config file.") + "the config sets filter config file.") flags.StringVar(&opt.ServiceOpt.PodIP, "pod-ip", opt.ServiceOpt.PodIP, - "the config set pod ip address.") + "the config sets pod ip address.") flags.IntVar(&opt.ServiceOpt.GrpcPort, "tcp", opt.ServiceOpt.GrpcPort, - "the config set service port.") + "the config sets service port.") flags.BoolVar(&opt.ServiceOpt.DebugMode, "debug", opt.ServiceOpt.DebugMode, - "the config set debug.") + "the config sets debug mode.") flags.StringVar((*string)(&opt.ServiceOpt.ContainerRuntime), "container-runtime", string(opt.ServiceOpt.ContainerRuntime), - "the config set cri runtime type.") + "the config sets cri runtime type.") flags.StringVar(&opt.ServiceOpt.RuntimeEndpoint, "runtime-endpoint", opt.ServiceOpt.RuntimeEndpoint, - "the config set cri runtime endpoint.") + "the config sets cri runtime endpoint.") flags.BoolVar(&opt.ServiceOpt.ContainerRuntimeEnable, "cri-enable", opt.ServiceOpt.ContainerRuntimeEnable, - "the config set enable cri.") + "the config sets enable cri.") flags.BoolVar(&opt.ServiceOpt.RemoteOnlineUpdateEnable, "operator-update-enable", opt.ServiceOpt.ContainerRuntimeEnable, - "the config set enable operator update parameter.") + "the config sets enable operator update parameter.") } diff --git a/cmd/reloader/app/proxy.go b/cmd/reloader/app/proxy.go index cb2d440a5..9ad296f8a 100644 --- a/cmd/reloader/app/proxy.go +++ b/cmd/reloader/app/proxy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package app @@ -72,11 +75,11 @@ func (r *reconfigureProxy) initContainerKiller() error { func (r *reconfigureProxy) StopContainer(ctx context.Context, request *cfgproto.StopContainerRequest) (*cfgproto.StopContainerResponse, error) { if r.killer == nil { - return nil, cfgcore.MakeError("container killer is not initialized.") + return nil, cfgcore.MakeError("container killing process is not initialized.") } ds := request.GetContainerIDs() if len(ds) == 0 { - return &cfgproto.StopContainerResponse{ErrMessage: "not any containerId."}, nil + return &cfgproto.StopContainerResponse{ErrMessage: "no match for any container with containerId."}, nil } if err := r.killer.Kill(ctx, ds, stopContainerSignal, nil); err != nil { return nil, err @@ -86,11 +89,11 @@ func (r *reconfigureProxy) StopContainer(ctx context.Context, request *cfgproto. func (r *reconfigureProxy) OnlineUpgradeParams(ctx context.Context, request *cfgproto.OnlineUpgradeParamsRequest) (*cfgproto.OnlineUpgradeParamsResponse, error) { if r.updater == nil { - return nil, cfgcore.MakeError("online updater is not initialized.") + return nil, cfgcore.MakeError("online updating process is not initialized.") } params := request.GetParams() if len(params) == 0 { - return nil, cfgcore.MakeError("update params not empty.") + return nil, cfgcore.MakeError("update params is empty.") } if err := r.updater(ctx, params); err != nil { return nil, err @@ -105,7 +108,7 @@ func (r *reconfigureProxy) initOnlineUpdater(opt *VolumeWatcherOpts) error { updater, err := cfgcm.OnlineUpdateParamsHandle(opt.TPLScriptPath, opt.FormatterConfig, opt.DataType, opt.DSN) if err != nil { - return cfgcore.WrapError(err, "failed to create online updater") + return cfgcore.WrapError(err, "failed to create online updating process") } r.updater = updater return nil diff --git a/cmd/reloader/container_killer/killer.go b/cmd/reloader/container_killer/killer.go index 2b3564240..99eeeaed2 100644 --- a/cmd/reloader/container_killer/killer.go +++ b/cmd/reloader/container_killer/killer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main @@ -42,18 +45,18 @@ var logger logr.Logger func main() { var containerRuntime cfgutil.CRIType var runtimeEndpoint string - var contaienrID []string + var containerID []string pflag.StringVar((*string)(&containerRuntime), - "container-runtime", "auto", "the config set cri runtime type.") + "container-runtime", "auto", "the config sets cri runtime type.") pflag.StringVar(&runtimeEndpoint, - "runtime-endpoint", runtimeEndpoint, "the config set cri runtime endpoint.") - pflag.StringArrayVar(&contaienrID, - "container-id", contaienrID, "the container-id killed.") + "runtime-endpoint", runtimeEndpoint, "the config sets cri runtime endpoint.") + pflag.StringArrayVar(&containerID, + "container-id", containerID, "the container-id to be killed.") pflag.Parse() - if len(contaienrID) == 0 { - fmt.Fprintf(os.Stderr, "require container-id!\n\n") + if len(containerID) == 0 { + fmt.Fprintf(os.Stderr, " container-id required!\n\n") pflag.Usage() os.Exit(-1) } @@ -68,7 +71,7 @@ func main() { killer, err := cfgutil.NewContainerKiller(containerRuntime, runtimeEndpoint, zapLogger.Sugar()) if err != nil { - logger.Error(err, "failed to create container killer") + logger.Error(err, "failed to create container killing process") os.Exit(-1) } @@ -76,7 +79,7 @@ func main() { logger.Error(err, "failed to init killer") } - if err := killer.Kill(context.Background(), contaienrID, viper.GetString(cfgutil.KillContainerSignalEnvName), nil); err != nil { - logger.Error(err, fmt.Sprintf("failed to kill container[%s]", contaienrID)) + if err := killer.Kill(context.Background(), containerID, viper.GetString(cfgutil.KillContainerSignalEnvName), nil); err != nil { + logger.Error(err, fmt.Sprintf("failed to kill container[%s]", containerID)) } } diff --git a/cmd/reloader/main.go b/cmd/reloader/main.go index fe3b23eb1..52695d4b2 100644 --- a/cmd/reloader/main.go +++ b/cmd/reloader/main.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main diff --git a/cmd/reloader/tools/README.md b/cmd/reloader/tools/README.md index 8f17ed74f..d505d4170 100644 --- a/cmd/reloader/tools/README.md +++ b/cmd/reloader/tools/README.md @@ -11,7 +11,7 @@ You can get started with cue-helper, by the following methods: ## 2.1 Build -Compiler `Go 1.19+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. +Compiler `Go 1.20+` (Generics Programming Support), checking the [Go Installation](https://go.dev/doc/install) to see how to install Go on your platform. Use `make cue-helper` to build and produce the `cue-helper` binary file. The executable is produced under current directory. @@ -31,7 +31,7 @@ Usage of ./bin/cue-helper: -boolean-promotion enable using OFF or ON. -file-path string - The generate cue scripts from file. + The generated cue scripts from file. -ignore-string-default ignore string default. (default true) -type-name string diff --git a/cmd/reloader/tools/cue_auto_generator.go b/cmd/reloader/tools/cue_auto_generator.go index db3180e31..140acb577 100644 --- a/cmd/reloader/tools/cue_auto_generator.go +++ b/cmd/reloader/tools/cue_auto_generator.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main @@ -94,8 +97,8 @@ var ValueTypeParserMap = map[ValueType]ValueParser{ } func main() { - // The source file format is per line per parameter, the fields are separated by tabs, and the fields are as follows: - // parameter name | default value | value restrict | is immutable(true/false) | value type(boolean/integer/string) | change type(static/dynamic) | description + // The source file format is one line per parameter, the fields are separated by tabs, and the fields are as follows: + // parameter name | default value | value restriction | is immutable(true/false) | value type(boolean/integer/string) | change type(static/dynamic) | description // file format example: // default_authentication_plugin\tmysql_native_password\tmysql_native_password, sha256_password, caching_sha2_password\tfalse\string\tstatic\tThe default authentication plugin diff --git a/code-of-conduct.md b/code-of-conduct.md deleted file mode 100644 index 16e75a924..000000000 --- a/code-of-conduct.md +++ /dev/null @@ -1,3 +0,0 @@ -# Community Code of Conduct - -KubeBlocks follows the [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md). \ No newline at end of file diff --git a/config/crd/bases/apps.kubeblocks.io_backuppolicytemplates.yaml b/config/crd/bases/apps.kubeblocks.io_backuppolicytemplates.yaml new file mode 100644 index 000000000..51ba57b1c --- /dev/null +++ b/config/crd/bases/apps.kubeblocks.io_backuppolicytemplates.yaml @@ -0,0 +1,430 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: backuppolicytemplates.apps.kubeblocks.io +spec: + group: apps.kubeblocks.io + names: + categories: + - kubeblocks + kind: BackupPolicyTemplate + listKind: BackupPolicyTemplateList + plural: backuppolicytemplates + shortNames: + - bpt + singular: backuppolicytemplate + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ClusterDefinition referenced by cluster. + jsonPath: .spec.clusterDefinitionRef + name: CLUSTER-DEFINITION + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BackupPolicyTemplate is the Schema for the BackupPolicyTemplates + API (defined by provider) + 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: BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate + properties: + backupPolicies: + description: backupPolicies is a list of backup policy template for + the specified componentDefinition. + items: + properties: + componentDefRef: + description: componentDefRef references componentDef defined + in ClusterDefinition spec. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + datafile: + description: the policy for datafile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, + only support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + logfile: + description: the policy for logfile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, + only support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + retention: + description: retention describe how long the Backup should be + retained. if not set, will be retained forever. + properties: + ttl: + description: ttl is a time string ending with the 'd'|'D'|'h'|'H' + character to describe how long the Backup should be retained. + if not set, will be retained forever. + pattern: ^\d+[d|D|h|H]$ + type: string + type: object + schedule: + description: schedule policy for backup. + properties: + datafile: + description: schedule policy for datafile backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + logfile: + description: schedule policy for logfile backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + snapshot: + description: schedule policy for snapshot backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + type: object + snapshot: + description: the policy for snapshot backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + hooks: + description: execute hook commands for backup. + properties: + containerName: + description: which container can exec command + type: string + image: + description: exec command with image + type: string + postCommands: + description: post backup to perform commands + items: + type: string + type: array + preCommands: + description: pre backup to perform commands + items: + type: string + type: array + type: object + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + required: + - componentDefRef + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - componentDefRef + x-kubernetes-list-type: map + clusterDefinitionRef: + description: clusterDefinitionRef references ClusterDefinition name, + this is an immutable attribute. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + identifier: + description: Identifier is a unique identifier for this BackupPolicyTemplate. + this identifier will be the suffix of the automatically generated + backupPolicy name. and must be added when multiple BackupPolicyTemplates + exist, otherwise the generated backupPolicy override will occur. + maxLength: 20 + type: string + required: + - backupPolicies + - clusterDefinitionRef + type: object + status: + description: BackupPolicyTemplateStatus defines the observed state of + BackupPolicyTemplate + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml index 2916d11b2..60423c646 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusterdefinitions.yaml @@ -94,6 +94,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: set name: description: Specify the name of configuration template. maxLength: 63 @@ -219,17 +220,72 @@ spec: - accessMode - name type: object + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object updateStrategy: default: Serial - description: 'updateStrategy, Pods update strategy. serial: - update Pods one by one that guarantee minimum component - unavailable time. Learner -> Follower(with AccessMode=none) - -> Follower(with AccessMode=readonly) -> Follower(with - AccessMode=readWrite) -> Leader bestEffortParallel: update - Pods in parallel that guarantee minimum component un-writable - time. Learner, Follower(minority) in parallel -> Follower(majority) - -> Leader, keep majority online all the time. parallel: - force parallel' + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" enum: - Serial - BestEffortParallel @@ -286,12 +342,10 @@ spec: description: horizontalScalePolicy controls the behavior of horizontal scale. properties: - backupTemplateSelector: - additionalProperties: - type: string - description: backupTemplateSelector defines the label selector - for finding associated BackupTemplate API object. - type: object + backupPolicyTemplateName: + description: BackupPolicyTemplateName reference the backup + policy template. + type: string type: default: None description: 'type controls what kind of data synchronization @@ -299,10 +353,10 @@ spec: Snapshot}. The default policy is `None`. None: Default policy, do nothing. Snapshot: Do native volume snapshot before scaling and restore to newly scaled pods. Prefer - backup job to create snapshot if `BackupTemplateSelector` - can find a template. Notice that ''Snapshot'' policy will - only take snapshot on one volumeMount, default is the - first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]), + backup job to create snapshot if can find a backupPolicy + from ''BackupPolicyTemplateName''. Notice that ''Snapshot'' + policy will only take snapshot on one volumeMount, default + is the first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]), since take multiple snapshots at one time might cause consistency problem.' enum: @@ -340,16 +394,6 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - maxUnavailable: - anyOf: - - type: integer - - type: string - description: 'The maximum number of pods that can be unavailable - during scaling. Value can be an absolute number (ex: 5) or - a percentage of desired pods (ex: 10%). Absolute number is - calculated from percentage by rounding down. This value is - ignored if workloadType is Consensus.' - x-kubernetes-int-or-string: true monitor: description: monitor is monitoring config which provided by provider. @@ -357,9 +401,10 @@ spec: builtIn: default: false description: builtIn is a switch to enable KubeBlocks builtIn - monitoring. If BuiltIn is set to false, the provider should - set ExporterConfig and Sidecar container own. BuiltIn - set to true is not currently supported but will be soon. + monitoring. If BuiltIn is set to true, monitor metrics + will be scraped automatically. If BuiltIn is set to false, + the provider should set ExporterConfig and Sidecar container + own. type: boolean exporterConfig: description: exporterConfig provided by provider, which @@ -373,12 +418,12 @@ spec: maxLength: 128 type: string scrapePort: + anyOf: + - type: integer + - type: string description: scrapePort is exporter port for Time Series Database to scrape metrics. - format: int32 - maximum: 65535 - minimum: 0 - type: integer + x-kubernetes-int-or-string: true required: - scrapePort type: object @@ -7916,7 +7961,7 @@ spec: probes: description: probes setting for healthy checks. properties: - roleChangedProbe: + roleProbe: description: Probe for DB role changed check. properties: commands: @@ -7963,9 +8008,8 @@ spec: exceed the InitializationTimeoutSeconds time without a role label, this component will enter the Failed/Abnormal phase. Note that this configuration will only take effect - if the component supports RoleChangedProbe and will not - affect the life cycle of the pod. default values are 60 - seconds. + if the component supports RoleProbe and will not affect + the life cycle of the pod. default values are 60 seconds. format: int32 minimum: 30 type: integer @@ -8052,9 +8096,62 @@ spec: type: object replicationSpec: description: replicationSpec defines replication related spec - if workloadType is Replication, required if workloadType is - Replication. + if workloadType is Replication. properties: + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object switchCmdExecutorConfig: description: switchCmdExecutorConfig configs how to get client SDK and perform switch statements. @@ -8286,6 +8383,23 @@ spec: type: object minItems: 1 type: array + updateStrategy: + default: Serial + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" + enum: + - Serial + - BestEffortParallel + - Parallel + type: string required: - switchCmdExecutorConfig type: object @@ -8403,6 +8517,141 @@ spec: - protocol x-kubernetes-list-type: map type: object + statefulSpec: + description: statefulSpec defines stateful related spec if workloadType + is Stateful. + properties: + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + updateStrategy: + default: Serial + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" + enum: + - Serial + - BestEffortParallel + - Parallel + type: string + type: object + statelessSpec: + description: statelessSpec defines stateless related spec if + workloadType is Stateless. + properties: + updateStrategy: + description: updateStrategy defines the underlying deployment + strategy to use to replace existing pods with new ones. + properties: + rollingUpdate: + description: 'Rolling update config params. Present + only if DeploymentStrategyType = RollingUpdate. --- + TODO: Update this to follow our convention for oneOf, + whatever we decide it to be.' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be scheduled above the desired number of pods. + Value can be an absolute number (ex: 5) or a percentage + of desired pods (ex: 10%). This can not be 0 if + MaxUnavailable is 0. Absolute number is calculated + from percentage by rounding up. Defaults to 25%. + Example: when this is set to 30%, the new ReplicaSet + can be scaled up immediately when the rolling + update starts, such that the total number of old + and new pods do not exceed 130% of desired pods. + Once old pods have been killed, new ReplicaSet + can be scaled up further, ensuring that total + number of pods running at any time during the + update is at most 130% of desired pods.' + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding down. This can not + be 0 if MaxSurge is 0. Defaults to 25%. Example: + when this is set to 30%, the old ReplicaSet can + be scaled down to 70% of desired pods immediately + when the rolling update starts. Once new pods + are ready, old ReplicaSet can be scaled down further, + followed by scaling up the new ReplicaSet, ensuring + that the total number of pods available at all + times during the update is at least 70% of desired + pods.' + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + type: object systemAccounts: description: Statement to create system account. properties: @@ -8457,7 +8706,16 @@ spec: type: string deletion: description: deletion specifies statement - how to delete this account. + how to delete this account. Used in combination + with `CreateionStatement` to delete the + account before create it. For instance, + one usually uses `drop user if exists` statement + followed by `create user` statement to create + an account. + type: string + update: + description: update specifies statement how + to update account's password. type: string required: - creation @@ -8662,9 +8920,9 @@ spec: the volumes mapping the name of the VolumeMounts in the PodSpec.Container field, such as data volume, log volume, etc. When backing up the volume, the volume can be correctly backed up according - to the volumeType. \n For example: `{name: data, type: data}` + to the volumeType. \n For example: `name: data, type: data` means that the volume named `data` is used to store `data`. - `{name: binlog, type: log}` means that the volume named `binlog` + `name: binlog, type: log` means that the volume named `binlog` is used to store `log`. \n NOTE: When volumeTypes is not defined, the backup function will not be supported, even if a persistent volume has been specified." @@ -8685,8 +8943,13 @@ spec: - data - log type: string + required: + - name type: object type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map workloadType: description: workloadType defines type of the workload. Stateless is a stateless workload type used to describe stateless applications. @@ -8706,6 +8969,11 @@ spec: - name - workloadType type: object + x-kubernetes-validations: + - message: componentDefs.consensusSpec is required when componentDefs.workloadType + is Consensus, and forbidden otherwise + rule: 'has(self.workloadType) && self.workloadType == ''Consensus'' + ? has(self.consensusSpec) : !has(self.consensusSpec)' minItems: 1 type: array x-kubernetes-list-map-keys: @@ -8715,17 +8983,20 @@ spec: additionalProperties: type: string description: 'Connection credential template used for creating a connection - credential secret for cluster.apps.kubeblock.io object. Built-in + credential secret for cluster.apps.kubeblocks.io object. Built-in objects are: `$(RANDOM_PASSWD)` - random 8 characters. `$(UUID)` - generate a random UUID v4 string. `$(UUID_B64)` - generate a random - UUID v4 BASE64 encoded string``. `$(UUID_STR_B64)` - generate a - random UUID v4 string then BASE64 encoded``. `$(UUID_HEX)` - generate - a random UUID v4 wth HEX representation``. `$(SVC_FQDN)` - service - FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc, + UUID v4 BASE64 encoded string. `$(UUID_STR_B64)` - generate a random + UUID v4 string then BASE64 encoded. `$(UUID_HEX)` - generate a random + UUID v4 HEX representation. `$(HEADLESS_SVC_FQDN)` - headless service + FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc, where 1ST_COMP_NAME is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` - attribute; `$(SVC_PORT_)` - a ServicePort''s port value - with specified port name, i.e, a servicePort JSON struct: { "name": - "mysql", "targetPort": "mysqlContainerPort", "port": 3306 }, and + attribute; `$(SVC_FQDN)` - service FQDN placeholder, value pattern + - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc, where 1ST_COMP_NAME + is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` + attribute; `$(SVC_PORT_{PORT-NAME})` - a ServicePort''s port value + with specified port name, i.e, a servicePort JSON struct: `"name": + "mysql", "targetPort": "mysqlContainerPort", "port": 3306`, and "$(SVC_PORT_mysql)" in the connection credential value is 3306.' type: object type: @@ -8750,9 +9021,9 @@ spec: format: int64 type: integer phase: - description: ClusterDefinition phase, valid values are , Available. - Available is ClusterDefinition become available, and can be referenced - for co-related objects. + description: ClusterDefinition phase, valid values are `empty`, `Available`, + 'Unavailable`. Available is ClusterDefinition become available, + and can be referenced for co-related objects. enum: - Available - Unavailable diff --git a/config/crd/bases/apps.kubeblocks.io_clusters.yaml b/config/crd/bases/apps.kubeblocks.io_clusters.yaml index 68e463d91..8f4d20db2 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusters.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusters.yaml @@ -41,7 +41,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Cluster is the Schema for the clusters API + description: Cluster is the Schema for the clusters API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -56,7 +56,7 @@ spec: metadata: type: object spec: - description: ClusterSpec defines the desired state of Cluster + description: ClusterSpec defines the desired state of Cluster. properties: affinity: description: affinity is a group of affinity scheduling rules. @@ -99,12 +99,12 @@ spec: x-kubernetes-list-type: set type: object clusterDefinitionRef: - description: Cluster referenced ClusterDefinition name, this is an + description: Cluster referencing ClusterDefinition name. This is an immutable attribute. pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string clusterVersionRef: - description: Cluster referenced ClusterVersion name. + description: Cluster referencing ClusterVersion name. pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string componentSpecs: @@ -114,8 +114,7 @@ spec: items: properties: affinity: - description: affinity describes affinities which specific by - users. + description: affinity describes affinities specified by users. properties: nodeLabels: additionalProperties: @@ -156,28 +155,41 @@ spec: type: array x-kubernetes-list-type: set type: object + classDefRef: + description: classDefRef references the class defined in ComponentClassDefinition. + properties: + class: + description: Class refers to the name of the class that + is defined in the ComponentClassDefinition. + type: string + name: + description: Name refers to the name of the ComponentClassDefinition. + type: string + required: + - class + type: object componentDefRef: - description: componentDefRef reference componentDef defined + description: componentDefRef references the componentDef defined in ClusterDefinition spec. maxLength: 63 pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string enabledLogs: - description: enabledLogs indicate which log file takes effect - in database cluster element is the log type which defined - in cluster definition logConfig.name, and will set relative - variables about this log type in database kernel. + description: enabledLogs indicates which log file takes effect + in the database cluster. element is the log type which is + defined in cluster definition logConfig.name, and will set + relative variables about this log type in database kernel. items: type: string type: array x-kubernetes-list-type: set issuer: - description: issuer who provides tls certs required when TLS - enabled + description: issuer defines provider context for TLS certs. + required when TLS enabled properties: name: default: KubeBlocks - description: 'name of issuer options supported: - KubeBlocks + description: 'Name of issuer. Options supported: - KubeBlocks - Certificates signed by KubeBlocks Operator. - UserProvided - User provided own CA-signed certificates.' enum: @@ -185,20 +197,20 @@ spec: - UserProvided type: string secretRef: - description: secretRef, Tls certs Secret reference required + description: secretRef. TLS certs Secret reference required when from is UserProvided properties: ca: - description: ca cert key in Secret + description: CA cert key in Secret type: string cert: - description: cert key in Secret + description: Cert key in Secret type: string key: - description: key of TLS private key in Secret + description: Key of TLS private key in Secret type: string name: - description: name of the Secret + description: Name of the Secret type: string required: - ca @@ -211,33 +223,39 @@ spec: type: object monitor: default: false - description: monitor which is a switch to enable monitoring, - default is false KubeBlocks provides an extension mechanism - to support component level monitoring, which will scrape metrics - auto or manually from servers in component and export metrics - to Time Series Database. + description: monitor is a switch to enable monitoring and is + set as false by default. KubeBlocks provides an extension + mechanism to support component level monitoring, which will + scrape metrics auto or manually from servers in component + and export metrics to Time Series Database. type: boolean name: description: name defines cluster's component name. maxLength: 15 pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string + noCreatePDB: + default: false + description: noCreatePDB defines the PodDistruptionBudget creation + behavior and is set to true if creation of PodDistruptionBudget + for this component is not needed. It defaults to false. + type: boolean primaryIndex: description: primaryIndex determines which index is primary - when workloadType is Replication, index number starts from + when workloadType is Replication. Index number starts from zero. format: int32 minimum: 0 type: integer replicas: default: 1 - description: Component replicas, use default value in ClusterDefinition - spec. if not specified. + description: Component replicas. The default value is used in + ClusterDefinition spec if not specified. format: int32 minimum: 0 type: integer resources: - description: resources requests and limits of workload. + description: Resources requests and limits of workload. properties: claims: description: "Claims lists the names of resources, defined @@ -285,18 +303,24 @@ spec: type: object type: object x-kubernetes-preserve-unknown-fields: true + serviceAccountName: + description: serviceAccountName is the name of the ServiceAccount + that running component depends on. + type: string services: - description: services expose endpoints can be accessed by clients + description: Services expose endpoints that can be accessed + by clients. items: properties: annotations: additionalProperties: type: string description: 'If ServiceType is LoadBalancer, cloud provider - related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -305,15 +329,16 @@ spec: LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not specified, - by manual construction of an Endpoints object or EndpointSlice - objects. If clusterIP is "None", no virtual IP is allocated - and the endpoints are published as a set of endpoints - rather than a virtual IP. "NodePort" builds on ClusterIP - and allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" builds - on NodePort and creates an external load-balancer (if - supported in the current cloud) which routes to the - same endpoints as the clusterIP. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + they are determined by manual construction of an Endpoints + object or EndpointSlice objects. If clusterIP is "None", + no virtual IP is allocated and the endpoints are published + as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node + which routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external + load-balancer (if supported in the current cloud) which + routes to the same endpoints as the clusterIP. More + info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -339,7 +364,7 @@ spec: type: string type: object tls: - description: tls should be enabled or not + description: Enables or disables TLS certs. type: boolean tolerations: description: Component tolerations will override ClusterSpec.Tolerations @@ -390,7 +415,7 @@ spec: items: properties: name: - description: Ref ClusterVersion.spec.components.containers.volumeMounts.name + description: Reference `ClusterDefinition.spec.componentDefs.containers.volumeMounts.name`. type: string spec: description: spec defines the desired characteristics @@ -398,17 +423,18 @@ spec: properties: accessModes: description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1.' items: type: string type: array + x-kubernetes-preserve-unknown-fields: true resources: description: 'resources represents the minimum resources the volume should have. If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements that are lower than previous value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources.' properties: claims: description: "Claims lists the names of resources, @@ -458,12 +484,12 @@ spec: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object + x-kubernetes-preserve-unknown-fields: true storageClassName: description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1.' type: string type: object - x-kubernetes-preserve-unknown-fields: true required: - name type: object @@ -479,12 +505,12 @@ spec: - name x-kubernetes-list-type: map terminationPolicy: - description: Cluster termination policy. One of DoNotTerminate, Halt, - Delete, WipeOut. DoNotTerminate will block delete operation. Halt - will delete workload resources such as statefulset, deployment workloads - but keep PVCs. Delete is based on Halt and deletes PVCs. WipeOut - is based on Delete and wipe out all volume snapshots and snapshot - data from backup storage location. + description: Cluster termination policy. Valid values are DoNotTerminate, + Halt, Delete, WipeOut. DoNotTerminate will block delete operation. + Halt will delete workload resources such as statefulset, deployment + workloads but keep PVCs. Delete is based on Halt and deletes PVCs. + WipeOut is based on Delete and wipe out all volume snapshots and + snapshot data from backup storage location. enum: - DoNotTerminate - Halt @@ -493,7 +519,7 @@ spec: type: string tolerations: description: tolerations are attached to tolerate any taint that matches - the triple using the matching operator . + the triple `key,value,effect` using the matching operator `operator`. items: description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching @@ -538,7 +564,7 @@ spec: - terminationPolicy type: object status: - description: ClusterStatus defines the observed state of Cluster + description: ClusterStatus defines the observed state of Cluster. properties: clusterDefGeneration: description: clusterDefGeneration represents the generation number @@ -547,18 +573,20 @@ spec: type: integer components: additionalProperties: - description: ClusterComponentStatus record components status information + description: ClusterComponentStatus records components status. properties: consensusSetStatus: - description: consensusSetStatus role and pod name mapping. + description: consensusSetStatus specifies the mapping of role + and pod name. properties: followers: - description: followers status. + description: Followers status. items: properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this + pod provides. enum: - None - Readonly @@ -566,11 +594,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -579,11 +607,12 @@ spec: type: object type: array leader: - description: leader status. + description: Leader status. properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this pod + provides. enum: - None - Readonly @@ -591,11 +620,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -603,11 +632,12 @@ spec: - pod type: object learner: - description: learner status. + description: Learner status. properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this pod + provides. enum: - None - Readonly @@ -615,11 +645,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -633,22 +663,22 @@ spec: additionalProperties: type: string description: message records the component details message in - current phase. keys are podName or deployName or statefulSetName, - the format is `/`. + current phase. Keys are podName or deployName or statefulSetName. + The format is `ObjectKind/Name`. type: object phase: - description: 'phase describes the phase of the component, the - detail information of the phases are as following: Running: - component is running. [terminal state] Stopped: component - is stopped, as no running pod. [terminal state] Failed: component - is unavailable. i.e, all pods are not ready for Stateless/Stateful - component, Leader/Primary pod is not ready for Consensus/Replication - component. [terminal state] Abnormal: component is running - but part of its pods are not ready. Leader/Primary pod is + description: 'phase describes the phase of the component and + the detail information of the phases are as following: Running: + the component is running. [terminal state] Stopped: the component + is stopped, as no running pod. [terminal state] Failed: the + component is unavailable, i.e. all pods are not ready for + Stateless/Stateful component and Leader/Primary pod is not ready for Consensus/Replication component. [terminal state] - Creating: component has entered creating process. Updating: - component has entered updating process, triggered by Spec. - updated.' + Abnormal: the component is running but part of its pods are + not ready. Leader/Primary pod is ready for Consensus/Replication + component. [terminal state] Creating: the component has entered + creating process. Updating: the component has entered updating + process, triggered by Spec. updated.' enum: - Running - Stopped @@ -668,25 +698,26 @@ spec: format: date-time type: string replicationSetStatus: - description: replicationSetStatus role and pod name mapping. + description: replicationSetStatus specifies the mapping of role + and pod name. properties: primary: - description: primary status. + description: Primary status. properties: pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - pod type: object secondaries: - description: secondaries status. + description: Secondaries status. items: properties: pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - pod diff --git a/config/crd/bases/apps.kubeblocks.io_clusterversions.yaml b/config/crd/bases/apps.kubeblocks.io_clusterversions.yaml index 517450462..8d04ae87c 100644 --- a/config/crd/bases/apps.kubeblocks.io_clusterversions.yaml +++ b/config/crd/bases/apps.kubeblocks.io_clusterversions.yaml @@ -100,6 +100,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: set name: description: Specify the name of configuration template. maxLength: 63 @@ -134,6 +135,146 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + systemAccountSpec: + description: systemAccountSpec define image for the component + to connect database or engines. It overrides `image` and `env` + attributes defined in ClusterDefinition.spec.componentDefs.systemAccountSpec.cmdExecutorConfig. + To clean default envs settings, set `SystemAccountSpec.CmdExecutorConfig.Env` + to empty list. + properties: + cmdExecutorConfig: + description: cmdExecutorConfig configs how to get client + SDK and perform statements. + properties: + env: + description: envs is a list of environment variables. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + x-kubernetes-preserve-unknown-fields: true + image: + description: image for Connector when executing the + command. + type: string + required: + - image + type: object + required: + - cmdExecutorConfig + type: object versionsContext: description: versionContext defines containers images' context for component versions, this value replaces ClusterDefinition.spec.componentDefs.podSpec.[initContainers diff --git a/config/crd/bases/apps.kubeblocks.io_componentclassdefinitions.yaml b/config/crd/bases/apps.kubeblocks.io_componentclassdefinitions.yaml new file mode 100644 index 000000000..9c8b10c7c --- /dev/null +++ b/config/crd/bases/apps.kubeblocks.io_componentclassdefinitions.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: componentclassdefinitions.apps.kubeblocks.io +spec: + group: apps.kubeblocks.io + names: + categories: + - kubeblocks + kind: ComponentClassDefinition + listKind: ComponentClassDefinitionList + plural: componentclassdefinitions + shortNames: + - ccd + singular: componentclassdefinition + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ComponentClassDefinition is the Schema for the componentclassdefinitions + 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: ComponentClassDefinitionSpec defines the desired state of + ComponentClassDefinition + properties: + groups: + description: group defines a list of class series that conform to + the same constraint. + items: + properties: + resourceConstraintRef: + description: resourceConstraintRef reference to the resource + constraint object name, indicates that the series defined + below all conform to the constraint. + type: string + series: + description: series is a series of class definitions. + items: + properties: + classes: + description: classes are definitions of classes that come + in two forms. In the first form, only ComponentClass.Args + need to be defined, and the complete class definition + is generated by rendering the ComponentClassGroup.Template + and Name. In the second form, the Name, CPU and Memory + must be defined. + items: + properties: + args: + description: args are variable's value + items: + type: string + type: array + cpu: + anyOf: + - type: integer + - type: string + description: the CPU of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + description: the memory of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: name is the class name + type: string + type: object + type: array + namingTemplate: + description: 'namingTemplate is a template that uses the + Go template syntax and allows for referencing variables + defined in ComponentClassGroup.Template. This enables + dynamic generation of class names. For example: name: + "general-{{ .cpu }}c{{ .memory }}g"' + type: string + type: object + type: array + template: + description: "template is a class definition template that uses + the Go template syntax and allows for variable declaration. + When defining a class in Series, specifying the variable's + value is sufficient, as the complete class definition will + be generated through rendering the template. \n For example: + template: | cpu: \"{{ or .cpu 1 }}\" memory: \"{{ or .memory + 4 }}Gi\"" + type: string + vars: + description: vars defines the variables declared in the template + and will be used to generating the complete class definition + by render the template. + items: + type: string + type: array + x-kubernetes-list-type: set + required: + - resourceConstraintRef + type: object + type: array + type: object + status: + description: ComponentClassDefinitionStatus defines the observed state + of ComponentClassDefinition + properties: + classes: + description: classes is the list of classes that have been observed + for this ComponentClassDefinition + items: + properties: + args: + description: args are variable's value + items: + type: string + type: array + cpu: + anyOf: + - type: integer + - type: string + description: the CPU of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + description: the memory of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: name is the class name + type: string + resourceConstraintRef: + description: resourceConstraintRef reference to the resource + constraint object name. + type: string + type: object + type: array + observedGeneration: + description: observedGeneration is the most recent generation observed + for this ComponentClassDefinition. It corresponds to the ComponentClassDefinition's + generation, which is updated on mutation by the API Server. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/bases/apps.kubeblocks.io_classfamilies.yaml b/config/crd/bases/apps.kubeblocks.io_componentresourceconstraints.yaml similarity index 92% rename from config/crd/bases/apps.kubeblocks.io_classfamilies.yaml rename to config/crd/bases/apps.kubeblocks.io_componentresourceconstraints.yaml index 1c4f66d32..5f00d7070 100644 --- a/config/crd/bases/apps.kubeblocks.io_classfamilies.yaml +++ b/config/crd/bases/apps.kubeblocks.io_componentresourceconstraints.yaml @@ -5,25 +5,26 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null - name: classfamilies.apps.kubeblocks.io + name: componentresourceconstraints.apps.kubeblocks.io spec: group: apps.kubeblocks.io names: categories: - kubeblocks - all - kind: ClassFamily - listKind: ClassFamilyList - plural: classfamilies + kind: ComponentResourceConstraint + listKind: ComponentResourceConstraintList + plural: componentresourceconstraints shortNames: - - cf - singular: classfamily + - crc + singular: componentresourceconstraint scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: - description: ClassFamily is the Schema for the classfamilies API + description: ComponentResourceConstraint is the Schema for the componentresourceconstraints + API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -38,11 +39,11 @@ spec: metadata: type: object spec: - description: ClassFamilySpec defines the desired state of ClassFamily + description: ComponentResourceConstraintSpec defines the desired state + of ComponentResourceConstraint properties: - models: - description: Class family models, generally, a model is a specific - constraint for CPU, memory and their relation. + constraints: + description: Component resource constraints items: properties: cpu: @@ -133,6 +134,9 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + required: + - cpu + - memory type: object type: array type: object diff --git a/config/crd/bases/apps.kubeblocks.io_configconstraints.yaml b/config/crd/bases/apps.kubeblocks.io_configconstraints.yaml index 5a6a5d3b0..c73a6b1d2 100644 --- a/config/crd/bases/apps.kubeblocks.io_configconstraints.yaml +++ b/config/crd/bases/apps.kubeblocks.io_configconstraints.yaml @@ -284,16 +284,12 @@ spec: format: int64 type: integer phase: - allOf: - - enum: - - Available - - Unavailable - - enum: - - Available - - Unavailable - - Deleting description: phase is status of configuration template, when set to - AvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. + CCAvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. + enum: + - Available + - Unavailable + - Deleting type: string type: object type: object diff --git a/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml b/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml index 0ab01c7f7..119abe42a 100644 --- a/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml +++ b/config/crd/bases/apps.kubeblocks.io_opsrequests.yaml @@ -60,9 +60,18 @@ spec: spec: description: OpsRequestSpec defines the desired state of OpsRequest properties: + cancel: + description: 'cancel defines the action to cancel the Pending/Creating/Running + opsRequest, supported types: [VerticalScaling, HorizontalScaling]. + once cancel is set to true, this opsRequest will be canceled and + modifying this property again will not take effect.' + type: boolean clusterRef: description: clusterRef references clusterDefinition. type: string + x-kubernetes-validations: + - message: forbidden to update spec.clusterRef + rule: self == oldSelf expose: description: expose defines services the component needs to expose. items: @@ -78,10 +87,11 @@ spec: additionalProperties: type: string description: 'If ServiceType is LoadBalancer, cloud provider - related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -90,15 +100,16 @@ spec: LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not specified, - by manual construction of an Endpoints object or EndpointSlice - objects. If clusterIP is "None", no virtual IP is allocated - and the endpoints are published as a set of endpoints - rather than a virtual IP. "NodePort" builds on ClusterIP - and allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" builds - on NodePort and creates an external load-balancer (if - supported in the current cloud) which routes to the - same endpoints as the clusterIP. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + they are determined by manual construction of an Endpoints + object or EndpointSlice objects. If clusterIP is "None", + no virtual IP is allocated and the endpoints are published + as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node + which routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external + load-balancer (if supported in the current cloud) which + routes to the same endpoints as the clusterIP. More + info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -117,6 +128,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.expose + rule: self == oldSelf horizontalScaling: description: horizontalScaling defines what component need to horizontal scale the specified replicas. @@ -140,6 +154,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.horizontalScaling + rule: self == oldSelf reconfigure: description: reconfigure defines the variables that need to input when updating configuration. @@ -207,6 +224,9 @@ spec: - componentName - configurations type: object + x-kubernetes-validations: + - message: forbidden to update spec.reconfigure + rule: self == oldSelf restart: description: restart the specified component. items: @@ -223,6 +243,52 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.restart + rule: self == oldSelf + restoreFrom: + description: cluster RestoreFrom backup or point in time + properties: + backup: + description: use the backup name and component name for restore, + support for multiple components' recovery. + items: + properties: + ref: + description: specify a reference backup to restore + properties: + name: + description: specified the name + type: string + namespace: + description: specified the namespace + type: string + type: object + type: object + type: array + pointInTime: + description: specified the point in time to recovery + properties: + ref: + description: specify a reference source cluster to restore + properties: + name: + description: specified the name + type: string + namespace: + description: specified the namespace + type: string + type: object + time: + description: specify the time point to restore, with UTC as + the time zone. + format: date-time + type: string + type: object + type: object + x-kubernetes-validations: + - message: forbidden to update spec.restoreFrom + rule: self == oldSelf ttlSecondsAfterSucceed: description: ttlSecondsAfterSucceed OpsRequest will be deleted after TTLSecondsAfterSucceed second when OpsRequest.status.phase is Succeed. @@ -241,6 +307,9 @@ spec: - Stop - Expose type: string + x-kubernetes-validations: + - message: forbidden to update spec.type + rule: self == oldSelf upgrade: description: upgrade specifies the cluster version by specifying clusterVersionRef. properties: @@ -250,6 +319,9 @@ spec: required: - clusterVersionRef type: object + x-kubernetes-validations: + - message: forbidden to update spec.upgrade + rule: self == oldSelf verticalScaling: description: verticalScaling defines what component need to vertical scale the specified compute resources. @@ -277,6 +349,9 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + class: + description: class specifies the class name of the component + type: string componentName: description: componentName cluster component name. type: string @@ -310,6 +385,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.verticalScaling + rule: self == oldSelf volumeExpansion: description: volumeExpansion defines what component and volumeClaimTemplate need to expand the specified storage. @@ -352,13 +430,24 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.volumeExpansion + rule: self == oldSelf required: - clusterRef - type type: object + x-kubernetes-validations: + - message: forbidden to cancel the opsRequest which type not in ['VerticalScaling','HorizontalScaling'] + rule: 'has(self.cancel) && self.cancel ? (self.type in [''VerticalScaling'', + ''HorizontalScaling'']) : true' status: description: OpsRequestStatus defines the observed state of OpsRequest properties: + cancelTimestamp: + description: CancelTimestamp defines cancel time. + format: date-time + type: string clusterGeneration: description: ClusterGeneration records the cluster generation after handling the opsRequest action. @@ -539,6 +628,9 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + class: + description: the last class name of the component. + type: string limits: additionalProperties: anyOf: @@ -575,10 +667,11 @@ spec: type: string description: 'If ServiceType is LoadBalancer, cloud provider related parameters can be put here More - info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -587,17 +680,17 @@ spec: and LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not - specified, by manual construction of an Endpoints - object or EndpointSlice objects. If clusterIP is - "None", no virtual IP is allocated and the endpoints - are published as a set of endpoints rather than - a virtual IP. "NodePort" builds on ClusterIP and - allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" + specified, they are determined by manual construction + of an Endpoints object or EndpointSlice objects. + If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints + rather than a virtual IP. "NodePort" builds on ClusterIP + and allocates a port on every node which routes + to the same endpoints as the clusterIP. "LoadBalancer" builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP. More info: - https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -649,6 +742,8 @@ spec: - Pending - Creating - Running + - Cancelling + - Cancelled - Failed - Succeed type: string diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicies.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backuppolicies.yaml index 6e359243d..b15056397 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicies.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backuppolicies.yaml @@ -14,6 +14,8 @@ spec: kind: BackupPolicy listKind: BackupPolicyList plural: backuppolicies + shortNames: + - bp singular: backuppolicy scope: Namespaced versions: @@ -21,9 +23,6 @@ spec: - jsonPath: .status.phase name: STATUS type: string - - jsonPath: .spec.schedule - name: SCHEDULE - type: string - jsonPath: .status.lastScheduleTime name: LAST SCHEDULE type: string @@ -51,1696 +50,569 @@ spec: spec: description: BackupPolicySpec defines the desired state of BackupPolicy properties: - backupPolicyTemplateName: - description: policy can inherit from backup config and override some - fields. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - backupStatusUpdates: - description: define how to update metadata for backup status. - items: - properties: - containerName: - description: which container name that kubectl can execute. - type: string - path: - description: 'specify the json path of backup object for patch. - example: manifests.backupLog -- means patch the backup json - path of status.manifests.backupLog.' - type: string - script: - description: the shell Script commands to collect backup status - metadata. The script must exist in the container of ContainerName - and the output format must be set to JSON. Note that outputting - to stderr may cause the result format to not be in JSON. - type: string - updateStage: - description: 'when to update the backup status, pre: before - backup, post: after backup' - enum: - - pre - - post - type: string - type: object - type: array - backupToolName: - description: which backup tool to perform database backup, only support - one tool. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - backupType: - default: snapshot - description: Backup ComponentDefRef. full or incremental or snapshot. - if unset, default is snapshot. - enum: - - full - - incremental - - snapshot - type: string - backupsHistoryLimit: - default: 7 - description: The number of automatic backups to retain. Value must - be non-negative integer. 0 means NO limit on the number of backups. - format: int32 - type: integer - hooks: - description: execute hook commands for backup. + datafile: + description: the policy for datafile backup. properties: - containerName: - description: which container can exec command - type: string - image: - description: exec command with image - type: string - postCommands: - description: post backup to perform commands + backupStatusUpdates: + description: define how to update metadata for backup status. items: - type: string - type: array - preCommands: - description: pre backup to perform commands - items: - type: string + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object type: array - type: object - onFailAttempted: - description: count of backup stop retries on fail. - format: int32 - type: integer - remoteVolume: - description: array of remote volumes from CSI driver definition. - properties: - awsElasticBlockStore: - description: 'awsElasticBlockStore represents an AWS Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty).' - format: int32 - type: integer - readOnly: - description: 'readOnly value true will force the readOnly - setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: boolean - volumeID: - description: 'volumeID is unique ID of the persistent disk - resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob storage - type: string - fsType: - description: fsType is Filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple blob - disks per storage account Dedicated: single blob disk per - storage account Managed: azure managed data disk (only - in managed availability set). defaults to shared' - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime + backupToolName: + description: which backup tool to perform database backup, only + support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + persistentVolumeClaim: + description: refer to PersistentVolumeClaim and the backup data + will be stored in the corresponding persistent volume. properties: - monitors: - description: 'monitors is Required: Monitors is a collection - of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' - type: string - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: boolean - secretFile: - description: 'secretFile is Optional: SecretFile is the path - to key ring for User, default is /etc/ceph/user.secret More - info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - secretRef: - description: 'secretRef is Optional: SecretRef is reference - to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + createPolicy: + default: IfNotPresent + description: 'createPolicy defines the policy for creating + the PersistentVolumeClaim, enum values: - Never: do nothing + if the PersistentVolumeClaim not exists. - IfNotPresent: + create the PersistentVolumeClaim if not present and the + accessModes only contains ''ReadWriteMany''.' + enum: + - IfNotPresent + - Never + type: string + initCapacity: + anyOf: + - type: integer + - type: string + description: initCapacity represents the init storage size + of the PersistentVolumeClaim which should be created if + not exist. and the default value is 100Gi if it is empty. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: the name of the PersistentVolumeClaim. + type: string + persistentVolumeConfigMap: + description: 'persistentVolumeConfigMap references the configmap + which contains a persistentVolume template. key must be + "persistentVolume" and value is the "PersistentVolume" struct. + support the following built-in Objects: - $(GENERATE_NAME): + generate a specific format "pvcName-pvcNamespace". if the + PersistentVolumeClaim not exists and CreatePolicy is "IfNotPresent", + the controller will create it by this template. this is + a mutually exclusive setting with "storageClassName".' 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?' + description: the name of the persistentVolume ConfigMap. type: string - type: object - user: - description: 'user is optional: User is the rados user name, - default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - required: - - monitors - type: object - cinder: - description: 'cinder represents a cinder volume attached and mounted - on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to - be "ext4" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - readOnly: - description: 'readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. More - info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: boolean - secretRef: - description: 'secretRef is optional: points to a secret object - containing parameters used to connect to OpenStack.' - 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?' + namespace: + description: the namespace of the persistentVolume ConfigMap. type: string + required: + - name + - namespace type: object - volumeID: - description: 'volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. type: string required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: 'defaultMode is optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items if unspecified, each key-value pair in - the Data field of the referenced ConfigMap will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - ConfigMap, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean + - name type: object - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). + target: + description: target database cluster for backup. properties: - driver: - description: driver is the name of the CSI driver that handles - this volume. Consult with your admin for the correct name - as registered in the cluster. - type: string - fsType: - description: fsType to mount. Ex. "ext4", "xfs", "ntfs". If - not provided, the empty value is passed to the associated - CSI driver which will determine the default filesystem to - apply. - type: string - nodePublishSecretRef: - description: nodePublishSecretRef is a reference to the secret - object containing sensitive information to pass to the CSI - driver to complete the CSI NodePublishVolume and NodeUnpublishVolume - calls. This field is optional, and may be empty if no secret - is required. If the secret object contains more than one - secret, all secret references are passed. + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. 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 - readOnly: - description: readOnly specifies a read-only configuration - for the volume. Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: volumeAttributes stores driver-specific properties - that are passed to the CSI driver. Consult your driver's - documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: - description: 'Optional: mode bits to use on created files - by default. Must be a Optional: mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Defaults to 0644. Directories within - the path are not affected by this setting. This might be - in conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits set.' - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are supported.' + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + key: + description: key is the label key that the selector + applies to. type: string - fieldPath: - description: Path of the field to select in the - specified API version. + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array required: - - fieldPath + - key + - operator type: object - mode: - description: 'Optional: mode bits used to set permissions - on this file, must be an octal value between 0000 - and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative path name - of the file to be created. Must not be absolute or - contain the ''..'' path. Must be utf-8 encoded. The - first item of the relative path must not start with - ''..''' + type: array + matchLabels: + additionalProperties: type: string - resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - requests.cpu and requests.memory) are currently supported.' - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. + properties: + name: + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret + type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret + type: string + required: + - name + type: object + required: + - labelsSelector type: object - emptyDir: - description: 'emptyDir represents a temporary directory that shares - a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + required: + - persistentVolumeClaim + - target + type: object + logfile: + description: the policy for logfile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, only + support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + persistentVolumeClaim: + description: refer to PersistentVolumeClaim and the backup data + will be stored in the corresponding persistent volume. properties: - medium: - description: 'medium represents what type of storage medium - should back this directory. The default is "" which means - to use the node''s default medium. Must be an empty string - (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - type: string - sizeLimit: + createPolicy: + default: IfNotPresent + description: 'createPolicy defines the policy for creating + the PersistentVolumeClaim, enum values: - Never: do nothing + if the PersistentVolumeClaim not exists. - IfNotPresent: + create the PersistentVolumeClaim if not present and the + accessModes only contains ''ReadWriteMany''.' + enum: + - IfNotPresent + - Never + type: string + initCapacity: anyOf: - type: integer - type: string - description: 'sizeLimit is the total amount of local storage - required for this EmptyDir volume. The size limit is also - applicable for memory medium. The maximum usage on memory - medium EmptyDir would be the minimum value between the SizeLimit - specified here and the sum of memory limits of all containers - in a pod. The default is nil which means that the limit - is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + description: initCapacity represents the init storage size + of the PersistentVolumeClaim which should be created if + not exist. and the default value is 100Gi if it is empty. pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + name: + description: the name of the PersistentVolumeClaim. + type: string + persistentVolumeConfigMap: + description: 'persistentVolumeConfigMap references the configmap + which contains a persistentVolume template. key must be + "persistentVolume" and value is the "PersistentVolume" struct. + support the following built-in Objects: - $(GENERATE_NAME): + generate a specific format "pvcName-pvcNamespace". if the + PersistentVolumeClaim not exists and CreatePolicy is "IfNotPresent", + the controller will create it by this template. this is + a mutually exclusive setting with "storageClassName".' + properties: + name: + description: the name of the persistentVolume ConfigMap. + type: string + namespace: + description: the namespace of the persistentVolume ConfigMap. + type: string + required: + - name + - namespace + type: object + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + required: + - name type: object - ephemeral: - description: "ephemeral represents a volume that is handled by - a cluster storage driver. The volume's lifecycle is tied to - the pod that defines it - it will be created before the pod - starts, and deleted when the pod is removed. \n Use this if: - a) the volume is only needed while the pod runs, b) features - of normal volumes like restoring from snapshot or capacity tracking - are needed, c) the storage driver is specified through a storage - class, and d) the storage driver supports dynamic volume provisioning - through a PersistentVolumeClaim (see EphemeralVolumeSource for - more information on the connection between this volume type - and PersistentVolumeClaim). \n Use PersistentVolumeClaim or - one of the vendor-specific APIs for volumes that persist for - longer than the lifecycle of an individual pod. \n Use CSI for - light-weight local ephemeral volumes if the CSI driver is meant - to be used that way - see the documentation of the driver for - more information. \n A pod can use both types of ephemeral volumes - and persistent volumes at the same time." + target: + description: target database cluster for backup. properties: - volumeClaimTemplate: - description: "Will be used to create a stand-alone PVC to - provision the volume. The pod in which this EphemeralVolumeSource - is embedded will be the owner of the PVC, i.e. the PVC will - be deleted together with the pod. The name of the PVC will - be `-` where `` is the - name from the `PodSpec.Volumes` array entry. Pod validation - will reject the pod if the concatenated name is not valid - for a PVC (for example, too long). \n An existing PVC with - that name that is not owned by the pod will *not* be used - for the pod to avoid using an unrelated volume by mistake. - Starting the pod is then blocked until the unrelated PVC - is removed. If such a pre-created PVC is meant to be used - by the pod, the PVC has to updated with an owner reference - to the pod once the pod exists. Normally this should not - be necessary, but it may be useful when manually reconstructing - a broken cluster. \n This field is read-only and no changes - will be made by Kubernetes to the PVC after it has been - created. \n Required, must not be nil." + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. properties: - metadata: - description: May contain labels and annotations that will - be copied into the PVC when creating it. No other fields - are allowed and will be rejected during validation. - properties: - annotations: - additionalProperties: - type: string - type: object - finalizers: - items: - type: string - type: array - labels: - additionalProperties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. type: string - type: object - name: - type: string - namespace: - type: string - type: object - spec: - description: The specification for the PersistentVolumeClaim. - The entire content is copied unchanged into the PVC - that gets created from this template. The same fields - as in a PersistentVolumeClaim are also valid here. - properties: - accessModes: - description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. type: string - type: array - dataSource: - description: 'dataSource field can be used to specify - either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the - provisioner or an external controller can support - the specified data source, it will create a new - volume based on the contents of the specified data - source. When the AnyVolumeDataSource feature gate - is enabled, dataSource contents will be copied to - dataSourceRef, and dataSourceRef contents will be - copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, - then dataSourceRef will not be copied to dataSource.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name - type: object - dataSourceRef: - description: 'dataSourceRef specifies the object from - which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a - non-empty API group (non core object) or a PersistentVolumeClaim - object. When this field is specified, volume binding - will only succeed if the type of the specified object - matches some installed volume populator or dynamic - provisioner. This field will replace the functionality - of the dataSource field and as such if both fields - are non-empty, they must have the same value. For - backwards compatibility, when namespace isn''t specified - in dataSourceRef, both fields (dataSource and dataSourceRef) - will be set to the same value automatically if one - of them is empty and the other is non-empty. When - namespace is specified in dataSourceRef, dataSource - isn''t set to the same value and must be empty. - There are three important differences between dataSource - and dataSourceRef: * While dataSource only allows - two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim - objects. * While dataSource ignores disallowed values - (dropping them), dataSourceRef preserves all values, - and generates an error if a disallowed value is - specified. * While dataSource only allows local - objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource - feature gate to be enabled. (Alpha) Using the namespace - field of dataSourceRef requires the CrossNamespaceVolumeDataSource - feature gate to be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: - description: Namespace is the namespace of resource - being referenced Note that when a namespace - is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace - to allow that namespace's owner to accept the - reference. See the ReferenceGrant documentation - for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate - to be enabled. + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: type: string - required: - - kind - - name - type: object - resources: - description: 'resources represents the minimum resources - the volume should have. If RecoverVolumeExpansionFailure - feature is enabled users are allowed to specify - resource requirements that are lower than previous - value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, - defined in spec.resourceClaims, that are used - by this container. \n This is an alpha field - and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable." - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of - one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes - that resource available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is - omitted for a container, it defaults to Limits - if that is explicitly specified, otherwise to - an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement is - a selector that contains values, a key, and - an operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If - the operator is Exists or DoesNotExist, - the values array must be empty. This array - is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - storageClassName: - description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume - is required by the claim. Value of Filesystem is - implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference to - the PersistentVolume backing this claim. - type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. type: object - required: - - spec type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is attached - to a kubelet's host machine and then exposed to the pod. - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. TODO: how do we prevent errors in the filesystem - from compromising the machine' - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: 'wwids Optional: FC volume world wide identifiers - (wwids) Either wwids or combination of targetWWNs and lun - must be set, but not both simultaneously.' - items: - type: string - type: array - type: object - flexVolume: - description: flexVolume represents a generic volume resource that - is provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for this - volume. - type: string - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends - on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' - type: object - readOnly: - description: 'readOnly is Optional: defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - secretRef: - description: 'secretRef is Optional: secretRef is reference - to the secret object containing sensitive information to - pass to the plugin scripts. This may be empty if no secret - object is specified. If the secret object contains more - than one secret, all secrets are passed to the plugin scripts.' + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. 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?' + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret + type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret type: string + required: + - name type: object required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to a - kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: datasetName is Name of the dataset stored as - metadata -> name on the dataset for Flocker should be considered - as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string + - labelsSelector type: object - gcePersistentDisk: - description: 'gcePersistentDisk represents a GCE Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + required: + - persistentVolumeClaim + - target + type: object + retention: + description: retention describe how long the Backup should be retained. + if not set, will be retained forever. + properties: + ttl: + description: ttl is a time string ending with the 'd'|'D'|'h'|'H' + character to describe how long the Backup should be retained. + if not set, will be retained forever. + pattern: ^\d+[d|D|h|H]$ + type: string + type: object + schedule: + description: schedule policy for backup. + properties: + datafile: + description: schedule policy for datafile backup. properties: - fsType: - description: 'fsType is filesystem type of the volume that - you want to mount. Tip: Ensure that the filesystem type - is supported by the host operating system. Examples: "ext4", - "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty). More - info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - format: int32 - type: integer - pdName: - description: 'pdName is unique name of the PD resource in - GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + enable: + description: enable or disable the schedule. type: boolean required: - - pdName + - cronExpression + - enable type: object - gitRepo: - description: 'gitRepo represents a git repository at a particular - revision. DEPRECATED: GitRepo is deprecated. To provision a - container with a git repo, mount an EmptyDir into an InitContainer - that clones the repo using git, then mount the EmptyDir into - the Pod''s container.' + logfile: + description: schedule policy for logfile backup. properties: - directory: - description: directory is the target directory name. Must - not contain or start with '..'. If '.' is supplied, the - volume directory will be the git repository. Otherwise, - if specified, the volume will contain the git repository - in the subdirectory with the given name. + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: 'glusterfs represents a Glusterfs mount on the host - that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' - properties: - endpoints: - description: 'endpoints is the endpoint name that details - Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - path: - description: 'path is the Glusterfs volume path. More info: - https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - readOnly: - description: 'readOnly here will force the Glusterfs volume - to be mounted with read-only permissions. Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + enable: + description: enable or disable the schedule. type: boolean required: - - endpoints - - path + - cronExpression + - enable type: object - hostPath: - description: 'hostPath represents a pre-existing file or directory - on the host machine that is directly exposed to the container. - This is generally used for system agents or other privileged - things that are allowed to see the host machine. Most containers - will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- TODO(jonesdl) We need to restrict who can use host directory - mounts and who can/can not mount host directories as read/write.' + snapshot: + description: schedule policy for snapshot backup. properties: - path: - description: 'path of the directory on the host. If the path - is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - type: - description: 'type for HostPath Volume Defaults to "" More - info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string + enable: + description: enable or disable the schedule. + type: boolean required: - - path + - cronExpression + - enable type: object - iscsi: - description: 'iscsi represents an ISCSI Disk resource that is - attached to a kubelet''s host machine and then exposed to the - pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + type: object + snapshot: + description: the policy for snapshot backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + hooks: + description: execute hook commands for backup. properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - initiatorName: - description: initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, - new iSCSI interface : will be - created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. + containerName: + description: which container can exec command type: string - iscsiInterface: - description: iscsiInterface is the interface Name that uses - an iSCSI transport. Defaults to 'default' (tcp). + image: + description: exec command with image type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: portals is the iSCSI Target Portal List. The - portal is either an IP or ip_addr:port if the port is other - than default (typically TCP ports 860 and 3260). + postCommands: + description: post backup to perform commands + items: + type: string + type: array + preCommands: + description: pre backup to perform commands items: type: string type: array - readOnly: - description: readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication - 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 - targetPortal: - description: targetPortal is iSCSI Target Portal. The Portal - is either an IP or ip_addr:port if the port is other than - default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: 'name of the volume. Must be a DNS_LABEL and unique - within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - nfs: - description: 'nfs represents an NFS mount on the host that shares - a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - properties: - path: - description: 'path that is exported by the NFS server. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - readOnly: - description: 'readOnly here will force the NFS export to be - mounted with read-only permissions. Defaults to false. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: boolean - server: - description: 'server is the hostname or IP address of the - NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: 'persistentVolumeClaimVolumeSource represents a reference - to a PersistentVolumeClaim in the same namespace. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - properties: - claimName: - description: 'claimName is the name of a PersistentVolumeClaim - in the same namespace as the pod using this volume. More - info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - type: string - readOnly: - description: readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating - system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target database cluster for backup. properties: - defaultMode: - description: defaultMode are the mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Directories within the path are not - affected by this setting. This might be in conflict with - other options that affect the file mode, like fsGroup, and - the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced ConfigMap - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the ConfigMap, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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?' + key: + description: key is the label key that the selector + applies to. type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing the - pod field - properties: - fieldRef: - description: 'Required: Selects a field of - the pod: only annotations, labels, name - and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, defaults - to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to - set permissions on this file, must be an - octal value between 0000 and 0777 or a decimal - value between 0 and 511. YAML accepts both - octal and decimal values, JSON requires - decimal values for mode bits. If not specified, - the volume defaultMode will be used. This - might be in conflict with other options - that affect the file mode, like fsGroup, - and the result can be other mode bits set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' path. - Must be utf-8 encoded. The first item of - the relative path must not start with ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: - only resources limits and requests (limits.cpu, - limits.memory, requests.cpu and requests.memory) - are currently supported.' - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced Secret - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the Secret, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object + type: string type: array - 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 - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean - type: object - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project - properties: - audience: - description: audience is the intended audience of - the token. A recipient of a token must identify - itself with an identifier specified in the audience - of the token, and otherwise should reject the - token. The audience defaults to the identifier - of the apiserver. - type: string - expirationSeconds: - description: expirationSeconds is the requested - duration of validity of the service account token. - As the token approaches expiration, the kubelet - volume plugin will proactively rotate the service - account token. The kubelet will start trying to - rotate the token if the token is older than 80 - percent of its time to live or if the token is - older than 24 hours.Defaults to 1 hour and must - be at least 10 minutes. - format: int64 - type: integer - path: - description: path is the path relative to the mount - point of the file to project the token into. - type: string required: - - path + - key + - operator type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host that - shares a pod's lifetime - properties: - group: - description: group to map volume access to Default is no group - type: string - readOnly: - description: readOnly here will force the Quobyte volume to - be mounted with read-only permissions. Defaults to false. - type: boolean - registry: - description: registry represents a single or multiple Quobyte - Registry services specified as a string as host:port pair - (multiple entries are separated with commas) which acts - as the central registry for volumes - type: string - tenant: - description: tenant owning the given Quobyte volume in the - Backend Used with dynamically provisioned Quobyte volumes, - value is set by the plugin - type: string - user: - description: user to map volume access to Defaults to serivceaccount - user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: 'rbd represents a Rados Block Device mount on the - host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - image: - description: 'image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - keyring: - description: 'keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - monitors: - description: 'monitors is a collection of Ceph monitors. More - info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - items: - type: string - type: array - pool: - description: 'pool is the rados pool name. Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: boolean - secretRef: - description: 'secretRef is name of the authentication secret - for RBDUser. If provided overrides keyring. Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - 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: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object type: object - user: - description: 'user is the rados user name. Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO API - Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO Protection - Domain for the configured storage. - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef references to the secret for ScaleIO - user and other sensitive information. If this is not provided, - Login operation will fail. + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. 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?' + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string - type: object - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage for - a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as configured - in ScaleIO. - type: string - volumeName: - description: volumeName is the name of a volume already created - in the ScaleIO system that is associated with this volume - source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: 'secret represents a secret that should populate - this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items If unspecified, each key-value pair in - the Data field of the referenced Secret will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - Secret, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: 'secretName is the name of the secret in the - pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef specifies the secret to use for obtaining - the StorageOS API credentials. If not specified, default - values will be attempted. - 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?' + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret + type: string + required: + - name type: object - volumeName: - description: volumeName is the human-readable name of the - StorageOS volume. Volume names are only unique within a - namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of the volume - within StorageOS. If no namespace is specified then the - Pod's namespace will be used. This allows the Kubernetes - name scoping to be mirrored within StorageOS for tighter - integration. Set VolumeName to any name to override the - default behaviour. Set to "default" if you are not using - namespaces within StorageOS. Namespaces that do not pre-exist - within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fsType is filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based Management - (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - schedule: - description: The schedule in Cron format, the timezone is in UTC. - see https://en.wikipedia.org/wiki/Cron. - type: string - target: - description: database cluster service - properties: - labelsSelector: - description: LabelSelector is used to find matching pods. Pods - that match this label selector are counted to determine the - number of pods in their corresponding topology domain. - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, NotIn, - Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If - the operator is In or NotIn, the values array must - be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced - during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A - single {key,value} in the matchLabels map is equivalent - to an element of matchExpressions, whose key field is "key", - the operator is "In", and the values array contains only - "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-preserve-unknown-fields: true - secret: - description: Secret is used to connect to the target database - cluster. If not set, secret will be inherited from backup policy - template. if still not set, the controller will check if any - system account for dataprotection has been created. - properties: - name: - description: the secret name - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - passwordKeyword: - description: PasswordKeyword the map keyword of the password - in the connection credential secret - type: string - userKeyword: - description: UserKeyword the map keyword of the user in the - connection credential secret - type: string required: - - name + - labelsSelector type: object required: - - labelsSelector + - target type: object - ttl: - description: TTL is a time.Duration-parseable string describing how - long the Backup should be retained for. - type: string - required: - - remoteVolume - - target type: object status: description: BackupPolicyStatus defines the observed state of BackupPolicy @@ -1749,22 +621,25 @@ spec: description: the reason if backup policy check failed. type: string lastScheduleTime: - description: Information when was the last time the job was successfully + description: information when was the last time the job was successfully scheduled. format: date-time type: string lastSuccessfulTime: - description: Information when was the last time the job successfully + description: information when was the last time the job successfully completed. format: date-time type: string + observedGeneration: + description: observedGeneration is the most recent generation observed + for this BackupPolicy. It corresponds to the Cluster's generation, + which is updated on mutation by the API Server. + format: int64 + type: integer phase: - description: 'backup policy phase valid value: available, failed, - new.' + description: 'backup policy phase valid value: Available, Failed.' enum: - - New - Available - - InProgress - Failed type: string type: object diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml deleted file mode 100644 index ea15b74c6..000000000 --- a/config/crd/bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml +++ /dev/null @@ -1,144 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.9.0 - creationTimestamp: null - name: backuppolicytemplates.dataprotection.kubeblocks.io -spec: - group: dataprotection.kubeblocks.io - names: - categories: - - kubeblocks - kind: BackupPolicyTemplate - listKind: BackupPolicyTemplateList - plural: backuppolicytemplates - singular: backuppolicytemplate - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: BackupPolicyTemplate is the Schema for the BackupPolicyTemplates - API (defined by provider) - 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: BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate - properties: - backupStatusUpdates: - description: define how to update metadata for backup status. - items: - properties: - containerName: - description: which container name that kubectl can execute. - type: string - path: - description: 'specify the json path of backup object for patch. - example: manifests.backupLog -- means patch the backup json - path of status.manifests.backupLog.' - type: string - script: - description: the shell Script commands to collect backup status - metadata. The script must exist in the container of ContainerName - and the output format must be set to JSON. Note that outputting - to stderr may cause the result format to not be in JSON. - type: string - updateStage: - description: 'when to update the backup status, pre: before - backup, post: after backup' - enum: - - pre - - post - type: string - type: object - type: array - backupToolName: - description: which backup tool to perform database backup, only support - one tool. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - credentialKeyword: - description: CredentialKeyword determines backupTool connection credential - keyword in secret. the backupTool gets the credentials according - to the user and password keyword defined by secret - properties: - passwordKeyword: - default: password - description: PasswordKeyword the map keyword of the password in - the connection credential secret - type: string - userKeyword: - default: username - description: UserKeyword the map keyword of the user in the connection - credential secret - type: string - type: object - hooks: - description: execute hook commands for backup. - properties: - containerName: - description: which container can exec command - type: string - image: - description: exec command with image - type: string - postCommands: - description: post backup to perform commands - items: - type: string - type: array - preCommands: - description: pre backup to perform commands - items: - type: string - type: array - type: object - onFailAttempted: - description: limit count of backup stop retries on fail. if unset, - retry unlimit attempted. - format: int32 - type: integer - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - ttl: - description: TTL is a time.Duration-parseable string describing how - long the Backup should be retained for. - type: string - required: - - backupToolName - type: object - status: - description: BackupPolicyTemplateStatus defines the observed state of - BackupPolicyTemplate - properties: - failureReason: - type: string - phase: - description: BackupPolicyTemplatePhase defines phases for BackupPolicyTemplate - CR. - enum: - - New - - Available - - InProgress - - Failed - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml index 188fb3fb3..d8ce45b88 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backups.yaml @@ -39,7 +39,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Backup is the Schema for the backups API (defined by User) + description: Backup is the Schema for the backups API (defined by User). properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -54,37 +54,33 @@ spec: metadata: type: object spec: - description: BackupSpec defines the desired state of Backup + description: BackupSpec defines the desired state of Backup. properties: backupPolicyName: - description: which backupPolicy to perform this backup + description: Which backupPolicy is applied to perform this backup pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string backupType: - default: full - description: Backup Type. full or incremental or snapshot. if unset, - default is full. + default: datafile + description: Backup Type. datafile or logfile or snapshot. If not + set, datafile is the default type. enum: - - full - - incremental + - datafile + - logfile - snapshot type: string parentBackupName: description: if backupType is incremental, parentBackupName is required. type: string - ttl: - description: ttl is a time.Duration-parsable string describing how - long the Backup should be retained for. - type: string required: - backupPolicyName - backupType type: object status: - description: BackupStatus defines the observed state of Backup + description: BackupStatus defines the observed state of Backup. properties: backupToolName: - description: backupToolName referenced backup tool name. + description: backupToolName references the backup tool name. type: string completionTimestamp: description: Date/time when the backup finished being processed. @@ -101,26 +97,26 @@ spec: format: date-time type: string failureReason: - description: the reason if backup failed. + description: The reason for a backup failure. type: string manifests: - description: manifests determines the backup metadata info + description: manifests determines the backup metadata info. properties: backupLog: description: backupLog records startTime and stopTime of data - logging + logging. properties: startTime: - description: startTime record start time of data logging + description: startTime records the start time of data logging. format: date-time type: string stopTime: - description: stopTime record start time of data logging + description: stopTime records the stop time of data logging. format: date-time type: string type: object backupSnapshot: - description: snapshot records the volume snapshot metadata + description: snapshot records the volume snapshot metadata. properties: volumeSnapshotContentName: description: volumeSnapshotContentName specifies the name @@ -130,35 +126,32 @@ spec: in Kubernetes. This field is immutable. type: string volumeSnapshotName: - description: volumeSnapshotName record the volumeSnapshot - name + description: volumeSnapshotName records the volumeSnapshot + name. type: string type: object backupTool: description: backupTool records information about backup files generated by the backup tool. properties: - CheckPoint: - description: backup check point, for incremental backup. - type: string - backupToolName: - description: backupToolName referenced backup tool name. + checkpoint: + description: backup checkpoint, for incremental backup. type: string - checkSum: + checksum: description: checksum of backup file, generated by md5 or - sha1 or sha256 + sha1 or sha256. type: string filePath: description: filePath records the file path of backup. type: string uploadTotalSize: - description: backup upload total size string with capacity + description: Backup upload total size. A string with capacity units in the form of "1Gi", "1Mi", "1Ki". type: string type: object target: description: target records the target cluster metadata string, - which are in JSON format. + which is in JSON format. type: string userContext: additionalProperties: @@ -168,7 +161,10 @@ spec: type: object type: object parentBackupName: - description: record parentBackupName if backupType is incremental. + description: Records parentBackupName if backupType is incremental. + type: string + persistentVolumeClaimName: + description: remoteVolume saves the backup data. type: string phase: description: BackupPhase The current phase. Valid values are New, @@ -179,1542 +175,13 @@ spec: - Completed - Failed type: string - remoteVolume: - description: remoteVolume saves the backup data. - properties: - awsElasticBlockStore: - description: 'awsElasticBlockStore represents an AWS Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty).' - format: int32 - type: integer - readOnly: - description: 'readOnly value true will force the readOnly - setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: boolean - volumeID: - description: 'volumeID is unique ID of the persistent disk - resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob storage - type: string - fsType: - description: fsType is Filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple blob - disks per storage account Dedicated: single blob disk per - storage account Managed: azure managed data disk (only - in managed availability set). defaults to shared' - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime - properties: - monitors: - description: 'monitors is Required: Monitors is a collection - of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' - type: string - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: boolean - secretFile: - description: 'secretFile is Optional: SecretFile is the path - to key ring for User, default is /etc/ceph/user.secret More - info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - secretRef: - description: 'secretRef is Optional: SecretRef is reference - to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - 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 - user: - description: 'user is optional: User is the rados user name, - default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - required: - - monitors - type: object - cinder: - description: 'cinder represents a cinder volume attached and mounted - on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to - be "ext4" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - readOnly: - description: 'readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. More - info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: boolean - secretRef: - description: 'secretRef is optional: points to a secret object - containing parameters used to connect to OpenStack.' - 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 - volumeID: - description: 'volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: 'defaultMode is optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items if unspecified, each key-value pair in - the Data field of the referenced ConfigMap will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - ConfigMap, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean - type: object - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). - properties: - driver: - description: driver is the name of the CSI driver that handles - this volume. Consult with your admin for the correct name - as registered in the cluster. - type: string - fsType: - description: fsType to mount. Ex. "ext4", "xfs", "ntfs". If - not provided, the empty value is passed to the associated - CSI driver which will determine the default filesystem to - apply. - type: string - nodePublishSecretRef: - description: nodePublishSecretRef is a reference to the secret - object containing sensitive information to pass to the CSI - driver to complete the CSI NodePublishVolume and NodeUnpublishVolume - calls. This field is optional, and may be empty if no secret - is required. If the secret object contains more than one - secret, all secret references are passed. - 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 - readOnly: - description: readOnly specifies a read-only configuration - for the volume. Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: volumeAttributes stores driver-specific properties - that are passed to the CSI driver. Consult your driver's - documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: - description: 'Optional: mode bits to use on created files - by default. Must be a Optional: mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Defaults to 0644. Directories within - the path are not affected by this setting. This might be - in conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits set.' - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to set permissions - on this file, must be an octal value between 0000 - and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative path name - of the file to be created. Must not be absolute or - contain the ''..'' path. Must be utf-8 encoded. The - first item of the relative path must not start with - ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - requests.cpu and requests.memory) are currently supported.' - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - emptyDir: - description: 'emptyDir represents a temporary directory that shares - a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - properties: - medium: - description: 'medium represents what type of storage medium - should back this directory. The default is "" which means - to use the node''s default medium. Must be an empty string - (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - type: string - sizeLimit: - anyOf: - - type: integer - - type: string - description: 'sizeLimit is the total amount of local storage - required for this EmptyDir volume. The size limit is also - applicable for memory medium. The maximum usage on memory - medium EmptyDir would be the minimum value between the SizeLimit - specified here and the sum of memory limits of all containers - in a pod. The default is nil which means that the limit - is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: "ephemeral represents a volume that is handled by - a cluster storage driver. The volume's lifecycle is tied to - the pod that defines it - it will be created before the pod - starts, and deleted when the pod is removed. \n Use this if: - a) the volume is only needed while the pod runs, b) features - of normal volumes like restoring from snapshot or capacity tracking - are needed, c) the storage driver is specified through a storage - class, and d) the storage driver supports dynamic volume provisioning - through a PersistentVolumeClaim (see EphemeralVolumeSource for - more information on the connection between this volume type - and PersistentVolumeClaim). \n Use PersistentVolumeClaim or - one of the vendor-specific APIs for volumes that persist for - longer than the lifecycle of an individual pod. \n Use CSI for - light-weight local ephemeral volumes if the CSI driver is meant - to be used that way - see the documentation of the driver for - more information. \n A pod can use both types of ephemeral volumes - and persistent volumes at the same time." - properties: - volumeClaimTemplate: - description: "Will be used to create a stand-alone PVC to - provision the volume. The pod in which this EphemeralVolumeSource - is embedded will be the owner of the PVC, i.e. the PVC will - be deleted together with the pod. The name of the PVC will - be `-` where `` is the - name from the `PodSpec.Volumes` array entry. Pod validation - will reject the pod if the concatenated name is not valid - for a PVC (for example, too long). \n An existing PVC with - that name that is not owned by the pod will *not* be used - for the pod to avoid using an unrelated volume by mistake. - Starting the pod is then blocked until the unrelated PVC - is removed. If such a pre-created PVC is meant to be used - by the pod, the PVC has to updated with an owner reference - to the pod once the pod exists. Normally this should not - be necessary, but it may be useful when manually reconstructing - a broken cluster. \n This field is read-only and no changes - will be made by Kubernetes to the PVC after it has been - created. \n Required, must not be nil." - properties: - metadata: - description: May contain labels and annotations that will - be copied into the PVC when creating it. No other fields - are allowed and will be rejected during validation. - properties: - annotations: - additionalProperties: - type: string - type: object - finalizers: - items: - type: string - type: array - labels: - additionalProperties: - type: string - type: object - name: - type: string - namespace: - type: string - type: object - spec: - description: The specification for the PersistentVolumeClaim. - The entire content is copied unchanged into the PVC - that gets created from this template. The same fields - as in a PersistentVolumeClaim are also valid here. - properties: - accessModes: - description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: - type: string - type: array - dataSource: - description: 'dataSource field can be used to specify - either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the - provisioner or an external controller can support - the specified data source, it will create a new - volume based on the contents of the specified data - source. When the AnyVolumeDataSource feature gate - is enabled, dataSource contents will be copied to - dataSourceRef, and dataSourceRef contents will be - copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, - then dataSourceRef will not be copied to dataSource.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name - type: object - dataSourceRef: - description: 'dataSourceRef specifies the object from - which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a - non-empty API group (non core object) or a PersistentVolumeClaim - object. When this field is specified, volume binding - will only succeed if the type of the specified object - matches some installed volume populator or dynamic - provisioner. This field will replace the functionality - of the dataSource field and as such if both fields - are non-empty, they must have the same value. For - backwards compatibility, when namespace isn''t specified - in dataSourceRef, both fields (dataSource and dataSourceRef) - will be set to the same value automatically if one - of them is empty and the other is non-empty. When - namespace is specified in dataSourceRef, dataSource - isn''t set to the same value and must be empty. - There are three important differences between dataSource - and dataSourceRef: * While dataSource only allows - two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim - objects. * While dataSource ignores disallowed values - (dropping them), dataSourceRef preserves all values, - and generates an error if a disallowed value is - specified. * While dataSource only allows local - objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource - feature gate to be enabled. (Alpha) Using the namespace - field of dataSourceRef requires the CrossNamespaceVolumeDataSource - feature gate to be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: - description: Namespace is the namespace of resource - being referenced Note that when a namespace - is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace - to allow that namespace's owner to accept the - reference. See the ReferenceGrant documentation - for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate - to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: 'resources represents the minimum resources - the volume should have. If RecoverVolumeExpansionFailure - feature is enabled users are allowed to specify - resource requirements that are lower than previous - value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, - defined in spec.resourceClaims, that are used - by this container. \n This is an alpha field - and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable." - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of - one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes - that resource available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is - omitted for a container, it defaults to Limits - if that is explicitly specified, otherwise to - an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement is - a selector that contains values, a key, and - an operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If - the operator is Exists or DoesNotExist, - the values array must be empty. This array - is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - storageClassName: - description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume - is required by the claim. Value of Filesystem is - implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference to - the PersistentVolume backing this claim. - type: string - type: object - required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is attached - to a kubelet's host machine and then exposed to the pod. - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. TODO: how do we prevent errors in the filesystem - from compromising the machine' - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: 'wwids Optional: FC volume world wide identifiers - (wwids) Either wwids or combination of targetWWNs and lun - must be set, but not both simultaneously.' - items: - type: string - type: array - type: object - flexVolume: - description: flexVolume represents a generic volume resource that - is provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for this - volume. - type: string - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends - on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' - type: object - readOnly: - description: 'readOnly is Optional: defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - secretRef: - description: 'secretRef is Optional: secretRef is reference - to the secret object containing sensitive information to - pass to the plugin scripts. This may be empty if no secret - object is specified. If the secret object contains more - than one secret, all secrets are passed to the plugin scripts.' - 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 - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to a - kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: datasetName is Name of the dataset stored as - metadata -> name on the dataset for Flocker should be considered - as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: 'gcePersistentDisk represents a GCE Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - properties: - fsType: - description: 'fsType is filesystem type of the volume that - you want to mount. Tip: Ensure that the filesystem type - is supported by the host operating system. Examples: "ext4", - "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty). More - info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - format: int32 - type: integer - pdName: - description: 'pdName is unique name of the PD resource in - GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - type: boolean - required: - - pdName - type: object - gitRepo: - description: 'gitRepo represents a git repository at a particular - revision. DEPRECATED: GitRepo is deprecated. To provision a - container with a git repo, mount an EmptyDir into an InitContainer - that clones the repo using git, then mount the EmptyDir into - the Pod''s container.' - properties: - directory: - description: directory is the target directory name. Must - not contain or start with '..'. If '.' is supplied, the - volume directory will be the git repository. Otherwise, - if specified, the volume will contain the git repository - in the subdirectory with the given name. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: 'glusterfs represents a Glusterfs mount on the host - that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' - properties: - endpoints: - description: 'endpoints is the endpoint name that details - Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - path: - description: 'path is the Glusterfs volume path. More info: - https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - readOnly: - description: 'readOnly here will force the Glusterfs volume - to be mounted with read-only permissions. Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: 'hostPath represents a pre-existing file or directory - on the host machine that is directly exposed to the container. - This is generally used for system agents or other privileged - things that are allowed to see the host machine. Most containers - will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- TODO(jonesdl) We need to restrict who can use host directory - mounts and who can/can not mount host directories as read/write.' - properties: - path: - description: 'path of the directory on the host. If the path - is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - type: - description: 'type for HostPath Volume Defaults to "" More - info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - required: - - path - type: object - iscsi: - description: 'iscsi represents an ISCSI Disk resource that is - attached to a kubelet''s host machine and then exposed to the - pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - initiatorName: - description: initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, - new iSCSI interface : will be - created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: iscsiInterface is the interface Name that uses - an iSCSI transport. Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: portals is the iSCSI Target Portal List. The - portal is either an IP or ip_addr:port if the port is other - than default (typically TCP ports 860 and 3260). - items: - type: string - type: array - readOnly: - description: readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication - 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 - targetPortal: - description: targetPortal is iSCSI Target Portal. The Portal - is either an IP or ip_addr:port if the port is other than - default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: 'name of the volume. Must be a DNS_LABEL and unique - within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - nfs: - description: 'nfs represents an NFS mount on the host that shares - a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - properties: - path: - description: 'path that is exported by the NFS server. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - readOnly: - description: 'readOnly here will force the NFS export to be - mounted with read-only permissions. Defaults to false. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: boolean - server: - description: 'server is the hostname or IP address of the - NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: 'persistentVolumeClaimVolumeSource represents a reference - to a PersistentVolumeClaim in the same namespace. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - properties: - claimName: - description: 'claimName is the name of a PersistentVolumeClaim - in the same namespace as the pod using this volume. More - info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - type: string - readOnly: - description: readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating - system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API - properties: - defaultMode: - description: defaultMode are the mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Directories within the path are not - affected by this setting. This might be in conflict with - other options that affect the file mode, like fsGroup, and - the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced ConfigMap - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the ConfigMap, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing the - pod field - properties: - fieldRef: - description: 'Required: Selects a field of - the pod: only annotations, labels, name - and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, defaults - to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to - set permissions on this file, must be an - octal value between 0000 and 0777 or a decimal - value between 0 and 511. YAML accepts both - octal and decimal values, JSON requires - decimal values for mode bits. If not specified, - the volume defaultMode will be used. This - might be in conflict with other options - that affect the file mode, like fsGroup, - and the result can be other mode bits set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' path. - Must be utf-8 encoded. The first item of - the relative path must not start with ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: - only resources limits and requests (limits.cpu, - limits.memory, requests.cpu and requests.memory) - are currently supported.' - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced Secret - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the Secret, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean - type: object - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project - properties: - audience: - description: audience is the intended audience of - the token. A recipient of a token must identify - itself with an identifier specified in the audience - of the token, and otherwise should reject the - token. The audience defaults to the identifier - of the apiserver. - type: string - expirationSeconds: - description: expirationSeconds is the requested - duration of validity of the service account token. - As the token approaches expiration, the kubelet - volume plugin will proactively rotate the service - account token. The kubelet will start trying to - rotate the token if the token is older than 80 - percent of its time to live or if the token is - older than 24 hours.Defaults to 1 hour and must - be at least 10 minutes. - format: int64 - type: integer - path: - description: path is the path relative to the mount - point of the file to project the token into. - type: string - required: - - path - type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host that - shares a pod's lifetime - properties: - group: - description: group to map volume access to Default is no group - type: string - readOnly: - description: readOnly here will force the Quobyte volume to - be mounted with read-only permissions. Defaults to false. - type: boolean - registry: - description: registry represents a single or multiple Quobyte - Registry services specified as a string as host:port pair - (multiple entries are separated with commas) which acts - as the central registry for volumes - type: string - tenant: - description: tenant owning the given Quobyte volume in the - Backend Used with dynamically provisioned Quobyte volumes, - value is set by the plugin - type: string - user: - description: user to map volume access to Defaults to serivceaccount - user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: 'rbd represents a Rados Block Device mount on the - host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - image: - description: 'image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - keyring: - description: 'keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - monitors: - description: 'monitors is a collection of Ceph monitors. More - info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - items: - type: string - type: array - pool: - description: 'pool is the rados pool name. Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: boolean - secretRef: - description: 'secretRef is name of the authentication secret - for RBDUser. If provided overrides keyring. Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - 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 - user: - description: 'user is the rados user name. Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO API - Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO Protection - Domain for the configured storage. - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef references to the secret for ScaleIO - user and other sensitive information. If this is not provided, - Login operation will fail. - 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 - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage for - a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as configured - in ScaleIO. - type: string - volumeName: - description: volumeName is the name of a volume already created - in the ScaleIO system that is associated with this volume - source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: 'secret represents a secret that should populate - this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items If unspecified, each key-value pair in - the Data field of the referenced Secret will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - Secret, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: 'secretName is the name of the secret in the - pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef specifies the secret to use for obtaining - the StorageOS API credentials. If not specified, default - values will be attempted. - 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 - volumeName: - description: volumeName is the human-readable name of the - StorageOS volume. Volume names are only unique within a - namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of the volume - within StorageOS. If no namespace is specified then the - Pod's namespace will be used. This allows the Kubernetes - name scoping to be mirrored within StorageOS for tighter - integration. Set VolumeName to any name to override the - default behaviour. Set to "default" if you are not using - namespaces within StorageOS. Namespaces that do not pre-exist - within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fsType is filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based Management - (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object startTimestamp: description: Date/time when the backup started being processed. format: date-time type: string totalSize: - description: backup total size string with capacity units in the form - of "1Gi", "1Mi", "1Ki". + description: Backup total size. A string with capacity units in the + form of "1Gi", "1Mi", "1Ki". type: string type: object type: object diff --git a/config/crd/bases/dataprotection.kubeblocks.io_backuptools.yaml b/config/crd/bases/dataprotection.kubeblocks.io_backuptools.yaml index 3f84de80f..3fcd126fe 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_backuptools.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_backuptools.yaml @@ -286,6 +286,13 @@ spec: type: object type: object x-kubernetes-preserve-unknown-fields: true + type: + default: file + description: the type of backup tool, file or pitr + enum: + - file + - pitr + type: string required: - backupCommands - image diff --git a/config/crd/bases/dataprotection.kubeblocks.io_restorejobs.yaml b/config/crd/bases/dataprotection.kubeblocks.io_restorejobs.yaml index 84334be2b..b282429e6 100644 --- a/config/crd/bases/dataprotection.kubeblocks.io_restorejobs.yaml +++ b/config/crd/bases/dataprotection.kubeblocks.io_restorejobs.yaml @@ -59,7 +59,7 @@ spec: description: the target database workload to restore properties: labelsSelector: - description: LabelSelector is used to find matching pods. Pods + description: labelsSelector is used to find matching pods. Pods that match this label selector are counted to determine the number of pods in their corresponding topology domain. properties: @@ -106,7 +106,7 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true secret: - description: Secret is used to connect to the target database + description: secret is used to connect to the target database cluster. If not set, secret will be inherited from backup policy template. if still not set, the controller will check if any system account for dataprotection has been created. @@ -115,14 +115,16 @@ spec: description: the secret name pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string - passwordKeyword: - description: PasswordKeyword the map keyword of the password - in the connection credential secret - type: string - userKeyword: - description: UserKeyword the map keyword of the user in the + passwordKey: + default: password + description: passwordKey the map key of the password in the connection credential secret type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the connection + credential secret + type: string required: - name type: object diff --git a/config/crd/bases/extensions.kubeblocks.io_addons.yaml b/config/crd/bases/extensions.kubeblocks.io_addons.yaml index 626c098b3..0ce3ddaf0 100644 --- a/config/crd/bases/extensions.kubeblocks.io_addons.yaml +++ b/config/crd/bases/extensions.kubeblocks.io_addons.yaml @@ -32,7 +32,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Addon is the Schema for the addons API + description: Addon is the Schema for the add-ons API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -47,8 +47,26 @@ spec: metadata: type: object spec: - description: AddonSpec defines the desired state of Addon + description: AddonSpec defines the desired state of an add-on. properties: + cliPlugins: + description: Plugin installation spec. + items: + properties: + description: + description: The description of the plugin. + type: string + indexRepository: + description: The index repository of the plugin. + type: string + name: + description: Name of the plugin. + type: string + required: + - indexRepository + - name + type: object + type: array defaultInstallValues: description: Default installation parameters. items: @@ -58,7 +76,7 @@ spec: attributes to be set. type: boolean extras: - description: Install spec. for extra items. + description: Installs spec. for extra items. items: properties: name: @@ -82,7 +100,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -94,8 +112,8 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is - explicitly specified, otherwise to an implementation-defined - value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + explicitly specified; otherwise, it defaults to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -129,7 +147,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -140,19 +158,19 @@ spec: x-kubernetes-int-or-string: true description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + it defaults to Limits if that is explicitly specified; + otherwise, it defaults to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object selectors: - description: Addon default install parameters selectors. If - multiple selectors are provided that all selectors must evaluate + description: Addon installs parameters selectors by default. + If multiple selectors are provided, all selectors must evaluate to true. items: properties: key: - description: The selector key, valid values are KubeVersion, + description: The selector key. Valid values are KubeVersion, KubeGitVersion. "KubeVersion" the semver expression of Kubernetes versions, i.e., v1.24. "KubeGitVersion" may contain distro. info., i.e., v1.24.4+eks. @@ -164,10 +182,10 @@ spec: description: "Represents a key's relationship to a set of values. Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex. \n Possible enum - values: `\"Contains\"` line contains string `\"DoesNotContain\"` - line does not contain string `\"MatchRegex\"` line contains - a match to the regular expression `\"DoesNotMatchRegex\"` - line does not contain a match to the regular expression" + values: `\"Contains\"` line contains a string. `\"DoesNotContain\"` + line does not contain a string. `\"MatchRegex\"` line + contains a match to the regular expression. `\"DoesNotMatchRegex\"` + line does not contain a match to the regular expression." enum: - Contains - DoesNotContain @@ -175,8 +193,8 @@ spec: - DoesNotMatchRegex type: string values: - description: An array of string values. Server as "OR" - expression to the operator. + description: An array of string values. It serves as an + "OR" expression to the operator. items: type: string type: array @@ -198,8 +216,7 @@ spec: description: Addon description. type: string helm: - description: Helm installation spec., it's only being processed if - type=helm. + description: Helm installation spec. It's processed only when type=helm. properties: chartLocationURL: description: A Helm Chart location URL. @@ -207,17 +224,18 @@ spec: installOptions: additionalProperties: type: string - description: installOptions defines Helm release install options. + description: installOptions defines Helm release installation + options. type: object installValues: - description: HelmInstallValues defines Helm release install set - values. + description: HelmInstallValues defines Helm release installation + set values. properties: configMapRefs: - description: Selects a key of a ConfigMap item list, the value - of ConfigMap can be a JSON or YAML string content, use key - name with ".json" or ".yaml" or ".yml" extension name to - specify content type. + description: Selects a key of a ConfigMap item list. The value + of ConfigMap can be a JSON or YAML string content. Use a + key name with ".json" or ".yaml" or ".yml" extension name + to specify a content type. items: properties: key: @@ -232,10 +250,10 @@ spec: type: object type: array secretRefs: - description: Selects a key of a Secrets item list, the value - of Secrets can be a JSON or YAML string content, use key + description: Selects a key of a Secrets item list. The value + of Secrets can be a JSON or YAML string content. Use a key name with ".json" or ".yaml" or ".yml" extension name to - specify content type. + specify a content type. items: properties: key: @@ -250,13 +268,13 @@ spec: type: object type: array setJSONValues: - description: Helm install set JSON values, can specify multiple - or separate values with commas(key1=jsonval1,key2=jsonval2). + description: Helm install set JSON values. It can specify + multiple or separate values with commas(key1=jsonval1,key2=jsonval2). items: type: string type: array setValues: - description: Helm install set values, can specify multiple + description: Helm install set values. It can specify multiple or separate values with commas(key1=val1,key2=val2). items: type: string @@ -267,7 +285,7 @@ spec: type: array type: object valuesMapping: - description: valuesMapping defines addon normalized resources + description: valuesMapping defines add-on normalized resources parameters mapped to Helm values' keys. properties: extras: @@ -275,12 +293,12 @@ spec: items: properties: jsonMap: - description: 'jsonMap define the "key" mapping values, - valid keys are tolerations. Enum values explained: - `"tolerations"` sets toleration mapping key' + description: 'jsonMap defines the "key" mapping values. + The valid key is tolerations. Enum values explained: + `"tolerations"` sets the toleration mapping key.' properties: tolerations: - description: tolerations sets toleration mapping + description: tolerations sets the toleration mapping key. type: string type: object @@ -321,29 +339,29 @@ spec: via PVC resize. type: object storage: - description: storage sets storage size value mapping - key. + description: storage sets the storage size value + mapping key. type: string type: object valueMap: - description: 'valueMap define the "key" mapping values, - valid keys are replicaCount, persistentVolumeEnabled, + description: 'valueMap define the "key" mapping values. + Valid keys are replicaCount, persistentVolumeEnabled, and storageClass. Enum values explained: `"replicaCount"` - sets replicaCount value mapping key `"persistentVolumeEnabled"` - sets persistent volume enabled mapping key `"storageClass"` - sets storageClass mapping key' + sets the replicaCount value mapping key. `"persistentVolumeEnabled"` + sets the persistent volume enabled mapping key. `"storageClass"` + sets the storageClass mapping key.' properties: persistentVolumeEnabled: - description: persistentVolumeEnabled persistent + description: persistentVolumeEnabled sets the persistent volume enabled mapping key. type: string replicaCount: - description: replicaCount sets replicaCount value - mapping key. + description: replicaCount sets the replicaCount + value mapping key. type: string storageClass: - description: storageClass sets storageClass mapping - key. + description: storageClass sets the storageClass + mapping key. type: string type: object required: @@ -354,12 +372,12 @@ spec: - name x-kubernetes-list-type: map jsonMap: - description: 'jsonMap define the "key" mapping values, valid - keys are tolerations. Enum values explained: `"tolerations"` - sets toleration mapping key' + description: 'jsonMap defines the "key" mapping values. The + valid key is tolerations. Enum values explained: `"tolerations"` + sets the toleration mapping key.' properties: tolerations: - description: tolerations sets toleration mapping key. + description: tolerations sets the toleration mapping key. type: string type: object resources: @@ -395,27 +413,29 @@ spec: resize. type: object storage: - description: storage sets storage size value mapping key. + description: storage sets the storage size value mapping + key. type: string type: object valueMap: - description: 'valueMap define the "key" mapping values, valid + description: 'valueMap define the "key" mapping values. Valid keys are replicaCount, persistentVolumeEnabled, and storageClass. - Enum values explained: `"replicaCount"` sets replicaCount - value mapping key `"persistentVolumeEnabled"` sets persistent - volume enabled mapping key `"storageClass"` sets storageClass - mapping key' + Enum values explained: `"replicaCount"` sets the replicaCount + value mapping key. `"persistentVolumeEnabled"` sets the + persistent volume enabled mapping key. `"storageClass"` + sets the storageClass mapping key.' properties: persistentVolumeEnabled: - description: persistentVolumeEnabled persistent volume - enabled mapping key. + description: persistentVolumeEnabled sets the persistent + volume enabled mapping key. type: string replicaCount: - description: replicaCount sets replicaCount value mapping - key. + description: replicaCount sets the replicaCount value + mapping key. type: string storageClass: - description: storageClass sets storageClass mapping key. + description: storageClass sets the storageClass mapping + key. type: string type: object type: object @@ -430,7 +450,7 @@ spec: attributes to be set. type: boolean extras: - description: Install spec. for extra items. + description: Installs spec. for extra items. items: properties: name: @@ -454,7 +474,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of - compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -466,8 +486,8 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is - explicitly specified, otherwise to an implementation-defined - value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + explicitly specified; otherwise, it defaults to an + implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -501,7 +521,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -512,8 +532,9 @@ spec: x-kubernetes-int-or-string: true description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + it defaults to Limits if that is explicitly specified; otherwise, + it defaults to an implementation-defined value. More info: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -524,20 +545,21 @@ spec: type: string type: object installable: - description: Addon installable spec., provide selector and auto-install + description: Addon installable spec. It provides selector and auto-install settings. properties: autoInstall: default: false - description: autoInstall defines an addon should auto installed + description: autoInstall defines an add-on should be installed + automatically. type: boolean selectors: - description: Addon installable selectors. If multiple selectors - are provided that all selectors must evaluate to true. + description: Add-on installable selectors. If multiple selectors + are provided, all selectors must evaluate to true. items: properties: key: - description: The selector key, valid values are KubeVersion, + description: The selector key. Valid values are KubeVersion, KubeGitVersion. "KubeVersion" the semver expression of Kubernetes versions, i.e., v1.24. "KubeGitVersion" may contain distro. info., i.e., v1.24.4+eks. @@ -549,10 +571,10 @@ spec: description: "Represents a key's relationship to a set of values. Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex. \n Possible enum values: - `\"Contains\"` line contains string `\"DoesNotContain\"` - line does not contain string `\"MatchRegex\"` line contains - a match to the regular expression `\"DoesNotMatchRegex\"` - line does not contain a match to the regular expression" + `\"Contains\"` line contains a string. `\"DoesNotContain\"` + line does not contain a string. `\"MatchRegex\"` line + contains a match to the regular expression. `\"DoesNotMatchRegex\"` + line does not contain a match to the regular expression." enum: - Contains - DoesNotContain @@ -560,8 +582,8 @@ spec: - DoesNotMatchRegex type: string values: - description: An array of string values. Server as "OR" expression - to the operator. + description: An array of string values. It serves as an + "OR" expression to the operator. items: type: string type: array @@ -574,7 +596,7 @@ spec: - autoInstall type: object type: - description: Addon type, valid value is helm. + description: Add-on type. The valid value is helm. enum: - Helm type: string @@ -587,10 +609,11 @@ spec: otherwise rule: 'has(self.type) && self.type == ''Helm'' ? has(self.helm) : !has(self.helm)' status: - description: AddonStatus defines the observed state of Addon + description: AddonStatus defines the observed state of an add-on. properties: conditions: - description: Describe current state of Addon API installation conditions. + description: Describes the current state of add-on API installation + conditions. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct @@ -660,12 +683,12 @@ spec: type: array observedGeneration: description: observedGeneration is the most recent generation observed - for this Addon. It corresponds to the Addon's generation, which + for this add-on. It corresponds to the add-on's generation, which is updated on mutation by the API Server. format: int64 type: integer phase: - description: Addon installation phases. Valid values are Disabled, + description: Add-on installation phases. Valid values are Disabled, Enabled, Failed, Enabling, Disabling. enum: - Disabled diff --git a/config/crd/bases/workloads.kubeblocks.io_consensussets.yaml b/config/crd/bases/workloads.kubeblocks.io_consensussets.yaml new file mode 100644 index 000000000..4bcdc8f36 --- /dev/null +++ b/config/crd/bases/workloads.kubeblocks.io_consensussets.yaml @@ -0,0 +1,8649 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: consensussets.workloads.kubeblocks.io +spec: + group: workloads.kubeblocks.io + names: + categories: + - kubeblocks + - all + kind: ConsensusSet + listKind: ConsensusSetList + plural: consensussets + shortNames: + - csset + singular: consensusset + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: leader pod name. + jsonPath: .status.membersStatus[?(@.role.isLeader==true)].podName + name: LEADER + type: string + - description: ready replicas. + jsonPath: .status.readyReplicas + name: READY + type: string + - description: total replicas. + jsonPath: .status.replicas + name: REPLICAS + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ConsensusSet is the Schema for the consensussets 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: ConsensusSetSpec defines the desired state of ConsensusSet + properties: + credential: + description: Credential used to connect to DB engine + properties: + password: + description: Password variable name will be KB_CONSENSUS_SET_PASSWORD + properties: + value: + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the + container and any service environment variables. If a variable + cannot be resolved, the reference in the input string will + be unchanged. Double $$ are reduced to a single $, which + allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + type: object + type: object + username: + description: Username variable name will be KB_CONSENSUS_SET_USERNAME + properties: + value: + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the + container and any service environment variables. If a variable + cannot be resolved, the reference in the input string will + be unchanged. Double $$ are reduced to a single $, which + allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + type: object + type: object + required: + - password + - username + type: object + membershipReconfiguration: + description: MembershipReconfiguration provides actions to do membership + dynamic reconfiguration. + properties: + logSyncAction: + description: LogSyncAction specifies how to trigger the new member + to start log syncing previous none-nil action's Image wil be + used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + memberJoinAction: + description: MemberJoinAction specifies how to add member previous + none-nil action's Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + memberLeaveAction: + description: MemberLeaveAction specifies how to remove member + previous none-nil action's Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + promoteAction: + description: PromoteAction specifies how to tell the cluster that + the new member can join voting now previous none-nil action's + Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + switchoverAction: + description: SwitchoverAction specifies how to do switchover latest + [BusyBox](https://busybox.net/) image will be used if Image + not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + type: object + replicas: + default: 1 + description: Replicas defines number of Pods + format: int32 + minimum: 0 + type: integer + roleObservation: + description: RoleObservation provides method to observe role. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the observation + to be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + initialDelaySeconds: + default: 0 + description: Number of seconds after the container has started + before role observation has started. + format: int32 + minimum: 0 + type: integer + observationActions: + description: 'ObservationActions define Actions to be taken in + serial. after all actions done, the final output should be a + single string of the role name defined in spec.Roles latest + [BusyBox](https://busybox.net/) image will be used if Image + not configured Environment variables can be used in Command: + - v_KB_CONSENSUS_SET_LAST_STDOUT stdout from last action, watch + ''v_'' prefixed - KB_CONSENSUS_SET_USERNAME username part of + credential - KB_CONSENSUS_SET_PASSWORD password part of credential' + items: + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be + used to retrieve of process role info + type: string + required: + - command + type: object + type: array + periodSeconds: + default: 2 + description: How often (in seconds) to perform the observation. + Default to 2 seconds. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the observation + to be considered successful after having failed. Defaults to + 1. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the observation times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + required: + - observationActions + type: object + roles: + description: Roles, a list of roles defined in this consensus system. + items: + properties: + accessMode: + default: ReadWrite + description: AccessMode, what service this member capable. + enum: + - None + - Readonly + - ReadWrite + type: string + canVote: + default: true + description: CanVote, whether this member has voting rights + type: boolean + isLeader: + default: false + description: IsLeader, whether this member is the leader + type: boolean + name: + default: leader + description: Name, role name. + type: string + required: + - accessMode + - name + type: object + type: array + service: + description: service defines the behavior of a service spec. provides + read-write service https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + allocateLoadBalancerNodePorts: + description: allocateLoadBalancerNodePorts defines if NodePorts + will be automatically allocated for services with type LoadBalancer. Default + is "true". It may be set to "false" if the cluster load-balancer + does not rely on NodePorts. If the caller requests specific + NodePorts (by specifying a value), those requests will be respected, + regardless of this field. This field may only be set for services + with type LoadBalancer and will be cleared if the type is changed + to any other type. + type: boolean + clusterIP: + description: 'clusterIP is the IP address of the service and is + usually assigned randomly. If an address is specified manually, + is in-range (as per system configuration), and is not in use, + it will be allocated to the service; otherwise creation of the + service will fail. This field may not be changed through updates + unless the type field is also being changed to ExternalName + (which requires this field to be blank) or the type field is + being changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values are + "None", empty string (""), or a valid IP address. Setting this + to "None" makes a "headless service" (no virtual IP), which + is useful when direct endpoint connections are preferred and + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. If this field is specified when + creating a Service of type ExternalName, creation will fail. + This field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + clusterIPs: + description: "ClusterIPs is a list of IP addresses assigned to + this service, and are usually assigned randomly. If an address + is specified manually, is in-range (as per system configuration), + and is not in use, it will be allocated to the service; otherwise + creation of the service will fail. This field may not be changed + through updates unless the type field is also being changed + to ExternalName (which requires this field to be empty) or the + type field is being changed from ExternalName (in which case + this field may optionally be specified, as describe above). + \ Valid values are \"None\", empty string (\"\"), or a valid + IP address. Setting this to \"None\" makes a \"headless service\" + (no virtual IP), which is useful when direct endpoint connections + are preferred and proxying is not required. Only applies to + types ClusterIP, NodePort, and LoadBalancer. If this field is + specified when creating a Service of type ExternalName, creation + will fail. This field will be wiped when updating a Service + to type ExternalName. If this field is not specified, it will + be initialized from the clusterIP field. If this field is specified, + clients must ensure that clusterIPs[0] and clusterIP have the + same value. \n This field may hold a maximum of two entries + (dual-stack IPs, in either order). These IPs must correspond + to the values of the ipFamilies field. Both clusterIPs and ipFamilies + are governed by the ipFamilyPolicy field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies" + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: externalIPs is a list of IP addresses for which nodes + in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that discovery + mechanisms will return as an alias for this service (e.g. a + DNS CNAME record). No proxying will be involved. Must be a + lowercase RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) + and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: externalTrafficPolicy describes how nodes distribute + service traffic they receive on one of the Service's "externally-facing" + addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If + set to "Local", the proxy will configure the service in a way + that assumes that external load balancers will take care of + balancing the service traffic between nodes, and so each node + will deliver traffic only to the node-local endpoints of the + service, without masquerading the client source IP. (Traffic + mistakenly sent to a node with no endpoints will be dropped.) + The default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology + and other features). Note that traffic sent to an External IP + or LoadBalancer IP from within the cluster will always get "Cluster" + semantics, but clients sending to a NodePort from within the + cluster may need to take traffic policy into account when picking + a node. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. This only applies when type is set to LoadBalancer + and externalTrafficPolicy is set to Local. If a value is specified, + is in-range, and is not in use, it will be used. If not specified, + a value will be automatically allocated. External systems (e.g. + load-balancers) can use this port to determine if a given node + holds endpoints for this service or not. If this field is specified + when creating a Service which does not need it, creation will + fail. This field will be wiped when updating a Service to no + longer need it (e.g. changing type). This field cannot be updated + once set. + format: int32 + type: integer + internalTrafficPolicy: + description: InternalTrafficPolicy describes how nodes distribute + service traffic they receive on the ClusterIP. If set to "Local", + the proxy will assume that pods only want to talk to endpoints + of the service on the same node as the pod, dropping the traffic + if there are no local endpoints. The default value, "Cluster", + uses the standard behavior of routing to all endpoints evenly + (possibly modified by topology and other features). + type: string + ipFamilies: + description: "IPFamilies is a list of IP families (e.g. IPv4, + IPv6) assigned to this service. This field is usually assigned + automatically based on cluster configuration and the ipFamilyPolicy + field. If this field is specified manually, the requested family + is available in the cluster, and ipFamilyPolicy allows it, it + will be used; otherwise creation of the service will fail. This + field is conditionally mutable: it allows for adding or removing + a secondary IP family, but it does not allow changing the primary + IP family of the Service. Valid values are \"IPv4\" and \"IPv6\". + \ This field only applies to Services of types ClusterIP, NodePort, + and LoadBalancer, and does apply to \"headless\" services. This + field will be wiped when updating a Service to type ExternalName. + \n This field may hold a maximum of two entries (dual-stack + families, in either order). These families must correspond + to the values of the clusterIPs field, if specified. Both clusterIPs + and ipFamilies are governed by the ipFamilyPolicy field." + items: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed + by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness requested + or required by this Service. If there is no value provided, + then this field will be set to SingleStack. Services can be + "SingleStack" (a single IP family), "PreferDualStack" (two IP + families on dual-stack configured clusters or a single IP family + on single-stack clusters), or "RequireDualStack" (two IP families + on dual-stack configured clusters, otherwise fail). The ipFamilies + and clusterIPs fields depend on the value of this field. This + field will be wiped when updating a service to type ExternalName. + type: string + loadBalancerClass: + description: loadBalancerClass is the class of the load balancer + implementation this Service belongs to. If specified, the value + of this field must be a label-style identifier, with an optional + prefix, e.g. "internal-vip" or "example.com/internal-vip". Unprefixed + names are reserved for end-users. This field can only be set + when the Service type is 'LoadBalancer'. If not set, the default + load balancer implementation is used, today this is typically + done through the cloud provider integration, but should apply + for any default implementation. If set, it is assumed that a + load balancer implementation is watching for Services with a + matching class. Any default load balancer implementation (e.g. + cloud providers) should ignore Services that set this field. + This field can only be set when creating or updating a Service + to type 'LoadBalancer'. Once set, it can not be changed. This + field will be wiped when a service is updated to a non 'LoadBalancer' + type. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not support + the feature. Deprecated: This field was under-specified and + its meaning varies across implementations, and it cannot support + dual-stack. As of Kubernetes v1.24, users are encouraged to + use implementation-specific annotations when available. This + field may be removed in a future API version.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field will + be ignored if the cloud-provider does not support the feature." + More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's port. + properties: + appProtocol: + description: The application protocol for this port. This + field follows standard Kubernetes label syntax. Un-prefixed + names are reserved for IANA standard service names (as + per RFC-6335 and https://www.iana.org/assignments/service-names). + Non-standard protocols should use prefixed names such + as mycompany.com/my-custom-protocol. + type: string + name: + description: The name of this port within the service. This + must be a DNS_LABEL. All ports within a ServiceSpec must + have unique names. When considering the endpoints for + a Service, this must match the 'name' field in the EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type is NodePort or LoadBalancer. Usually + assigned by the system. If a value is specified, in-range, + and not in use it will be used, otherwise the operation + will fail. If not specified, a port will be allocated + if this Service requires one. If this field is specified + when creating a Service which does not need it, creation + will fail. This field will be wiped when updating a Service + to no longer need it (e.g. changing type from NodePort + to ClusterIP). More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on the + pods targeted by the service. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. If this is + a string, it will be looked up as a named port in the + target Pod''s container ports. If this is not specified, + the value of the ''port'' field is used (an identity map). + This field is ignored for services with clusterIP=None, + and should be omitted or set equal to the ''port'' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: publishNotReadyAddresses indicates that any agent + which deals with endpoints for this Service should disregard + any indications of ready/not-ready. The primary use case for + setting this field is for a StatefulSet's Headless Service to + propagate SRV DNS records for its Pods for the purpose of peer + discovery. The Kubernetes controllers that generate Endpoints + and EndpointSlice resources for Services interpret this to mean + that all endpoints are considered "ready" even if the Pods themselves + are not. Agents which consume only Kubernetes generated endpoints + through the Endpoints or EndpointSlice resources can safely + assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys and + values matching this selector. If empty or not present, the + service is assumed to have an external process managing its + endpoints, which Kubernetes will not modify. Only applies to + types ClusterIP, NodePort, and LoadBalancer. Ignored if type + is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. Must + be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of ClientIP + type session sticky time. The value must be >0 && <=86400(for + 1 day) if ServiceAffinity == "ClientIP". Default value + is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ClusterIP" allocates a cluster-internal IP + address for load-balancing to endpoints. Endpoints are determined + by the selector or if that is not specified, by manual construction + of an Endpoints object or EndpointSlice objects. If clusterIP + is "None", no virtual IP is allocated and the endpoints are + published as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node which + routes to the same endpoints as the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the same endpoints + as the clusterIP. "ExternalName" aliases this service to the + specified externalName. Several other fields do not apply to + ExternalName services. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + x-kubernetes-preserve-unknown-fields: true + template: + description: PodTemplateSpec describes the data a pod should have + when created from a template + properties: + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: 'Specification of the desired behavior of the pod. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + activeDeadlineSeconds: + description: Optional duration in seconds the pod may be active + on the node relative to StartTime before the system will + actively try to mark it failed and kill associated containers. + Value must be a positive integer. + format: int64 + type: integer + affinity: + description: If specified, the pod's scheduling constraints + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, + etc.), compute a sum by iterating through the elements + of this field and adding "weight" to the sum if + the node matches the corresponding matchExpressions; + the node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term + matches all objects with implicit weight 0 (i.e. + it's a no-op). A null preferred scheduling term + matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + affinity requirements specified by this field cease + to be met at some point during pod execution (e.g. + due to an update), the system may or may not try + to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: A null or empty node selector term + matches no objects. The requirements of them + are ANDed. The TopologySelectorTerm type implements + a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, + etc.), compute a sum by iterating through the elements + of this field and adding "weight" to the sum if + the node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest sum + are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in the + range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + affinity requirements specified by this field cease + to be met at some point during pod execution (e.g. + due to a pod label update), the system may or may + not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes + corresponding to each podAffinityTerm are intersected, + i.e. all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the given + namespace(s)) that this pod should be co-located + (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node + whose value of the label with key + matches that of any node on which a pod of the + set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. The term is applied + to the union of the namespaces selected by + this field and the ones listed in the namespaces + field. null selector and null or empty namespaces + list means "this pod's namespace". An empty + selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + The term is applied to the union of the namespaces + listed in this field and the ones selected + by namespaceSelector. null or empty namespaces + list and null namespaceSelector means "this + pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the + pods matching the labelSelector in the specified + namespaces, where co-located is defined as + running on a node whose value of the label + with key topologyKey matches that of any node + on which any of the selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the anti-affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity + expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to + the sum if the node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest sum + are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in the + range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + anti-affinity requirements specified by this field + cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may + or may not try to eventually evict the pod from + its node. When there are multiple elements, the + lists of nodes corresponding to each podAffinityTerm + are intersected, i.e. all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the given + namespace(s)) that this pod should be co-located + (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node + whose value of the label with key + matches that of any node on which a pod of the + set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. The term is applied + to the union of the namespaces selected by + this field and the ones listed in the namespaces + field. null selector and null or empty namespaces + list means "this pod's namespace". An empty + selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + The term is applied to the union of the namespaces + listed in this field and the ones selected + by namespaceSelector. null or empty namespaces + list and null namespaceSelector means "this + pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the + pods matching the labelSelector in the specified + namespaces, where co-located is defined as + running on a node whose value of the label + with key topologyKey matches that of any node + on which any of the selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + description: AutomountServiceAccountToken indicates whether + a service account token should be automatically mounted. + type: boolean + containers: + description: List of containers belonging to the pod. Containers + cannot currently be added or removed. There must be at least + one container in a Pod. Cannot be updated. + items: + description: A single application container that you want + to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. The container + image''s CMD is used if this is not provided. Variable + references $(VAR_NAME) are expanded using the container''s + environment. If a variable cannot be resolved, the + reference in the input string will be unchanged. Double + $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The container image''s ENTRYPOINT is used + if this is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. If + a variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config + management to default or override container images + in workload controllers like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system should + take in response to container lifecycle events. Cannot + be updated. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container liveness. + Container will be restarted if the probe fails. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: List of ports to expose from the container. + Not specifying a port here DOES NOT prevent that port + from being exposed. Any port which is listening on + the default "0.0.0.0" address inside a container will + be accessible from the network. Modifying this array + with strategic merge patch may corrupt the data. For + more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container service readiness. + Container will be removed from service endpoints if + the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by this container. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the security options + the container should be run with. If set, the fields + of SecurityContext override the equivalent fields + of PodSecurityContext. More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that the Pod has + successfully initialized. If specified, no other probes + are executed until this completes successfully. If + this probe fails, the Pod will be restarted, just + as if the livenessProbe failed. This can be used to + provide different probe parameters at the beginning + of a Pod''s lifecycle, when it might take a long time + to load data or warm a cache, than during steady-state + operation. This cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + dnsConfig: + description: Specifies the DNS parameters of a pod. Parameters + specified here will be merged to the generated DNS configuration + based on DNSPolicy. + properties: + nameservers: + description: A list of DNS name server IP addresses. This + will be appended to the base nameservers generated from + DNSPolicy. Duplicated nameservers will be removed. + items: + type: string + type: array + options: + description: A list of DNS resolver options. This will + be merged with the base options generated from DNSPolicy. + Duplicated entries will be removed. Resolution options + given in Options will override those that appear in + the base DNSPolicy. + items: + description: PodDNSConfigOption defines DNS resolver + options of a pod. + properties: + name: + description: Required. + type: string + value: + type: string + type: object + type: array + searches: + description: A list of DNS search domains for host-name + lookup. This will be appended to the base search paths + generated from DNSPolicy. Duplicated search paths will + be removed. + items: + type: string + type: array + type: object + dnsPolicy: + description: Set DNS policy for the pod. Defaults to "ClusterFirst". + Valid values are 'ClusterFirstWithHostNet', 'ClusterFirst', + 'Default' or 'None'. DNS parameters given in DNSConfig will + be merged with the policy selected with DNSPolicy. To have + DNS options set along with hostNetwork, you have to specify + DNS policy explicitly to 'ClusterFirstWithHostNet'. + type: string + enableServiceLinks: + description: 'EnableServiceLinks indicates whether information + about services should be injected into pod''s environment + variables, matching the syntax of Docker links. Optional: + Defaults to true.' + type: boolean + ephemeralContainers: + description: List of ephemeral containers run in this pod. + Ephemeral containers may be run in an existing pod to perform + user-initiated actions such as debugging. This list cannot + be specified when creating a pod, and it cannot be modified + by updating the pod spec. In order to add an ephemeral container + to an existing pod, use the pod's ephemeralcontainers subresource. + items: + description: "An EphemeralContainer is a temporary container + that you may add to an existing Pod for user-initiated + activities such as debugging. Ephemeral containers have + no resource or scheduling guarantees, and they will not + be restarted when they exit or when a Pod is removed or + restarted. The kubelet may evict a Pod if an ephemeral + container causes the Pod to exceed its resource allocation. + \n To add an ephemeral container, use the ephemeralcontainers + subresource of an existing Pod. Ephemeral containers may + not be removed or restarted." + properties: + args: + description: 'Arguments to the entrypoint. The image''s + CMD is used if this is not provided. Variable references + $(VAR_NAME) are expanded using the container''s environment. + If a variable cannot be resolved, the reference in + the input string will be unchanged. Double $$ are + reduced to a single $, which allows for escaping the + $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The image''s ENTRYPOINT is used if this is + not provided. Variable references $(VAR_NAME) are + expanded using the container''s environment. If a + variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Lifecycle is not allowed for ephemeral + containers. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the ephemeral container specified + as a DNS_LABEL. This name must be unique among all + containers, init containers and ephemeral containers. + type: string + ports: + description: Ports are not allowed for ephemeral containers. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: Resources are not allowed for ephemeral + containers. Ephemeral containers use spare resources + already allocated to the pod. + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'Optional: SecurityContext defines the + security options the ephemeral container should be + run with. If set, the fields of SecurityContext override + the equivalent fields of PodSecurityContext.' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + targetContainerName: + description: "If set, the name of the container from + PodSpec that this ephemeral container targets. The + ephemeral container will be run in the namespaces + (IPC, PID, etc) of this container. If not set then + the ephemeral container uses the namespaces configured + in the Pod spec. \n The container runtime must implement + support for this feature. If the runtime does not + support namespace targeting then the result of setting + this field is undefined." + type: string + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Subpath mounts are not allowed for ephemeral + containers. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + hostAliases: + description: HostAliases is an optional list of hosts and + IPs that will be injected into the pod's hosts file if specified. + This is only valid for non-hostNetwork pods. + items: + description: HostAlias holds the mapping between IP and + hostnames that will be injected as an entry in the pod's + hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + ip: + description: IP address of the host file entry. + type: string + type: object + type: array + hostIPC: + description: 'Use the host''s ipc namespace. Optional: Default + to false.' + type: boolean + hostNetwork: + description: Host networking requested for this pod. Use the + host's network namespace. If this option is set, the ports + that will be used must be specified. Default to false. + type: boolean + hostPID: + description: 'Use the host''s pid namespace. Optional: Default + to false.' + type: boolean + hostUsers: + description: 'Use the host''s user namespace. Optional: Default + to true. If set to true or not present, the pod will be + run in the host user namespace, useful for when the pod + needs a feature only available to the host user namespace, + such as loading a kernel module with CAP_SYS_MODULE. When + set to false, a new userns is created for the pod. Setting + false is useful for mitigating container breakout vulnerabilities + even allowing users to run their containers as root without + actually having root privileges on the host. This field + is alpha-level and is only honored by servers that enable + the UserNamespacesSupport feature.' + type: boolean + hostname: + description: Specifies the hostname of the Pod If not specified, + the pod's hostname will be set to a system-defined value. + type: string + imagePullSecrets: + description: 'ImagePullSecrets is an optional list of references + to secrets in the same namespace to use for pulling any + of the images used by this PodSpec. If specified, these + secrets will be passed to individual puller implementations + for them to use. More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod' + 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 + initContainers: + description: 'List of initialization containers belonging + to the pod. Init containers are executed in order prior + to containers being started. If any init container fails, + the pod is considered to have failed and is handled according + to its restartPolicy. The name for an init container or + normal container must be unique among all containers. Init + containers may not have Lifecycle actions, Readiness probes, + Liveness probes, or Startup probes. The resourceRequirements + of an init container are taken into account during scheduling + by finding the highest request/limit for each resource type, + and then using the max of of that value or the sum of the + normal containers. Limits are applied to init containers + in a similar fashion. Init containers cannot currently be + added or removed. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/' + items: + description: A single application container that you want + to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. The container + image''s CMD is used if this is not provided. Variable + references $(VAR_NAME) are expanded using the container''s + environment. If a variable cannot be resolved, the + reference in the input string will be unchanged. Double + $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The container image''s ENTRYPOINT is used + if this is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. If + a variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config + management to default or override container images + in workload controllers like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system should + take in response to container lifecycle events. Cannot + be updated. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container liveness. + Container will be restarted if the probe fails. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: List of ports to expose from the container. + Not specifying a port here DOES NOT prevent that port + from being exposed. Any port which is listening on + the default "0.0.0.0" address inside a container will + be accessible from the network. Modifying this array + with strategic merge patch may corrupt the data. For + more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container service readiness. + Container will be removed from service endpoints if + the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by this container. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the security options + the container should be run with. If set, the fields + of SecurityContext override the equivalent fields + of PodSecurityContext. More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that the Pod has + successfully initialized. If specified, no other probes + are executed until this completes successfully. If + this probe fails, the Pod will be restarted, just + as if the livenessProbe failed. This can be used to + provide different probe parameters at the beginning + of a Pod''s lifecycle, when it might take a long time + to load data or warm a cache, than during steady-state + operation. This cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + nodeName: + description: NodeName is a request to schedule this pod onto + a specific node. If it is non-empty, the scheduler simply + schedules this pod onto that node, assuming that it fits + resource requirements. + type: string + nodeSelector: + additionalProperties: + type: string + description: 'NodeSelector is a selector which must be true + for the pod to fit on a node. Selector which must match + a node''s labels for the pod to be scheduled on that node. + More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' + type: object + x-kubernetes-map-type: atomic + os: + description: "Specifies the OS of the containers in the pod. + Some pod and container fields are restricted if this is + set. \n If the OS field is set to linux, the following fields + must be unset: -securityContext.windowsOptions \n If the + OS field is set to windows, following fields must be unset: + - spec.hostPID - spec.hostIPC - spec.hostUsers - spec.securityContext.seLinuxOptions + - spec.securityContext.seccompProfile - spec.securityContext.fsGroup + - spec.securityContext.fsGroupChangePolicy - spec.securityContext.sysctls + - spec.shareProcessNamespace - spec.securityContext.runAsUser + - spec.securityContext.runAsGroup - spec.securityContext.supplementalGroups + - spec.containers[*].securityContext.seLinuxOptions - spec.containers[*].securityContext.seccompProfile + - spec.containers[*].securityContext.capabilities - spec.containers[*].securityContext.readOnlyRootFilesystem + - spec.containers[*].securityContext.privileged - spec.containers[*].securityContext.allowPrivilegeEscalation + - spec.containers[*].securityContext.procMount - spec.containers[*].securityContext.runAsUser + - spec.containers[*].securityContext.runAsGroup" + properties: + name: + description: 'Name is the name of the operating system. + The currently supported values are linux and windows. + Additional value may be defined in future and can be + one of: https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration + Clients should expect to handle additional values and + treat unrecognized values in this field as os: null' + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Overhead represents the resource overhead associated + with running a pod for a given RuntimeClass. This field + will be autopopulated at admission time by the RuntimeClass + admission controller. If the RuntimeClass admission controller + is enabled, overhead must not be set in Pod create requests. + The RuntimeClass admission controller will reject Pod create + requests which have the overhead already set. If RuntimeClass + is configured and selected in the PodSpec, Overhead will + be set to the value defined in the corresponding RuntimeClass, + otherwise it will remain unset and treated as zero. More + info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md' + type: object + preemptionPolicy: + description: PreemptionPolicy is the Policy for preempting + pods with lower priority. One of Never, PreemptLowerPriority. + Defaults to PreemptLowerPriority if unset. + type: string + priority: + description: The priority value. Various system components + use this field to find the priority of the pod. When Priority + Admission Controller is enabled, it prevents users from + setting this field. The admission controller populates this + field from PriorityClassName. The higher the value, the + higher the priority. + format: int32 + type: integer + priorityClassName: + description: If specified, indicates the pod's priority. "system-node-critical" + and "system-cluster-critical" are two special keywords which + indicate the highest priorities with the former being the + highest priority. Any other name must be defined by creating + a PriorityClass object with that name. If not specified, + the pod priority will be default or zero if there is no + default. + type: string + readinessGates: + description: 'If specified, all readiness gates will be evaluated + for pod readiness. A pod is ready when all its containers + are ready AND all conditions specified in the readiness + gates have status equal to "True" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates' + items: + description: PodReadinessGate contains the reference to + a pod condition + properties: + conditionType: + description: ConditionType refers to a condition in + the pod's condition list with matching type. + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + description: "ResourceClaims defines which ResourceClaims + must be allocated and reserved before the Pod is allowed + to start. The resources will be made available to those + containers which consume them by name. \n This is an alpha + field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: PodResourceClaim references exactly one ResourceClaim + through a ClaimSource. It adds a name to it that uniquely + identifies the ResourceClaim inside the Pod. Containers + that need access to the ResourceClaim reference it with + this name. + properties: + name: + description: Name uniquely identifies this resource + claim inside the pod. This must be a DNS_LABEL. + type: string + source: + description: Source describes where to find the ResourceClaim. + properties: + resourceClaimName: + description: ResourceClaimName is the name of a + ResourceClaim object in the same namespace as + this pod. + type: string + resourceClaimTemplateName: + description: "ResourceClaimTemplateName is the name + of a ResourceClaimTemplate object in the same + namespace as this pod. \n The template will be + used to create a new ResourceClaim, which will + be bound to this pod. When this pod is deleted, + the ResourceClaim will also be deleted. The name + of the ResourceClaim will be -, where is the PodResourceClaim.Name. + Pod validation will reject the pod if the concatenated + name is not valid for a ResourceClaim (e.g. too + long). \n An existing ResourceClaim with that + name that is not owned by the pod will not be + used for the pod to avoid using an unrelated resource + by mistake. Scheduling and pod startup are then + blocked until the unrelated ResourceClaim is removed. + \n This field is immutable and no changes will + be made to the corresponding ResourceClaim by + the control plane after creating the ResourceClaim." + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + description: 'Restart policy for all containers within the + pod. One of Always, OnFailure, Never. Default to Always. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy' + type: string + runtimeClassName: + description: 'RuntimeClassName refers to a RuntimeClass object + in the node.k8s.io group, which should be used to run this + pod. If no RuntimeClass resource matches the named class, + the pod will not be run. If unset or empty, the "legacy" + RuntimeClass will be used, which is an implicit class with + an empty definition that uses the default runtime handler. + More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class' + type: string + schedulerName: + description: If specified, the pod will be dispatched by specified + scheduler. If not specified, the pod will be dispatched + by default scheduler. + type: string + schedulingGates: + description: "SchedulingGates is an opaque list of values + that if specified will block scheduling the pod. More info: + \ https://git.k8s.io/enhancements/keps/sig-scheduling/3521-pod-scheduling-readiness. + \n This is an alpha-level feature enabled by PodSchedulingReadiness + feature gate." + items: + description: PodSchedulingGate is associated to a Pod to + guard its scheduling. + properties: + name: + description: Name of the scheduling gate. Each scheduling + gate must have a unique name field. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + description: 'SecurityContext holds pod-level security attributes + and common container settings. Optional: Defaults to empty. See + type description for default values of each field.' + properties: + fsGroup: + description: "A special supplemental group that applies + to all containers in a pod. Some volume types allow + the Kubelet to change the ownership of that volume to + be owned by the pod: \n 1. The owning GID will be the + FSGroup 2. The setgid bit is set (new files created + in the volume will be owned by FSGroup) 3. The permission + bits are OR'd with rw-rw---- \n If unset, the Kubelet + will not modify the ownership and permissions of any + volume. Note that this field cannot be set when spec.os.name + is windows." + format: int64 + type: integer + fsGroupChangePolicy: + description: 'fsGroupChangePolicy defines behavior of + changing ownership and permission of the volume before + being exposed inside Pod. This field will only apply + to volume types which support fsGroup based ownership(and + permissions). It will have no effect on ephemeral volume + types such as: secret, configmaps and emptydir. Valid + values are "OnRootMismatch" and "Always". If not specified, + "Always" is used. Note that this field cannot be set + when spec.os.name is windows.' + type: string + runAsGroup: + description: The GID to run the entrypoint of the container + process. Uses runtime default if unset. May also be + set in SecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as + a non-root user. If true, the Kubelet will validate + the image at runtime to ensure that it does not run + as UID 0 (root) and fail to start the container if it + does. If unset or false, no such validation will be + performed. May also be set in SecurityContext. If set + in both SecurityContext and PodSecurityContext, the + value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the container + process. Defaults to user specified in image metadata + if unspecified. May also be set in SecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence + for that container. Note that this field cannot be set + when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to all + containers. If unspecified, the container runtime will + allocate a random SELinux context for each container. May + also be set in SecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. Note that this + field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by the containers + in this pod. Note that this field cannot be set when + spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. The + profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's + configured seccomp profile location. Must only be + set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: \n Localhost + - a profile defined in a file on the node should + be used. RuntimeDefault - the container runtime + default profile should be used. Unconfined - no + profile should be applied." + type: string + required: + - type + type: object + supplementalGroups: + description: A list of groups applied to the first process + run in each container, in addition to the container's + primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container + process. If unspecified, no additional groups are added + to any container. Note that group memberships defined + in the container image for the uid of the container + process are still effective, even if they are not included + in this list. Note that this field cannot be set when + spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: Sysctls hold a list of namespaced sysctls + used for the pod. Pods with unsupported sysctls (by + the container runtime) might fail to launch. Note that + this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be + set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: The Windows specific settings applied to + all containers. If unspecified, the options within a + container's SecurityContext will be used. If set in + both SecurityContext and PodSecurityContext, the value + specified in SecurityContext takes precedence. Note + that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the GMSA + admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential spec + named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of + the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. This + field is alpha-level and will only be honored by + components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the feature + flag will result in errors when validating the Pod. + All of a Pod's containers must have the same effective + HostProcess value (it is not allowed to have a mix + of HostProcess containers and non-HostProcess containers). In + addition, if HostProcess is true then HostNetwork + must also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. Defaults to the user specified + in image metadata if unspecified. May also be set + in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence. + type: string + type: object + type: object + serviceAccount: + description: 'DeprecatedServiceAccount is a depreciated alias + for ServiceAccountName. Deprecated: Use serviceAccountName + instead.' + type: string + serviceAccountName: + description: 'ServiceAccountName is the name of the ServiceAccount + to use to run this pod. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/' + type: string + setHostnameAsFQDN: + description: If true the pod's hostname will be configured + as the pod's FQDN, rather than the leaf name (the default). + In Linux containers, this means setting the FQDN in the + hostname field of the kernel (the nodename field of struct + utsname). In Windows containers, this means setting the + registry value of hostname for the registry key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters + to FQDN. If a pod does not have FQDN, this has no effect. + Default to false. + type: boolean + shareProcessNamespace: + description: 'Share a single process namespace between all + of the containers in a pod. When this is set containers + will be able to view and signal processes from other containers + in the same pod, and the first process in each container + will not be assigned PID 1. HostPID and ShareProcessNamespace + cannot both be set. Optional: Default to false.' + type: boolean + subdomain: + description: If specified, the fully qualified Pod hostname + will be "...svc.". If not specified, the pod will not have a domainname + at all. + type: string + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod needs to + terminate gracefully. May be decreased in delete request. + Value must be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity to + shut down). If this value is nil, the default grace period + will be used instead. The grace period is the duration in + seconds after the processes running in the pod are sent + a termination signal and the time when the processes are + forcibly halted with a kill signal. Set this value longer + than the expected cleanup time for your process. Defaults + to 30 seconds. + format: int64 + type: integer + tolerations: + description: If specified, the pod's tolerations. + items: + description: The pod this Toleration is attached to tolerates + any taint that matches the triple using + the matching operator . + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. When specified, + allowed values are NoSchedule, PreferNoSchedule and + NoExecute. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. If the + key is empty, operator must be Exists; this combination + means to match all values and all keys. + type: string + operator: + description: Operator represents a key's relationship + to the value. Valid operators are Exists and Equal. + Defaults to Equal. Exists is equivalent to wildcard + for value, so that a pod can tolerate all taints of + a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period + of time the toleration (which must be of effect NoExecute, + otherwise this field is ignored) tolerates the taint. + By default, it is not set, which means tolerate the + taint forever (do not evict). Zero and negative values + will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. If the operator is Exists, the value should + be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + description: TopologySpreadConstraints describes how a group + of pods ought to spread across topology domains. Scheduler + will schedule pods in a way which abides by the constraints. + All topologySpreadConstraints are ANDed. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. Pods that match this label selector are counted + to determine the number of pods in their corresponding + topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + matchLabelKeys: + description: MatchLabelKeys is a set of pod label keys + to select the pods over which spreading will be calculated. + The keys are used to lookup values from the incoming + pod labels, those key-value labels are ANDed with + labelSelector to select the group of existing pods + over which spreading will be calculated for the incoming + pod. Keys that don't exist in the incoming pod labels + will be ignored. A null or empty list means only match + against labelSelector. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: 'MaxSkew describes the degree to which + pods may be unevenly distributed. When `whenUnsatisfiable=DoNotSchedule`, + it is the maximum permitted difference between the + number of matching pods in the target topology and + the global minimum. The global minimum is the minimum + number of matching pods in an eligible domain or zero + if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to + 1, and pods with the same labelSelector spread as + 2/2/1: In this case, the global minimum is 1. | zone1 + | zone2 | zone3 | | P P | P P | P | - if MaxSkew + is 1, incoming pod can only be scheduled to zone3 + to become 2/2/2; scheduling it onto zone1(zone2) would + make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto + any zone. When `whenUnsatisfiable=ScheduleAnyway`, + it is used to give higher precedence to topologies + that satisfy it. It''s a required field. Default value + is 1 and 0 is not allowed.' + format: int32 + type: integer + minDomains: + description: "MinDomains indicates a minimum number + of eligible domains. When the number of eligible domains + with matching topology keys is less than minDomains, + Pod Topology Spread treats \"global minimum\" as 0, + and then the calculation of Skew is performed. And + when the number of eligible domains with matching + topology keys equals or greater than minDomains, this + value has no effect on scheduling. As a result, when + the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to + those domains. If value is nil, the constraint behaves + as if MinDomains is equal to 1. Valid values are integers + greater than 0. When value is not nil, WhenUnsatisfiable + must be DoNotSchedule. \n For example, in a 3-zone + cluster, MaxSkew is set to 2, MinDomains is set to + 5 and pods with the same labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | | P P | P P | P P | + The number of domains is less than 5(MinDomains), + so \"global minimum\" is treated as 0. In this situation, + new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod + is scheduled to any of the three zones, it will violate + MaxSkew. \n This is a beta field and requires the + MinDomainsInPodTopologySpread feature gate to be enabled + (enabled by default)." + format: int32 + type: integer + nodeAffinityPolicy: + description: "NodeAffinityPolicy indicates how we will + treat Pod's nodeAffinity/nodeSelector when calculating + pod topology spread skew. Options are: - Honor: only + nodes matching nodeAffinity/nodeSelector are included + in the calculations. - Ignore: nodeAffinity/nodeSelector + are ignored. All nodes are included in the calculations. + \n If this value is nil, the behavior is equivalent + to the Honor policy. This is a beta-level feature + default enabled by the NodeInclusionPolicyInPodTopologySpread + feature flag." + type: string + nodeTaintsPolicy: + description: "NodeTaintsPolicy indicates how we will + treat node taints when calculating pod topology spread + skew. Options are: - Honor: nodes without taints, + along with tainted nodes for which the incoming pod + has a toleration, are included. - Ignore: node taints + are ignored. All nodes are included. \n If this value + is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the + NodeInclusionPolicyInPodTopologySpread feature flag." + type: string + topologyKey: + description: TopologyKey is the key of node labels. + Nodes that have a label with this key and identical + values are considered to be in the same topology. + We consider each as a "bucket", and try + to put balanced number of pods into each bucket. We + define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose + nodes meet the requirements of nodeAffinityPolicy + and nodeTaintsPolicy. e.g. If TopologyKey is "kubernetes.io/hostname", + each Node is a domain of that topology. And, if TopologyKey + is "topology.kubernetes.io/zone", each zone is a domain + of that topology. It's a required field. + type: string + whenUnsatisfiable: + description: 'WhenUnsatisfiable indicates how to deal + with a pod if it doesn''t satisfy the spread constraint. + - DoNotSchedule (default) tells the scheduler not + to schedule it. - ScheduleAnyway tells the scheduler + to schedule the pod in any location, but giving higher + precedence to topologies that would help reduce the + skew. A constraint is considered "Unsatisfiable" for + an incoming pod if and only if every possible node + assignment for that pod would violate "MaxSkew" on + some topology. For example, in a 3-zone cluster, MaxSkew + is set to 1, and pods with the same labelSelector + spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P + | P | P | If WhenUnsatisfiable is set to DoNotSchedule, + incoming pod can only be scheduled to zone2(zone3) + to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) + satisfies MaxSkew(1). In other words, the cluster + can still be imbalanced, but scheduler won''t make + it *more* imbalanced. It''s a required field.' + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + description: 'List of volumes that can be mounted by containers + belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes' + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'awsElasticBlockStore represents an AWS + Disk resource that is attached to a kubelet''s host + machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + partition: + description: 'partition is the partition in the + volume that you want to mount. If omitted, the + default is to mount by volume name. Examples: + For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda + is "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'readOnly value true will force the + readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'volumeID is unique ID of the persistent + disk resource in AWS (Amazon EBS volume). More + info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: fsType is Filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: readOnly Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: 'monitors is Required: Monitors is + a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: 'readOnly is Optional: Defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile + is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is + reference to the authentication secret for User, + default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + 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 + user: + description: 'user is optional: User is the rados + user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'cinder represents a cinder volume attached + and mounted on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'secretRef is optional: points to a + secret object containing parameters used to connect + to OpenStack.' + 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 + volumeID: + description: 'volumeID used to identify the volume + in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items if unspecified, each key-value + pair in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. Must + be an octal value between 0000 and 0777 + or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON + requires decimal values for mode bits. If + not specified, the volume defaultMode will + be used. This might be in conflict with + other options that affect the file mode, + like fsGroup, and the result can be other + mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be an + absolute path. May not contain the path + element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: driver is the name of the CSI driver + that handles this volume. Consult with your admin + for the correct name as registered in the cluster. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed + to the associated CSI driver which will determine + the default filesystem to apply. + type: string + nodePublishSecretRef: + description: nodePublishSecretRef is a reference + to the secret object containing sensitive information + to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no + secret is required. If the secret object contains + more than one secret, all secret references are + passed. + 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 + readOnly: + description: readOnly specifies a read-only configuration + for the volume. Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: volumeAttributes stores driver-specific + properties that are passed to the CSI driver. + Consult your driver's documentation for supported + values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default. Must be a Optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits used to + set permissions on this file, must be an + octal value between 0000 and 0777 or a decimal + value between 0 and 511. YAML accepts both + octal and decimal values, JSON requires + decimal values for mode bits. If not specified, + the volume defaultMode will be used. This + might be in conflict with other options + that affect the file mode, like fsGroup, + and the result can be other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'emptyDir represents a temporary directory + that shares a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'medium represents what type of storage + medium should back this directory. The default + is "" which means to use the node''s default medium. + Must be an empty string (default) or Memory. More + info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: 'sizeLimit is the total amount of local + storage required for this EmptyDir volume. The + size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified + here and the sum of memory limits of all containers + in a pod. The default is nil which means that + the limit is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: "ephemeral represents a volume that is + handled by a cluster storage driver. The volume's + lifecycle is tied to the pod that defines it - it + will be created before the pod starts, and deleted + when the pod is removed. \n Use this if: a) the volume + is only needed while the pod runs, b) features of + normal volumes like restoring from snapshot or capacity + tracking are needed, c) the storage driver is specified + through a storage class, and d) the storage driver + supports dynamic volume provisioning through a PersistentVolumeClaim + (see EphemeralVolumeSource for more information on + the connection between this volume type and PersistentVolumeClaim). + \n Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the + lifecycle of an individual pod. \n Use CSI for light-weight + local ephemeral volumes if the CSI driver is meant + to be used that way - see the documentation of the + driver for more information. \n A pod can use both + types of ephemeral volumes and persistent volumes + at the same time." + properties: + volumeClaimTemplate: + description: "Will be used to create a stand-alone + PVC to provision the volume. The pod in which + this EphemeralVolumeSource is embedded will be + the owner of the PVC, i.e. the PVC will be deleted + together with the pod. The name of the PVC will + be `-` where `` + is the name from the `PodSpec.Volumes` array entry. + Pod validation will reject the pod if the concatenated + name is not valid for a PVC (for example, too + long). \n An existing PVC with that name that + is not owned by the pod will *not* be used for + the pod to avoid using an unrelated volume by + mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created + PVC is meant to be used by the pod, the PVC has + to updated with an owner reference to the pod + once the pod exists. Normally this should not + be necessary, but it may be useful when manually + reconstructing a broken cluster. \n This field + is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. \n Required, + must not be nil." + properties: + metadata: + description: May contain labels and annotations + that will be copied into the PVC when creating + it. No other fields are allowed and will be + rejected during validation. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: The specification for the PersistentVolumeClaim. + The entire content is copied unchanged into + the PVC that gets created from this template. + The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: 'accessModes contains the desired + access modes the volume should have. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'dataSource field can be used + to specify either: * An existing VolumeSnapshot + object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller + can support the specified data source, + it will create a new volume based on the + contents of the specified data source. + When the AnyVolumeDataSource feature gate + is enabled, dataSource contents will be + copied to dataSourceRef, and dataSourceRef + contents will be copied to dataSource + when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef + will not be copied to dataSource.' + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. If + APIGroup is not specified, the specified + Kind must be in the core API group. + For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + dataSourceRef: + description: 'dataSourceRef specifies the + object from which to populate the volume + with data, if a non-empty volume is desired. + This may be any object from a non-empty + API group (non core object) or a PersistentVolumeClaim + object. When this field is specified, + volume binding will only succeed if the + type of the specified object matches some + installed volume populator or dynamic + provisioner. This field will replace the + functionality of the dataSource field + and as such if both fields are non-empty, + they must have the same value. For backwards + compatibility, when namespace isn''t specified + in dataSourceRef, both fields (dataSource + and dataSourceRef) will be set to the + same value automatically if one of them + is empty and the other is non-empty. When + namespace is specified in dataSourceRef, + dataSource isn''t set to the same value + and must be empty. There are three important + differences between dataSource and dataSourceRef: + * While dataSource only allows two specific + types of objects, dataSourceRef allows + any non-core object, as well as PersistentVolumeClaim + objects. * While dataSource ignores disallowed + values (dropping them), dataSourceRef + preserves all values, and generates an + error if a disallowed value is specified. + * While dataSource only allows local objects, + dataSourceRef allows objects in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource + feature gate to be enabled. (Alpha) Using + the namespace field of dataSourceRef requires + the CrossNamespaceVolumeDataSource feature + gate to be enabled.' + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. If + APIGroup is not specified, the specified + Kind must be in the core API group. + For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: Namespace is the namespace + of resource being referenced Note + that when a namespace is specified, + a gateway.networking.k8s.io/ReferenceGrant + object is required in the referent + namespace to allow that namespace's + owner to accept the reference. See + the ReferenceGrant documentation for + details. (Alpha) This field requires + the CrossNamespaceVolumeDataSource + feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: 'resources represents the minimum + resources the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to + specify resource requirements that are + lower than previous value but must still + be higher than capacity recorded in the + status field of the claim. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + claims: + description: "Claims lists the names + of resources, defined in spec.resourceClaims, + that are used by this container. \n + This is an alpha field and requires + enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the + name of one entry in pod.spec.resourceClaims + of the Pod where this field + is used. It makes that resource + available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum + amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the + minimum amount of compute resources + required. If Requests is omitted for + a container, it defaults to Limits + if that is explicitly specified, otherwise + to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + storageClassName: + description: 'storageClassName is the name + of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeMode: + description: volumeMode defines what type + of volume is required by the claim. Value + of Filesystem is implied when not included + in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: 'fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. TODO: how + do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'readOnly is Optional: Defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'wwids Optional: FC volume world wide + identifiers (wwids) Either wwids or combination + of targetWWNs and lun must be set, but not both + simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: flexVolume represents a generic volume + resource that is provisioned/attached using an exec + based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". The + default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: 'readOnly is Optional: defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'secretRef is Optional: secretRef is + reference to the secret object containing sensitive + information to pass to the plugin scripts. This + may be empty if no secret object is specified. + If the secret object contains more than one secret, + all secrets are passed to the plugin scripts.' + 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 + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: datasetName is Name of the dataset + stored as metadata -> name on the dataset for + Flocker should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'gcePersistentDisk represents a GCE Disk + resource that is attached to a kubelet''s host machine + and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'fsType is filesystem type of the volume + that you want to mount. Tip: Ensure that the filesystem + type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + partition: + description: 'partition is the partition in the + volume that you want to mount. If omitted, the + default is to mount by volume name. Examples: + For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda + is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'pdName is unique name of the PD resource + in GCE. Used to identify the disk in GCE. More + info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'gitRepo represents a git repository at + a particular revision. DEPRECATED: GitRepo is deprecated. + To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo + using git, then mount the EmptyDir into the Pod''s + container.' + properties: + directory: + description: directory is the target directory name. + Must not contain or start with '..'. If '.' is + supplied, the volume directory will be the git + repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory + with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'glusterfs represents a Glusterfs mount + on the host that shares a pod''s lifetime. More info: + https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'endpoints is the endpoint name that + details Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'readOnly here will force the Glusterfs + volume to be mounted with read-only permissions. + Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'hostPath represents a pre-existing file + or directory on the host machine that is directly + exposed to the container. This is generally used for + system agents or other privileged things that are + allowed to see the host machine. Most containers will + NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use + host directory mounts and who can/can not mount host + directories as read/write.' + properties: + path: + description: 'path of the directory on the host. + If the path is a symlink, it will follow the link + to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'type for HostPath Volume Defaults + to "" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'iscsi represents an ISCSI Disk resource + that is attached to a kubelet''s host machine and + then exposed to the pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. If initiatorName is specified with iscsiInterface + simultaneously, new iSCSI interface : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iscsiInterface is the interface Name + that uses an iSCSI transport. Defaults to 'default' + (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal + List. The portal is either an IP or ip_addr:port + if the port is other than default (typically TCP + ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + 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 + targetPortal: + description: targetPortal is iSCSI Target Portal. + The Portal is either an IP or ip_addr:port if + the port is other than default (typically TCP + ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'name of the volume. Must be a DNS_LABEL + and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'nfs represents an NFS mount on the host + that shares a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'readOnly here will force the NFS export + to be mounted with read-only permissions. Defaults + to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'server is the hostname or IP address + of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'persistentVolumeClaimVolumeSource represents + a reference to a PersistentVolumeClaim in the same + namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: readOnly Will force the ReadOnly setting + in VolumeMounts. Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: fSType represents the filesystem type + to mount Must be a filesystem type supported by + the host operating system. Ex. "ext4", "xfs". + Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used + to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Directories within the path + are not affected by this setting. This might be + in conflict with other options that affect the + file mode, like fsGroup, and the result can be + other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: items if unspecified, each + key-value pair in the Data field of + the referenced ConfigMap will be projected + into the volume as a file whose name + is the key and content is the value. + If specified, the listed keys will be + projected into the specified paths, + and unlisted keys will not be present. + If a key is specified which is not present + in the ConfigMap, the volume setup will + error unless it is marked optional. + Paths must be relative and may not contain + the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file. Must be an octal + value between 0000 and 0777 or + a decimal value between 0 and + 511. YAML accepts both octal and + decimal values, JSON requires + decimal values for mode bits. + If not specified, the volume defaultMode + will be used. This might be in + conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: path is the relative + path of the file to map the key + to. May not be an absolute path. + May not contain the path element + '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits + used to set permissions on this + file, must be an octal value between + 0000 and 0777 or a decimal value + between 0 and 511. YAML accepts + both octal and decimal values, + JSON requires decimal values for + mode bits. If not specified, the + volume defaultMode will be used. + This might be in conflict with + other options that affect the + file mode, like fsGroup, and the + result can be other mode bits + set.' + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, requests.cpu and + requests.memory) are currently + supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: items if unspecified, each + key-value pair in the Data field of + the referenced Secret will be projected + into the volume as a file whose name + is the key and content is the value. + If specified, the listed keys will be + projected into the specified paths, + and unlisted keys will not be present. + If a key is specified which is not present + in the Secret, the volume setup will + error unless it is marked optional. + Paths must be relative and may not contain + the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file. Must be an octal + value between 0000 and 0777 or + a decimal value between 0 and + 511. YAML accepts both octal and + decimal values, JSON requires + decimal values for mode bits. + If not specified, the volume defaultMode + will be used. This might be in + conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: path is the relative + path of the file to map the key + to. May not be an absolute path. + May not contain the path element + '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended + audience of the token. A recipient of + a token must identify itself with an + identifier specified in the audience + of the token, and otherwise should reject + the token. The audience defaults to + the identifier of the apiserver. + type: string + expirationSeconds: + description: expirationSeconds is the + requested duration of validity of the + service account token. As the token + approaches expiration, the kubelet volume + plugin will proactively rotate the service + account token. The kubelet will start + trying to rotate the token if the token + is older than 80 percent of its time + to live or if the token is older than + 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: path is the path relative + to the mount point of the file to project + the token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: group to map volume access to Default + is no group + type: string + readOnly: + description: readOnly here will force the Quobyte + volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: registry represents a single or multiple + Quobyte Registry services specified as a string + as host:port pair (multiple entries are separated + with commas) which acts as the central registry + for volumes + type: string + tenant: + description: tenant owning the given Quobyte volume + in the Backend Used with dynamically provisioned + Quobyte volumes, value is set by the plugin + type: string + user: + description: user to map volume access to Defaults + to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'rbd represents a Rados Block Device mount + on the host that shares a pod''s lifetime. More info: + https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + image: + description: 'image is the rados image name. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'keyring is the path to key ring for + RBDUser. Default is /etc/ceph/keyring. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'pool is the rados pool name. Default + is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'secretRef is name of the authentication + secret for RBDUser. If provided overrides keyring. + Default is nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + 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 + user: + description: 'user is the rados user name. Default + is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Default + is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: readOnly Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretRef: + description: secretRef references to the secret + for ScaleIO user and other sensitive information. + If this is not provided, Login operation will + fail. + 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 + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: volumeName is the name of a volume + already created in the ScaleIO system that is + associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'secret represents a secret that should + populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items If unspecified, each key-value + pair in the Data field of the referenced Secret + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. Must + be an octal value between 0000 and 0777 + or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON + requires decimal values for mode bits. If + not specified, the volume defaultMode will + be used. This might be in conflict with + other options that affect the file mode, + like fsGroup, and the result can be other + mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be an + absolute path. May not contain the path + element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: 'secretName is the name of the secret + in the pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretRef: + description: secretRef specifies the secret to use + for obtaining the StorageOS API credentials. If + not specified, default values will be attempted. + 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 + volumeName: + description: volumeName is the human-readable name + of the StorageOS volume. Volume names are only + unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope + of the volume within StorageOS. If no namespace + is specified then the Pod's namespace will be + used. This allows the Kubernetes name scoping + to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default + behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do + not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: fsType is filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - containers + type: object + type: object + updateStrategy: + default: Serial + description: 'UpdateStrategy, Pods update strategy. serial: update + Pods one by one that guarantee minimum component unavailable time. + Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component un-writable + time. Learner, Follower(minority) in parallel -> Follower(majority) + -> Leader, keep majority online all the time. parallel: force parallel' + enum: + - Serial + - BestEffortParallel + - Parallel + type: string + volumeClaimTemplates: + description: volumeClaimTemplates is a list of claims that pods are + allowed to reference. The ConsensusSet controller is responsible + for mapping network identities to claims in a way that maintains + the identity of a pod. Every claim in this list must have at least + one matching (by name) volumeMount in one container in the template. + A claim in this list takes precedence over any volumes in the template, + with the same name. + items: + description: PersistentVolumeClaim is a user's request for and claim + to a persistent volume + 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: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: 'spec defines the desired characteristics of a + volume requested by a pod author. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + accessModes: + description: 'accessModes contains the desired access modes + the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) If the provisioner + or an external controller can support the specified data + source, it will create a new volume based on the contents + of the specified data source. When the AnyVolumeDataSource + feature gate is enabled, dataSource contents will be copied + to dataSourceRef, and dataSourceRef contents will be copied + to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will + not be copied to dataSource.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, the + specified Kind must be in the core API group. For + any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + dataSourceRef: + description: 'dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty volume + is desired. This may be any object from a non-empty API + group (non core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only + succeed if the type of the specified object matches some + installed volume populator or dynamic provisioner. This + field will replace the functionality of the dataSource + field and as such if both fields are non-empty, they must + have the same value. For backwards compatibility, when + namespace isn''t specified in dataSourceRef, both fields + (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other + is non-empty. When namespace is specified in dataSourceRef, + dataSource isn''t set to the same value and must be empty. + There are three important differences between dataSource + and dataSourceRef: * While dataSource only allows two + specific types of objects, dataSourceRef allows any non-core + object, as well as PersistentVolumeClaim objects. * While + dataSource ignores disallowed values (dropping them), + dataSourceRef preserves all values, and generates an error + if a disallowed value is specified. * While dataSource + only allows local objects, dataSourceRef allows objects + in any namespaces. (Beta) Using this field requires the + AnyVolumeDataSource feature gate to be enabled. (Alpha) + Using the namespace field of dataSourceRef requires the + CrossNamespaceVolumeDataSource feature gate to be enabled.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, the + specified Kind must be in the core API group. For + any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource + being referenced Note that when a namespace is specified, + a gateway.networking.k8s.io/ReferenceGrant object + is required in the referent namespace to allow that + namespace's owner to accept the reference. See the + ReferenceGrant documentation for details. (Alpha) + This field requires the CrossNamespaceVolumeDataSource + feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: 'resources represents the minimum resources + the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to specify resource + requirements that are lower than previous value but must + still be higher than capacity recorded in the status field + of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + claims: + description: "Claims lists the names of resources, defined + in spec.resourceClaims, that are used by this container. + \n This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. \n This field + is immutable." + items: + description: ResourceClaim references one entry in + PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec.resourceClaims of the Pod where + this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of + compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is omitted + for a container, it defaults to Limits if that is + explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: selector is a label query over volumes to consider + for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + storageClassName: + description: 'storageClassName is the name of the StorageClass + required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeMode: + description: volumeMode defines what type of volume is required + by the claim. Value of Filesystem is implied when not + included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: 'status represents the current information/status + of a persistent volume claim. Read-only. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + accessModes: + description: 'accessModes contains the actual access modes + the volume backing the PVC has. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: allocatedResources is the storage resource + within AllocatedResources tracks the capacity allocated + to a PVC. It may be larger than the actual capacity when + a volume expansion operation is requested. For storage + quota, the larger value from allocatedResources and PVC.spec.resources + is used. If allocatedResources is not set, PVC.spec.resources + alone is used for quota calculation. If a volume expansion + capacity request is lowered, allocatedResources is only + lowered if there are no expansion operations in progress + and if the actual volume capacity is equal or lower than + the requested capacity. This is an alpha field and requires + enabling RecoverVolumeExpansionFailure feature. + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources of + the underlying volume. + type: object + conditions: + description: conditions is the current Condition of persistent + volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contails details + about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed the + condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: reason is a unique, this should be a + short, machine understandable string that gives + the reason for condition's last transition. If it + reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType is + a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + resizeStatus: + description: resizeStatus stores status of resize operation. + ResizeStatus is not set by default but when expansion + is complete resizeStatus is set to empty string by resize + controller or kubelet. This is an alpha field and requires + enabling RecoverVolumeExpansionFailure feature. + type: string + type: object + type: object + type: array + required: + - roleObservation + - roles + - service + - template + type: object + status: + description: ConsensusSetStatus defines the observed state of ConsensusSet + properties: + availableReplicas: + description: Total number of available pods (ready for at least minReadySeconds) + targeted by this statefulset. + format: int32 + type: integer + collisionCount: + description: collisionCount is the count of hash collisions for the + StatefulSet. The StatefulSet controller uses this field as a collision + avoidance mechanism when it needs to create the name for the newest + ControllerRevision. + format: int32 + type: integer + conditions: + description: Represents the latest available observations of a statefulset's + current state. + items: + description: StatefulSetCondition describes the state of a statefulset + at a certain point. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of statefulset condition. + type: string + required: + - status + - type + type: object + type: array + currentReplicas: + description: currentReplicas is the number of Pods created by the + StatefulSet controller from the StatefulSet version indicated by + currentRevision. + format: int32 + type: integer + currentRevision: + description: currentRevision, if not empty, indicates the version + of the StatefulSet used to generate Pods in the sequence [0,currentReplicas). + type: string + initReplicas: + description: InitReplicas is the number of pods(members) when cluster + first initialized it's set to spec.Replicas at object creation time + and never changes + format: int32 + type: integer + membersStatus: + description: members' status. + items: + properties: + podName: + default: Unknown + description: PodName pod name. + type: string + role: + properties: + accessMode: + default: ReadWrite + description: AccessMode, what service this member capable. + enum: + - None + - Readonly + - ReadWrite + type: string + canVote: + default: true + description: CanVote, whether this member has voting rights + type: boolean + isLeader: + default: false + description: IsLeader, whether this member is the leader + type: boolean + name: + default: leader + description: Name, role name. + type: string + required: + - accessMode + - name + type: object + required: + - podName + - role + type: object + type: array + observedGeneration: + description: observedGeneration is the most recent generation observed + for this StatefulSet. It corresponds to the StatefulSet's generation, + which is updated on mutation by the API Server. + format: int64 + type: integer + readyInitReplicas: + description: ReadyInitReplicas is the number of pods(members) already + in MembersStatus in the cluster initialization stage will never + change once equals to InitReplicas + format: int32 + type: integer + readyReplicas: + description: readyReplicas is the number of pods created for this + StatefulSet with a Ready Condition. + format: int32 + type: integer + replicas: + description: replicas is the number of Pods created by the StatefulSet + controller. + format: int32 + type: integer + updateRevision: + description: updateRevision, if not empty, indicates the version of + the StatefulSet used to generate Pods in the sequence [replicas-updatedReplicas,replicas) + type: string + updatedReplicas: + description: updatedReplicas is the number of Pods created by the + StatefulSet controller from the StatefulSet version indicated by + updateRevision. + format: int32 + type: integer + required: + - initReplicas + - replicas + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 7de82f64b..4d1ebf0c8 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -2,6 +2,7 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: +- bases/apps.kubeblocks.io_backuppolicytemplates.yaml - bases/apps.kubeblocks.io_clusters.yaml - bases/apps.kubeblocks.io_clusterdefinitions.yaml - bases/apps.kubeblocks.io_clusterversions.yaml @@ -11,9 +12,10 @@ resources: - bases/dataprotection.kubeblocks.io_backuppolicies.yaml - bases/dataprotection.kubeblocks.io_backups.yaml - bases/dataprotection.kubeblocks.io_restorejobs.yaml -- bases/dataprotection.kubeblocks.io_backuppolicytemplates.yaml - bases/extensions.kubeblocks.io_addons.yaml -- bases/apps.kubeblocks.io_classfamilies.yaml +- bases/apps.kubeblocks.io_componentresourceconstraints.yaml +- bases/apps.kubeblocks.io_componentclassdefinitions.yaml +- bases/workloads.kubeblocks.io_consensussets.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -33,7 +35,9 @@ patchesStrategicMerge: #- patches/webhook_in_hostpreflights.yaml #- patches/webhook_in_preflights.yaml #- patches/webhook_in_addons.yaml -#- patches/webhook_in_classfamilies.yaml +#- patches/webhook_in_componentresourceconstraints.yaml +#- patches/webhook_in_componentclassdefinitions.yaml +#- patches/webhook_in_consensussets.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -52,7 +56,9 @@ patchesStrategicMerge: #- patches/cainjection_in_hostpreflights.yaml #- patches/cainjection_in_preflights.yaml #- patches/cainjection_in_addonspecs.yaml -#- patches/cainjection_in_classfamilies.yaml +#- patches/cainjection_in_componentresourceconstraints.yaml +#- patches/cainjection_in_componentclassdefinitions.yaml +#- patches/cainjection_in_consensussets.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_dataprotection_backuppolicytemplates.yaml b/config/crd/patches/cainjection_in_apps_backuppolicytemplates.yaml similarity index 81% rename from config/crd/patches/cainjection_in_dataprotection_backuppolicytemplates.yaml rename to config/crd/patches/cainjection_in_apps_backuppolicytemplates.yaml index 8340a38e6..7600d148d 100644 --- a/config/crd/patches/cainjection_in_dataprotection_backuppolicytemplates.yaml +++ b/config/crd/patches/cainjection_in_apps_backuppolicytemplates.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: backuppolicytemplates.dataprotection.kubeblocks.io + name: backuppolicytemplates.apps.kubeblocks.io diff --git a/config/crd/patches/cainjection_in_apps_componentclassdefinitions.yaml b/config/crd/patches/cainjection_in_apps_componentclassdefinitions.yaml new file mode 100644 index 000000000..128d2cbe3 --- /dev/null +++ b/config/crd/patches/cainjection_in_apps_componentclassdefinitions.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: componentclassdefinitions.apps.kubeblocks.io diff --git a/config/crd/patches/cainjection_in_apps_componentresourceconstraints.yaml b/config/crd/patches/cainjection_in_apps_componentresourceconstraints.yaml new file mode 100644 index 000000000..611d9c6cb --- /dev/null +++ b/config/crd/patches/cainjection_in_apps_componentresourceconstraints.yaml @@ -0,0 +1,7 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: componentresourceconstraints.apps.kubeblocks.io diff --git a/config/crd/patches/cainjection_in_apps_classfamilies.yaml b/config/crd/patches/cainjection_in_workloads_consensussets.yaml similarity index 84% rename from config/crd/patches/cainjection_in_apps_classfamilies.yaml rename to config/crd/patches/cainjection_in_workloads_consensussets.yaml index bc1f3ff5c..f505ec87d 100644 --- a/config/crd/patches/cainjection_in_apps_classfamilies.yaml +++ b/config/crd/patches/cainjection_in_workloads_consensussets.yaml @@ -4,4 +4,4 @@ kind: CustomResourceDefinition metadata: annotations: cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) - name: classfamilies.apps.kubeblocks.io + name: consensussets.workloads.kubeblocks.io diff --git a/config/crd/patches/webhook_in_dataprotection_backuppolicytemplates.yaml b/config/crd/patches/webhook_in_apps_backuppolicytemplates.yaml similarity index 85% rename from config/crd/patches/webhook_in_dataprotection_backuppolicytemplates.yaml rename to config/crd/patches/webhook_in_apps_backuppolicytemplates.yaml index 2dea48c6a..c6cba6e35 100644 --- a/config/crd/patches/webhook_in_dataprotection_backuppolicytemplates.yaml +++ b/config/crd/patches/webhook_in_apps_backuppolicytemplates.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: backuppolicytemplates.dataprotection.kubeblocks.io + name: backuppolicytemplates.apps.kubeblocks.io spec: conversion: strategy: Webhook diff --git a/config/crd/patches/webhook_in_apps_componentclassdefinitions.yaml b/config/crd/patches/webhook_in_apps_componentclassdefinitions.yaml new file mode 100644 index 000000000..46abc75c9 --- /dev/null +++ b/config/crd/patches/webhook_in_apps_componentclassdefinitions.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: componentclassdefinitions.apps.kubeblocks.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_apps_componentresourceconstraints.yaml b/config/crd/patches/webhook_in_apps_componentresourceconstraints.yaml new file mode 100644 index 000000000..ea37a59b6 --- /dev/null +++ b/config/crd/patches/webhook_in_apps_componentresourceconstraints.yaml @@ -0,0 +1,16 @@ +# The following patch enables a conversion webhook for the CRD +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: componentresourceconstraints.apps.kubeblocks.io +spec: + conversion: + strategy: Webhook + webhook: + clientConfig: + service: + namespace: system + name: webhook-service + path: /convert + conversionReviewVersions: + - v1 diff --git a/config/crd/patches/webhook_in_apps_classfamilies.yaml b/config/crd/patches/webhook_in_workloads_consensussets.yaml similarity index 88% rename from config/crd/patches/webhook_in_apps_classfamilies.yaml rename to config/crd/patches/webhook_in_workloads_consensussets.yaml index 1667132fe..c27e9378e 100644 --- a/config/crd/patches/webhook_in_apps_classfamilies.yaml +++ b/config/crd/patches/webhook_in_workloads_consensussets.yaml @@ -2,7 +2,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: - name: classfamilies.apps.kubeblocks.io + name: consensussets.workloads.kubeblocks.io spec: conversion: strategy: Webhook diff --git a/config/probe/components/binding_custom.yaml b/config/probe/components/binding_custom.yaml new file mode 100644 index 000000000..f09216f65 --- /dev/null +++ b/config/probe/components/binding_custom.yaml @@ -0,0 +1,8 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: custom +spec: + type: bindings.custom + version: v1 + metadata: [] \ No newline at end of file diff --git a/config/probe/components/binding_kafka.yaml b/config/probe/components/binding_kafka.yaml new file mode 100644 index 000000000..bdc3eda57 --- /dev/null +++ b/config/probe/components/binding_kafka.yaml @@ -0,0 +1,18 @@ +apiVersion: dapr.io/v1alpha1 +kind: Component +metadata: + name: kafka +spec: + type: bindings.kafka + version: v1 + metadata: + - name: topics + value: "topic1,topic2" + - name: brokers + value: "localhost:9092,localhost:9093" + - name: publishTopic # Optional. Used for output bindings. + value: "topic3" + - name: authRequired # Required. + value: "false" + - name: initialOffset # Optional. Used for input bindings. + value: "newest" \ No newline at end of file diff --git a/config/probe/components/binding_mongodb.yaml b/config/probe/components/binding_mongodb.yaml index 03290456f..9f686633e 100644 --- a/config/probe/components/binding_mongodb.yaml +++ b/config/probe/components/binding_mongodb.yaml @@ -7,6 +7,6 @@ spec: version: v1 metadata: - name: host - value: "127.0.0.1:27018" + value: "127.0.0.1:27017" - name: params value: "?directConnection=true" diff --git a/config/rbac/dataprotection_backuppolicytemplate_editor_role.yaml b/config/rbac/apps_backuppolicytemplate_editor_role.yaml similarity index 85% rename from config/rbac/dataprotection_backuppolicytemplate_editor_role.yaml rename to config/rbac/apps_backuppolicytemplate_editor_role.yaml index 0c5f1c24e..ff9688280 100644 --- a/config/rbac/dataprotection_backuppolicytemplate_editor_role.yaml +++ b/config/rbac/apps_backuppolicytemplate_editor_role.yaml @@ -5,7 +5,7 @@ metadata: name: backuppolicytemplate-editor-role rules: - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates verbs: @@ -17,7 +17,7 @@ rules: - update - watch - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates/status verbs: diff --git a/config/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml b/config/rbac/apps_backuppolicytemplate_viewer_role.yaml similarity index 83% rename from config/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml rename to config/rbac/apps_backuppolicytemplate_viewer_role.yaml index b2f779fe8..aa2625708 100644 --- a/config/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml +++ b/config/rbac/apps_backuppolicytemplate_viewer_role.yaml @@ -5,7 +5,7 @@ metadata: name: backuppolicytemplate-viewer-role rules: - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates verbs: @@ -13,7 +13,7 @@ rules: - list - watch - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates/status verbs: diff --git a/config/rbac/apps_classfamily_editor_role.yaml b/config/rbac/apps_componentclassdefinition_editor_role.yaml similarity index 66% rename from config/rbac/apps_classfamily_editor_role.yaml rename to config/rbac/apps_componentclassdefinition_editor_role.yaml index 5a06154b4..0faf3fbe7 100644 --- a/config/rbac/apps_classfamily_editor_role.yaml +++ b/config/rbac/apps_componentclassdefinition_editor_role.yaml @@ -1,20 +1,20 @@ -# permissions for end users to edit classfamilies. +# permissions for end users to edit componentclassdefinitions. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: classfamily-editor-role + app.kubernetes.io/instance: componentclassdefinition-editor-role app.kubernetes.io/component: rbac app.kubernetes.io/created-by: kubeblocks app.kubernetes.io/part-of: kubeblocks app.kubernetes.io/managed-by: kustomize - name: classfamily-editor-role + name: componentclassdefinition-editor-role rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies + - componentclassdefinitions verbs: - create - delete @@ -26,6 +26,6 @@ rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies/status + - componentclassdefinitions/status verbs: - get diff --git a/config/rbac/apps_classfamily_viewer_role.yaml b/config/rbac/apps_componentclassdefinition_viewer_role.yaml similarity index 64% rename from config/rbac/apps_classfamily_viewer_role.yaml rename to config/rbac/apps_componentclassdefinition_viewer_role.yaml index d82810999..29e1d8f8d 100644 --- a/config/rbac/apps_classfamily_viewer_role.yaml +++ b/config/rbac/apps_componentclassdefinition_viewer_role.yaml @@ -1,20 +1,20 @@ -# permissions for end users to view classfamilies. +# permissions for end users to view componentclassdefinitions. apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: labels: app.kubernetes.io/name: clusterrole - app.kubernetes.io/instance: classfamily-viewer-role + app.kubernetes.io/instance: componentclassdefinition-viewer-role app.kubernetes.io/component: rbac app.kubernetes.io/created-by: kubeblocks app.kubernetes.io/part-of: kubeblocks app.kubernetes.io/managed-by: kustomize - name: classfamily-viewer-role + name: componentclassdefinition-viewer-role rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies + - componentclassdefinitions verbs: - get - list @@ -22,6 +22,6 @@ rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies/status + - componentclassdefinitions/status verbs: - get diff --git a/config/rbac/apps_componentresourceconstraint_editor_role.yaml b/config/rbac/apps_componentresourceconstraint_editor_role.yaml new file mode 100644 index 000000000..ff4e58c98 --- /dev/null +++ b/config/rbac/apps_componentresourceconstraint_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit componentresourceconstraints +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: componentresourceconstraint-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kubeblocks + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + name: componentresourceconstraint-editor-role +rules: +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints/status + verbs: + - get diff --git a/config/rbac/apps_componentresourceconstraint_viewer_role.yaml b/config/rbac/apps_componentresourceconstraint_viewer_role.yaml new file mode 100644 index 000000000..feae2809c --- /dev/null +++ b/config/rbac/apps_componentresourceconstraint_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view componentresourceconstraints +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: componentresourceconstraint-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kubeblocks + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + name: componentresourceconstraint-viewer-role +rules: +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints + verbs: + - get + - list + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index e2747ca6a..5352a83ea 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -30,30 +30,6 @@ rules: - deployments/status verbs: - get -- apiGroups: - - apps - resources: - - pods - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - apps - resources: - - pods/finalizers - verbs: - - update -- apiGroups: - - apps - resources: - - pods/status - verbs: - - get - apiGroups: - apps resources: @@ -106,11 +82,10 @@ rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies + - backuppolicytemplates verbs: - get - list - - watch - apiGroups: - apps.kubeblocks.io resources: @@ -189,6 +164,40 @@ rules: - get - patch - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions/finalizers + verbs: + - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions/status + verbs: + - get + - patch + - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints + verbs: + - get + - list + - watch - apiGroups: - apps.kubeblocks.io resources: @@ -383,6 +392,16 @@ rules: - persistentvolumeclaims/status verbs: - get +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -401,6 +420,19 @@ rules: - pods/exec verbs: - create +- apiGroups: + - "" + resources: + - pods/finalizers + verbs: + - update +- apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - list - apiGroups: - "" resources: @@ -496,32 +528,6 @@ rules: - get - patch - update -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates/finalizers - verbs: - - update -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates/status - verbs: - - get - - patch - - update - apiGroups: - dataprotection.kubeblocks.io resources: @@ -673,3 +679,29 @@ rules: - get - list - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/finalizers + verbs: + - update +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/status + verbs: + - get + - patch + - update diff --git a/config/rbac/workloads_consensusset_editor_role.yaml b/config/rbac/workloads_consensusset_editor_role.yaml new file mode 100644 index 000000000..1aa4b1efb --- /dev/null +++ b/config/rbac/workloads_consensusset_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit consensussets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: consensusset-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kubeblocks + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + name: consensusset-editor-role +rules: +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/status + verbs: + - get diff --git a/config/rbac/workloads_consensusset_viewer_role.yaml b/config/rbac/workloads_consensusset_viewer_role.yaml new file mode 100644 index 000000000..13bf9399f --- /dev/null +++ b/config/rbac/workloads_consensusset_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view consensussets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: consensusset-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: kubeblocks + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + name: consensusset-viewer-role +rules: +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets + verbs: + - get + - list + - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/status + verbs: + - get diff --git a/config/samples/apps_v1alpha1_componentclassdefinition.yaml b/config/samples/apps_v1alpha1_componentclassdefinition.yaml new file mode 100644 index 000000000..4e7209c0d --- /dev/null +++ b/config/samples/apps_v1alpha1_componentclassdefinition.yaml @@ -0,0 +1,12 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ComponentClassDefinition +metadata: + labels: + app.kubernetes.io/name: componentclassdefinition + app.kubernetes.io/instance: componentclassdefinition-sample + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: kubeblocks + name: componentclassdefinition-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/apps_v1alpha1_classfamily.yaml b/config/samples/apps_v1alpha1_componentresourceconstraint.yaml similarity index 53% rename from config/samples/apps_v1alpha1_classfamily.yaml rename to config/samples/apps_v1alpha1_componentresourceconstraint.yaml index af312721a..5e9f61c08 100644 --- a/config/samples/apps_v1alpha1_classfamily.yaml +++ b/config/samples/apps_v1alpha1_componentresourceconstraint.yaml @@ -1,12 +1,12 @@ apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClassFamily +kind: ComponentResourceConstraint metadata: labels: - app.kubernetes.io/name: classfamily - app.kubernetes.io/instance: classfamily-sample + app.kubernetes.io/name: componentresourceconstraint + app.kubernetes.io/instance: componentresourceconstraint-sample app.kubernetes.io/part-of: kubeblocks app.kuberentes.io/managed-by: kustomize app.kubernetes.io/created-by: kubeblocks - name: classfamily-sample + name: componentresourceconstraint-sample spec: # TODO(user): Add fields here diff --git a/config/samples/workloads_v1alpha1_consensusset.yaml b/config/samples/workloads_v1alpha1_consensusset.yaml new file mode 100644 index 000000000..d0bdb22f3 --- /dev/null +++ b/config/samples/workloads_v1alpha1_consensusset.yaml @@ -0,0 +1,12 @@ +apiVersion: workloads.kubeblocks.io/v1alpha1 +kind: ConsensusSet +metadata: + labels: + app.kubernetes.io/name: consensusset + app.kubernetes.io/instance: consensusset-sample + app.kubernetes.io/part-of: kubeblocks + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: kubeblocks + name: consensusset-sample +spec: + # TODO(user): Add fields here diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 6ddfb9474..2a10d46a2 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -45,6 +45,26 @@ webhooks: resources: - clusterdefinitions sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-workloads-kubeblocks-io-v1alpha1-consensusset + failurePolicy: Fail + name: mconsensusset.kb.io + rules: + - apiGroups: + - workloads.kubeblocks.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - consensussets + sideEffects: None --- apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -132,3 +152,23 @@ webhooks: resources: - opsrequests sideEffects: None +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-workloads-kubeblocks-io-v1alpha1-consensusset + failurePolicy: Fail + name: vconsensusset.kb.io + rules: + - apiGroups: + - workloads.kubeblocks.io + apiVersions: + - v1alpha1 + operations: + - CREATE + - UPDATE + resources: + - consensussets + sideEffects: None diff --git a/controllers/apps/class_controller.go b/controllers/apps/class_controller.go new file mode 100644 index 000000000..3494a14a7 --- /dev/null +++ b/controllers/apps/class_controller.go @@ -0,0 +1,120 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + "context" + "fmt" + + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/class" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=componentclassdefinitions,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=componentclassdefinitions/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=componentclassdefinitions/finalizers,verbs=update + +type ComponentClassReconciler struct { + client.Client + Scheme *k8sruntime.Scheme + Recorder record.EventRecorder +} + +func (r *ComponentClassReconciler) Reconcile(ctx context.Context, req reconcile.Request) (reconcile.Result, error) { + reqCtx := intctrlutil.RequestCtx{ + Ctx: ctx, + Req: req, + Log: log.FromContext(ctx).WithValues("classDefinition", req.NamespacedName), + Recorder: r.Recorder, + } + + classDefinition := &appsv1alpha1.ComponentClassDefinition{} + if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, classDefinition); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + ml := []client.ListOption{ + client.HasLabels{types.ResourceConstraintProviderLabelKey}, + } + constraintsList := &appsv1alpha1.ComponentResourceConstraintList{} + if err := r.Client.List(reqCtx.Ctx, constraintsList, ml...); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + constraintsMap := make(map[string]appsv1alpha1.ComponentResourceConstraint) + for idx := range constraintsList.Items { + cf := constraintsList.Items[idx] + if _, ok := cf.GetLabels()[types.ResourceConstraintProviderLabelKey]; !ok { + continue + } + constraintsMap[cf.GetName()] = cf + } + + res, err := intctrlutil.HandleCRDeletion(reqCtx, r, classDefinition, constant.DBClusterFinalizerName, func() (*ctrl.Result, error) { + // TODO validate if existing cluster reference classes being deleted + return nil, nil + }) + if res != nil { + return *res, err + } + + if classDefinition.Status.ObservedGeneration == classDefinition.Generation { + return intctrlutil.Reconciled() + } + + classInstances, err := class.ParseComponentClasses(*classDefinition) + if err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "parse component classes failed") + } + + patch := client.MergeFrom(classDefinition.DeepCopy()) + var classList []appsv1alpha1.ComponentClassInstance + for _, v := range classInstances { + constraint, ok := constraintsMap[v.ResourceConstraintRef] + if !ok { + return intctrlutil.CheckedRequeueWithError(nil, reqCtx.Log, fmt.Sprintf("resource constraint %s not found", v.ResourceConstraintRef)) + } + if !constraint.MatchClass(v) { + return intctrlutil.CheckedRequeueWithError(nil, reqCtx.Log, fmt.Sprintf("class %s does not conform to constraint %s", v.Name, v.ResourceConstraintRef)) + } + classList = append(classList, *v) + } + classDefinition.Status.Classes = classList + classDefinition.Status.ObservedGeneration = classDefinition.Generation + if err = r.Client.Status().Patch(ctx, classDefinition, patch); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "patch component class status failed") + } + + return intctrlutil.Reconciled() +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ComponentClassReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr).For(&appsv1alpha1.ComponentClassDefinition{}).Complete(r) +} diff --git a/controllers/apps/class_controller_test.go b/controllers/apps/class_controller_test.go new file mode 100644 index 000000000..baf466285 --- /dev/null +++ b/controllers/apps/class_controller_test.go @@ -0,0 +1,68 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" +) + +var _ = Describe("", func() { + + var componentClassDefinition *v1alpha1.ComponentClassDefinition + + cleanEnv := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete rest mocked objects + ml := client.HasLabels{testCtx.TestObjLabelKey} + testapps.ClearResources(&testCtx, intctrlutil.ComponentResourceConstraintSignature, ml) + testapps.ClearResources(&testCtx, intctrlutil.ComponentClassDefinitionSignature, ml) + } + + BeforeEach(cleanEnv) + + AfterEach(cleanEnv) + + It("Class should exist in status", func() { + constraint := testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName). + AddConstraints(testapps.GeneralResourceConstraint). + Create(&testCtx).GetObject() + + componentClassDefinition = testapps.NewComponentClassDefinitionFactory("custom", "apecloud-mysql", "mysql"). + AddClasses(constraint.Name, []string{testapps.Class1c1gName}). + Create(&testCtx).GetObject() + + key := client.ObjectKeyFromObject(componentClassDefinition) + Eventually(testapps.CheckObj(&testCtx, key, func(g Gomega, pobj *v1alpha1.ComponentClassDefinition) { + g.Expect(pobj.Status.Classes).ShouldNot(BeEmpty()) + g.Expect(pobj.Status.Classes[0].Name).Should(Equal(testapps.Class1c1gName)) + })).Should(Succeed()) + }) +}) diff --git a/controllers/apps/cluster_controller.go b/controllers/apps/cluster_controller.go index 018abcf9e..4d8bb728f 100644 --- a/controllers/apps/cluster_controller.go +++ b/controllers/apps/cluster_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -20,24 +23,26 @@ import ( "context" "time" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" "sigs.k8s.io/controller-runtime/pkg/source" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/k8score" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/lifecycle" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" @@ -47,6 +52,8 @@ import ( // +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters/status,verbs=get;update;patch // +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=clusters/finalizers,verbs=update +// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=backuppolicytemplates,verbs=get;list + // owned K8s core API resources controller-gen RBAC marker // full access on core API resources // +kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;watch;create;update;patch;delete;deletecollection @@ -67,6 +74,8 @@ import ( // +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims/status,verbs=get // +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims/finalizers,verbs=update +// +kubebuilder:rbac:groups=core,resources=persistentvolumes,verbs=get;list;watch;update;patch + // +kubebuilder:rbac:groups=apps,resources=replicasets,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=apps,resources=replicasets/status,verbs=get // +kubebuilder:rbac:groups=apps,resources=replicasets/finalizers,verbs=update @@ -85,17 +94,18 @@ import ( // read + update access // +kubebuilder:rbac:groups=core,resources=endpoints,verbs=get;list;watch;update;patch // +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;watch;update;patch +// +kubebuilder:rbac:groups=core,resources=pods/finalizers,verbs=update // +kubebuilder:rbac:groups=core,resources=pods/exec,verbs=create // read only + watch access // +kubebuilder:rbac:groups=storage.k8s.io,resources=storageclasses,verbs=get;list;watch // dataprotection get list and delete -// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicies,verbs=get;list;delete;deletecollection +// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicies,verbs=get;list;create;update;patch;delete;deletecollection // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups,verbs=get;list;delete;deletecollection -// classfamily get list -// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=classfamilies,verbs=get;list;watch +// componentresourceconstraint get list +// +kubebuilder:rbac:groups=apps.kubeblocks.io,resources=componentresourceconstraints,verbs=get;list;watch // ClusterReconciler reconciles a Cluster object type ClusterReconciler struct { @@ -104,15 +114,6 @@ type ClusterReconciler struct { Recorder record.EventRecorder } -// ClusterStatusEventHandler is the event handler for the cluster status event -type ClusterStatusEventHandler struct{} - -var _ k8score.EventHandler = &ClusterStatusEventHandler{} - -func init() { - k8score.EventHandlerMap["cluster-status-handler"] = &ClusterStatusEventHandler{} -} - // Reconcile is part of the main kubernetes reconciliation loop which aims to // move the current state of the cluster closer to the desired state. // @@ -129,21 +130,78 @@ func (r *ClusterReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct reqCtx.Log.V(1).Info("reconcile", "cluster", req.NamespacedName) requeueError := func(err error) (ctrl.Result, error) { - if re, ok := err.(lifecycle.RequeueError); ok { + if re, ok := err.(intctrlutil.RequeueError); ok { return intctrlutil.RequeueAfter(re.RequeueAfter(), reqCtx.Log, re.Reason()) } - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + return intctrlutil.RequeueWithError(err, reqCtx.Log, "") } - planBuilder := lifecycle.NewClusterPlanBuilder(reqCtx, r.Client, req, r.Recorder) + // the cluster reconciliation loop is a 3-stage model: plan Init, plan Build and plan Execute + // Init stage + planBuilder := lifecycle.NewClusterPlanBuilder(reqCtx, r.Client, req) if err := planBuilder.Init(); err != nil { - return requeueError(err) - } else if err := planBuilder.Validate(); err != nil { - return requeueError(err) - } else if plan, err := planBuilder.Build(); err != nil { - return requeueError(err) - } else if err = plan.Execute(); err != nil { - return requeueError(err) + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + // Build stage + // what you should do in most cases is writing your transformer. + // + // here are the how-to tips: + // 1. one transformer for one scenario + // 2. try not to modify the current transformers, make a new one + // 3. transformers are independent with each-other, with some exceptions. + // Which means transformers' order is not important in most cases. + // If you don't know where to put your transformer, append it to the end and that would be ok. + // 4. don't use client.Client for object write, use client.ReadonlyClient for object read. + // If you do need to create/update/delete object, make your intent operation a lifecycleVertex and put it into the DAG. + // + // TODO: transformers are vertices, theirs' dependencies are edges, make plan Build stage a DAG. + plan, errBuild := planBuilder. + AddTransformer( + // handle deletion + // handle cluster deletion first + &lifecycle.ClusterDeletionTransformer{}, + // check is recovering from halted cluster + &lifecycle.HaltRecoveryTransformer{}, + // assure meta-data info + // update finalizer and cd&cv labels + &lifecycle.AssureMetaTransformer{}, + // validate ref objects + // validate cd & cv's existence and availability + &lifecycle.ValidateAndLoadRefResourcesTransformer{}, + // validate config + &lifecycle.ValidateEnableLogsTransformer{}, + // fix spec + // fill class related info + &lifecycle.FillClassTransformer{}, + // create cluster connection credential secret object + &lifecycle.ClusterCredentialTransformer{}, + // handle restore + &lifecycle.RestoreTransformer{Client: r.Client}, + // create all components objects + &lifecycle.ComponentTransformer{Client: r.Client}, + // transform backupPolicy tpl to backuppolicy.dataprotection.kubeblocks.io + &lifecycle.BackupPolicyTPLTransformer{}, + // add our finalizer to all objects + &lifecycle.OwnershipTransformer{}, + // make all workload objects depending on credential secret + &lifecycle.SecretTransformer{}, + // make config configmap immutable + &lifecycle.ConfigTransformer{}, + // update cluster status + &lifecycle.ClusterStatusTransformer{}, + // always safe to put your transformer below + ). + Build() + + // Execute stage + // errBuild not nil means build stage partial success or validation error + // execute the plan first, delay error handling + if errExec := plan.Execute(); errExec != nil { + return requeueError(errExec) + } + if errBuild != nil { + return requeueError(errBuild) } return intctrlutil.Reconciled() } @@ -162,33 +220,33 @@ func (r *ClusterReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&corev1.PersistentVolumeClaim{}). Owns(&policyv1.PodDisruptionBudget{}). Owns(&dataprotectionv1alpha1.BackupPolicy{}). - Owns(&dataprotectionv1alpha1.Backup{}) + Owns(&dataprotectionv1alpha1.Backup{}). + Owns(&batchv1.Job{}). + Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(r.filterClusterPods)) if viper.GetBool("VOLUMESNAPSHOT") { - b.Owns(&snapshotv1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + if intctrlutil.InVolumeSnapshotV1Beta1() { + b.Owns(&snapshotv1beta1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + } else { + b.Owns(&snapshotv1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + } } - b.Watches(&source.Kind{Type: &appsv1alpha1.ClassFamily{}}, - &handler.EnqueueRequestForObject{}, - builder.WithPredicates(predicate.NewPredicateFuncs(func(object client.Object) bool { return true })), - ) return b.Complete(r) } -// Handle is the event handler for the cluster status event. -func (r *ClusterStatusEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) error { - if event.InvolvedObject.FieldPath != constant.ProbeCheckRolePath { - return handleEventForClusterStatus(reqCtx.Ctx, cli, recorder, event) +func (r *ClusterReconciler) filterClusterPods(obj client.Object) []reconcile.Request { + labels := obj.GetLabels() + if v, ok := labels[constant.AppManagedByLabelKey]; !ok || v != constant.AppName { + return []reconcile.Request{} } - - // parse probe event message when field path is probe-role-changed-check - message := k8score.ParseProbeEventMessage(reqCtx, event) - if message == nil { - reqCtx.Log.Info("parse probe event message failed", "message", event.Message) - return nil + if _, ok := labels[constant.AppInstanceLabelKey]; !ok { + return []reconcile.Request{} } - - // if probe message event is checkRoleFailed, it means the cluster is abnormal, need to handle the cluster status - if message.Event == k8score.ProbeEventCheckRoleFailed { - return handleEventForClusterStatus(reqCtx.Ctx, cli, recorder, event) + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: obj.GetNamespace(), + Name: labels[constant.AppInstanceLabelKey], + }, + }, } - return nil } diff --git a/controllers/apps/cluster_controller_test.go b/controllers/apps/cluster_controller_test.go index 7a00b4c6c..e166b29c3 100644 --- a/controllers/apps/cluster_controller_test.go +++ b/controllers/apps/cluster_controller_test.go @@ -1,30 +1,35 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( + "encoding/json" + "errors" "fmt" - "reflect" "strconv" "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + batchv1 "k8s.io/api/batch/v1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" @@ -37,45 +42,77 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/client-go/kubernetes/scheme" controllerruntime "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" + "github.com/apecloud/kubeblocks/controllers/apps/components/replication" "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/lifecycle" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" + probeutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" ) +const backupPolicyTPLName = "test-backup-policy-template-mysql" + var _ = Describe("Cluster Controller", func() { const ( clusterDefName = "test-clusterdef" clusterVersionName = "test-clusterversion" - clusterNamePrefix = "test-cluster" - mysqlCompType = "replicasets" - mysqlCompName = "mysql" - nginxCompType = "proxy" - nginxCompName = "nginx" - consensusCompName = "consensus" - consensusCompType = "consensus" + clusterName = "test-cluster" // this become cluster prefix name if used with testapps.NewClusterFactory().WithRandomName() leader = "leader" follower = "follower" + // REVIEW: + // - setup componentName and componentDefName as map entry pair + statelessCompName = "stateless" + statelessCompDefName = "stateless" + statefulCompName = "stateful" + statefulCompDefName = "stateful" + consensusCompName = "consensus" + consensusCompDefName = "consensus" + replicationCompName = "replication" + replicationCompDefName = "replication" ) var ( - randomStr = testCtx.GetRandomStr() - clusterNameRand = "mysql-" + randomStr - clusterDefNameRand = "mysql-definition-" + randomStr - clusterVersionNameRand = "mysql-cluster-version-" + randomStr + clusterNameRand string + clusterDefNameRand string + clusterVersionNameRand string + clusterDefObj *appsv1alpha1.ClusterDefinition + clusterVersionObj *appsv1alpha1.ClusterVersion + clusterObj *appsv1alpha1.Cluster + clusterKey types.NamespacedName + allSettings map[string]interface{} ) + resetViperCfg := func() { + if allSettings != nil { + Expect(viper.MergeConfigMap(allSettings)).ShouldNot(HaveOccurred()) + allSettings = nil + } + } + + resetTestContext := func() { + clusterDefObj = nil + clusterVersionObj = nil + clusterObj = nil + randomStr := testCtx.GetRandomStr() + clusterNameRand = "mysql-" + randomStr + clusterDefNameRand = "mysql-definition-" + randomStr + clusterVersionNameRand = "mysql-cluster-version-" + randomStr + resetViperCfg() + } + // Cleanups cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -88,119 +125,53 @@ var _ = Describe("Cluster Controller", func() { inNS := client.InNamespace(testCtx.DefaultNamespace) ml := client.HasLabels{testCtx.TestObjLabelKey} // namespaced - testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PersistentVolumeClaimSignature, true, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PodSignature, true, inNS, ml) + testapps.ClearResources(&testCtx, generics.BackupSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.BackupPolicySignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.VolumeSnapshotSignature, inNS) // non-namespaced - testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupToolSignature, ml) - testapps.ClearResources(&testCtx, intctrlutil.StorageClassSignature, ml) + testapps.ClearResources(&testCtx, generics.BackupPolicyTemplateSignature, ml) + testapps.ClearResources(&testCtx, generics.BackupToolSignature, ml) + testapps.ClearResources(&testCtx, generics.StorageClassSignature, ml) + resetTestContext() } BeforeEach(func() { cleanEnv() + allSettings = viper.AllSettings() }) AfterEach(func() { cleanEnv() }) - var ( - clusterDefObj *appsv1alpha1.ClusterDefinition - clusterVersionObj *appsv1alpha1.ClusterVersion - clusterObj *appsv1alpha1.Cluster - clusterKey types.NamespacedName - ) + // test function helpers + createAllWorkloadTypesClusterDef := func(noCreateAssociateCV ...bool) { + By("Create a clusterDefinition obj") + clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). + AddComponentDef(testapps.ConsensusMySQLComponent, consensusCompDefName). + AddComponentDef(testapps.ReplicationRedisComponent, replicationCompDefName). + AddComponentDef(testapps.StatelessNginxComponent, statelessCompDefName). + Create(&testCtx).GetObject() - // Test cases + if len(noCreateAssociateCV) > 0 && noCreateAssociateCV[0] { + return + } + By("Create a clusterVersion obj") + clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). + AddComponentVersion(statefulCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(consensusCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(replicationCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(statelessCompDefName).AddContainerShort("nginx", testapps.NginxImage). + Create(&testCtx).GetObject() + } waitForCreatingResourceCompletely := func(clusterKey client.ObjectKey, compNames ...string) { Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) for _, compName := range compNames { - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterKey.Name, compName)).Should(Equal(appsv1alpha1.CreatingClusterCompPhase)) - } - } - - checkAllResourcesCreated := func() { - By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(3). - AddComponent(nginxCompName, nginxCompType).SetReplicas(3). - WithRandomName().Create(&testCtx).GetObject() - clusterKey = client.ObjectKeyFromObject(clusterObj) - - By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) - - By("Check deployment workload has been created") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.DeploymentSignature, - client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - }, client.InNamespace(clusterKey.Namespace))).ShouldNot(BeEquivalentTo(0)) - - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - - By("Check statefulset pod's volumes") - for _, sts := range stsList.Items { - podSpec := sts.Spec.Template - volumeNames := map[string]struct{}{} - for _, v := range podSpec.Spec.Volumes { - volumeNames[v.Name] = struct{}{} - } - - for _, cc := range [][]corev1.Container{ - podSpec.Spec.Containers, - podSpec.Spec.InitContainers, - } { - for _, c := range cc { - for _, vm := range c.VolumeMounts { - _, ok := volumeNames[vm.Name] - Expect(ok).Should(BeTrue()) - } - } - } - } - - By("Check associated PDB has been created") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.PodDisruptionBudgetSignature, - client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - }, client.InNamespace(clusterKey.Namespace))).Should(Equal(0)) - - podSpec := stsList.Items[0].Spec.Template.Spec - By("Checking created sts pods template with built-in toleration") - Expect(podSpec.Tolerations).Should(HaveLen(1)) - Expect(podSpec.Tolerations[0].Key).To(Equal(constant.KubeBlocksDataNodeTolerationKey)) - - By("Checking created sts pods template with built-in Affinity") - Expect(podSpec.Affinity.PodAntiAffinity == nil && podSpec.Affinity.PodAffinity == nil).Should(BeTrue()) - Expect(podSpec.Affinity.NodeAffinity).ShouldNot(BeNil()) - Expect(podSpec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Preference.MatchExpressions[0].Key).To( - Equal(constant.KubeBlocksDataNodeLabelKey)) - - By("Checking created sts pods template without TopologySpreadConstraints") - Expect(podSpec.TopologySpreadConstraints).Should(BeEmpty()) - - By("Check should create env configmap") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.ConfigMapSignature, - client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - constant.AppConfigTypeLabelKey: "kubeblocks-env", - }, client.InNamespace(clusterKey.Namespace))).Should(Equal(2)) - - By("Make sure the cluster controller has set the cluster status to Running") - for i, comp := range clusterObj.Spec.ComponentSpecs { - if comp.ComponentDefRef != mysqlCompType || comp.Name != mysqlCompName { - continue - } - stsList := testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, client.ObjectKeyFromObject(clusterObj), clusterObj.Spec.ComponentSpecs[i].Name) - for _, v := range stsList.Items { - Expect(testapps.ChangeObjStatus(&testCtx, &v, func() { - testk8s.MockStatefulSetReady(&v) - })).ShouldNot(HaveOccurred()) - } + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)).Should(Equal(appsv1alpha1.CreatingClusterCompPhase)) } } @@ -209,6 +180,9 @@ var _ = Describe("Cluster Controller", func() { svcType corev1.ServiceType } + // getHeadlessSvcPorts returns the component's headless service ports by gathering all container's ports in the + // ClusterComponentDefinition.PodSpec, it's a subset of the real ports as some containers can be dynamically + // injected into the pod by the lifecycle controller, such as the probe container. getHeadlessSvcPorts := func(g Gomega, compDefName string) []corev1.ServicePort { comp, err := util.GetComponentDefByCluster(testCtx.Ctx, k8sClient, *clusterObj, compDefName) g.Expect(err).ShouldNot(HaveOccurred()) @@ -227,7 +201,7 @@ var _ = Describe("Cluster Controller", func() { return headlessSvcPorts } - validateCompSvcList := func(g Gomega, compName string, compType string, expectServices map[string]ExpectService) { + validateCompSvcList := func(g Gomega, compName string, compDefName string, expectServices map[string]ExpectService) { clusterKey = client.ObjectKeyFromObject(clusterObj) svcList := &corev1.ServiceList{} @@ -238,7 +212,11 @@ var _ = Describe("Cluster Controller", func() { for svcName, svcSpec := range expectServices { idx := slices.IndexFunc(svcList.Items, func(e corev1.Service) bool { - return strings.HasSuffix(e.Name, svcName) + parts := []string{clusterKey.Name, compName} + if svcName != "" { + parts = append(parts, svcName) + } + return strings.Join(parts, "-") == e.Name }) g.Expect(idx >= 0).To(BeTrue()) svc := svcList.Items[idx] @@ -250,24 +228,26 @@ var _ = Describe("Cluster Controller", func() { g.Expect(svc.Spec.ClusterIP).ShouldNot(Equal(corev1.ClusterIPNone)) case svc.Spec.Type == corev1.ServiceTypeClusterIP && svcSpec.headless: g.Expect(svc.Spec.ClusterIP).Should(Equal(corev1.ClusterIPNone)) - g.Expect(reflect.DeepEqual(svc.Spec.Ports, getHeadlessSvcPorts(g, compType))).Should(BeTrue()) + for _, port := range getHeadlessSvcPorts(g, compDefName) { + g.Expect(slices.Index(svc.Spec.Ports, port) >= 0).Should(BeTrue()) + } } } g.Expect(len(expectServices)).Should(Equal(len(svcList.Items))) } - testServiceAddAndDelete := func() { + testServiceAddAndDelete := func(compName, compDefName string) { By("Creating a cluster with two LoadBalancer services") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(1). + AddComponent(compName, compDefName).SetReplicas(1). AddService(testapps.ServiceVPCName, corev1.ServiceTypeLoadBalancer). AddService(testapps.ServiceInternetName, corev1.ServiceTypeLoadBalancer). WithRandomName().Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) expectServices := map[string]ExpectService{ testapps.ServiceHeadlessName: {svcType: corev1.ServiceTypeClusterIP, headless: true}, @@ -275,14 +255,14 @@ var _ = Describe("Cluster Controller", func() { testapps.ServiceVPCName: {svcType: corev1.ServiceTypeLoadBalancer, headless: false}, testapps.ServiceInternetName: {svcType: corev1.ServiceTypeLoadBalancer, headless: false}, } - Eventually(func(g Gomega) { validateCompSvcList(g, mysqlCompName, mysqlCompType, expectServices) }).Should(Succeed()) + Eventually(func(g Gomega) { validateCompSvcList(g, compName, compDefName, expectServices) }).Should(Succeed()) By("Delete a LoadBalancer service") deleteService := testapps.ServiceVPCName delete(expectServices, deleteService) Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { for idx, comp := range cluster.Spec.ComponentSpecs { - if comp.ComponentDefRef != mysqlCompType || comp.Name != mysqlCompName { + if comp.ComponentDefRef != compDefName || comp.Name != compName { continue } var services []appsv1alpha1.ClusterComponentService @@ -296,13 +276,13 @@ var _ = Describe("Cluster Controller", func() { return } })()).ShouldNot(HaveOccurred()) - Eventually(func(g Gomega) { validateCompSvcList(g, mysqlCompName, mysqlCompType, expectServices) }).Should(Succeed()) + Eventually(func(g Gomega) { validateCompSvcList(g, compName, compDefName, expectServices) }).Should(Succeed()) By("Add the deleted LoadBalancer service back") expectServices[deleteService] = ExpectService{svcType: corev1.ServiceTypeLoadBalancer, headless: false} Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { for idx, comp := range cluster.Spec.ComponentSpecs { - if comp.ComponentDefRef != mysqlCompType || comp.Name != mysqlCompName { + if comp.ComponentDefRef != compDefName || comp.Name != compName { continue } comp.Services = append(comp.Services, appsv1alpha1.ClusterComponentService{ @@ -313,55 +293,24 @@ var _ = Describe("Cluster Controller", func() { return } })()).ShouldNot(HaveOccurred()) - Eventually(func(g Gomega) { validateCompSvcList(g, mysqlCompName, mysqlCompType, expectServices) }).Should(Succeed()) + Eventually(func(g Gomega) { validateCompSvcList(g, compName, compDefName, expectServices) }).Should(Succeed()) } - checkAllServicesCreate := func() { + createClusterObj := func(compName, compDefName string) { By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(1). - AddComponent(nginxCompName, nginxCompType).SetReplicas(3). - WithRandomName().Create(&testCtx).GetObject() + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddComponent(compName, compDefName). + Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) - By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName, nginxCompName) - - By("Checking proxy services") - nginxExpectServices := map[string]ExpectService{ - // TODO: fix me later, proxy should not have internal headless service - testapps.ServiceHeadlessName: {svcType: corev1.ServiceTypeClusterIP, headless: true}, - testapps.ServiceDefaultName: {svcType: corev1.ServiceTypeClusterIP, headless: false}, - } - Eventually(func(g Gomega) { validateCompSvcList(g, nginxCompName, nginxCompType, nginxExpectServices) }).Should(Succeed()) - - By("Checking mysql services") - mysqlExpectServices := map[string]ExpectService{ - testapps.ServiceHeadlessName: {svcType: corev1.ServiceTypeClusterIP, headless: true}, - testapps.ServiceDefaultName: {svcType: corev1.ServiceTypeClusterIP, headless: false}, - } - Eventually(func(g Gomega) { validateCompSvcList(g, mysqlCompName, mysqlCompType, mysqlExpectServices) }).Should(Succeed()) - - By("Make sure the cluster controller has set the cluster status to Running") - for i, comp := range clusterObj.Spec.ComponentSpecs { - if comp.ComponentDefRef != mysqlCompType || comp.Name != mysqlCompName { - continue - } - stsList := testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, client.ObjectKeyFromObject(clusterObj), clusterObj.Spec.ComponentSpecs[i].Name) - for _, v := range stsList.Items { - Expect(testapps.ChangeObjStatus(&testCtx, &v, func() { - testk8s.MockStatefulSetReady(&v) - })).ShouldNot(HaveOccurred()) - } - } + By("Waiting for the cluster enter running phase") + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) } - testWipeOut := func() { - By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().Create(&testCtx).GetObject() - clusterKey = client.ObjectKeyFromObject(clusterObj) + testWipeOut := func(compName, compDefName string) { + createClusterObj(compName, compDefName) By("Waiting for the cluster enter running phase") Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) @@ -376,15 +325,8 @@ var _ = Describe("Cluster Controller", func() { Eventually(testapps.CheckObjExists(&testCtx, clusterKey, &appsv1alpha1.Cluster{}, false)).Should(Succeed()) } - testDoNotTermintate := func() { - By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().Create(&testCtx).GetObject() - clusterKey = client.ObjectKeyFromObject(clusterObj) - - By("Waiting for the cluster enter running phase") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) + testDoNotTermintate := func(compName, compDefName string) { + createClusterObj(compName, compDefName) // REVIEW: this test flow @@ -417,47 +359,68 @@ var _ = Describe("Cluster Controller", func() { })()).ShouldNot(HaveOccurred()) } - changeStatefulSetReplicas := func(clusterName types.NamespacedName, replicas int32) { + changeComponentReplicas := func(clusterName types.NamespacedName, replicas int32) { Expect(testapps.GetAndChangeObj(&testCtx, clusterName, func(cluster *appsv1alpha1.Cluster) { - if len(cluster.Spec.ComponentSpecs) == 0 { - cluster.Spec.ComponentSpecs = []appsv1alpha1.ClusterComponentSpec{ - { - Name: mysqlCompName, - ComponentDefRef: mysqlCompType, - Replicas: replicas, - }} - } else { - cluster.Spec.ComponentSpecs[0].Replicas = replicas - } + Expect(cluster.Spec.ComponentSpecs).Should(HaveLen(1)) + cluster.Spec.ComponentSpecs[0].Replicas = replicas })()).ShouldNot(HaveOccurred()) } - testChangeReplicas := func() { - By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().Create(&testCtx).GetObject() - clusterKey = client.ObjectKeyFromObject(clusterObj) + getPodSpec := func(sts *appsv1.StatefulSet, deploy *appsv1.Deployment) *corev1.PodSpec { + if sts != nil { + return &sts.Spec.Template.Spec + } else if deploy != nil { + return &deploy.Spec.Template.Spec + } + panic("unreachable") + } - By("Waiting for the cluster enter running phase") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) + checkSingleWorkload := func(compDefName string, expects func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment)) { + isStsWorkload := true + switch compDefName { + case statelessCompDefName: + isStsWorkload = false + case statefulCompDefName, replicationCompDefName, consensusCompDefName: + break + default: + panic("unreachable") + } + + if isStsWorkload { + Eventually(func(g Gomega) { + l := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) + expects(g, &l.Items[0], nil) + }).Should(Succeed()) + } else { + Eventually(func(g Gomega) { + l := testk8s.ListAndCheckDeployment(&testCtx, clusterKey) + expects(g, nil, &l.Items[0]) + }).Should(Succeed()) + } + } + testChangeReplicas := func(compName, compDefName string) { + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) + createClusterObj(compName, compDefName) replicasSeq := []int32{5, 3, 1, 0, 2, 4} expectedOG := int64(1) for _, replicas := range replicasSeq { By(fmt.Sprintf("Change replicas to %d", replicas)) - changeStatefulSetReplicas(clusterKey, replicas) + changeComponentReplicas(clusterKey, replicas) expectedOG++ - By("Checking cluster status and the number of replicas changed") Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { g.Expect(fetched.Status.ObservedGeneration).To(BeEquivalentTo(expectedOG)) g.Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) })).Should(Succeed()) - Eventually(func(g Gomega) { - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - g.Expect(int(*stsList.Items[0].Spec.Replicas)).To(BeEquivalentTo(replicas)) - }).Should(Succeed()) + + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + if sts != nil { + g.Expect(int(*sts.Spec.Replicas)).To(BeEquivalentTo(replicas)) + } else { + g.Expect(int(*deploy.Spec.Replicas)).To(BeEquivalentTo(replicas)) + } + }) } } @@ -470,7 +433,7 @@ var _ = Describe("Cluster Controller", func() { compName, "data").SetStorage("1Gi").CheckedCreate(&testCtx) } - mockPodsForConsensusTest := func(cluster *appsv1alpha1.Cluster, number int) []corev1.Pod { + mockPodsForTest := func(cluster *appsv1alpha1.Cluster, number int) []corev1.Pod { componentName := cluster.Spec.ComponentSpecs[0].Name clusterName := cluster.Name stsName := cluster.Name + "-" + componentName @@ -481,6 +444,7 @@ var _ = Describe("Cluster Controller", func() { Name: stsName + "-" + strconv.Itoa(i), Namespace: testCtx.DefaultNamespace, Labels: map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, constant.AppInstanceLabelKey: clusterName, constant.KBAppComponentLabelKey: componentName, appsv1.ControllerRevisionHashLabelKey: "mock-version", @@ -498,7 +462,7 @@ var _ = Describe("Cluster Controller", func() { return pods } - horizontalScaleComp := func(updatedReplicas int, comp *appsv1alpha1.ClusterComponentSpec) { + horizontalScaleComp := func(updatedReplicas int, comp *appsv1alpha1.ClusterComponentSpec, policy *appsv1alpha1.HorizontalScalePolicy) { By("Mocking components' PVCs to bound") for i := 0; i < int(comp.Replicas); i++ { pvcKey := types.NamespacedName{ @@ -517,8 +481,12 @@ var _ = Describe("Cluster Controller", func() { Expect(int(*stsList.Items[0].Spec.Replicas)).To(BeEquivalentTo(comp.Replicas)) By("Creating mock pods in StatefulSet") - pods := mockPodsForConsensusTest(clusterObj, int(comp.Replicas)) - for _, pod := range pods { + pods := mockPodsForTest(clusterObj, int(comp.Replicas)) + for i, pod := range pods { + if comp.ComponentDefRef == replicationCompDefName && i == 0 { + By("mocking primary for replication to pass check") + pods[0].ObjectMeta.Labels[constant.RoleLabelKey] = "primary" + } Expect(testCtx.CheckedCreateObj(testCtx.Ctx, &pod)).Should(Succeed()) // mock the status to pass the isReady(pod) check in consensus_set pod.Status.Conditions = []corev1.PodCondition{{ @@ -531,12 +499,25 @@ var _ = Describe("Cluster Controller", func() { By(fmt.Sprintf("Changing replicas to %d", updatedReplicas)) changeCompReplicas(clusterKey, int32(updatedReplicas), comp) + checkUpdatedStsReplicas := func() { + By("Checking updated sts replicas") + Eventually(func() int32 { + stsList = testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, clusterKey, comp.Name) + return *stsList.Items[0].Spec.Replicas + }).Should(BeEquivalentTo(updatedReplicas)) + } + + if policy == nil { + checkUpdatedStsReplicas() + return + } + By("Checking Backup created") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.BackupSignature, + Eventually(testapps.List(&testCtx, generics.BackupSignature, client.MatchingLabels{ constant.AppInstanceLabelKey: clusterKey.Name, constant.KBAppComponentLabelKey: comp.Name, - }, client.InNamespace(clusterKey.Namespace))).Should(Equal(1)) + }, client.InNamespace(clusterKey.Namespace))).Should(HaveLen(1)) By("Mocking VolumeSnapshot and set it as ReadyToUse") snapshotKey := types.NamespacedName{Name: fmt.Sprintf("%s-%s-scaling", @@ -579,24 +560,59 @@ var _ = Describe("Cluster Controller", func() { } By("Check backup job cleanup") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.BackupSignature, + Eventually(testapps.List(&testCtx, generics.BackupSignature, client.MatchingLabels{ constant.AppInstanceLabelKey: clusterKey.Name, constant.KBAppComponentLabelKey: comp.Name, - }, client.InNamespace(clusterKey.Namespace))).Should(Equal(0)) + }, client.InNamespace(clusterKey.Namespace))).Should(HaveLen(0)) Eventually(testapps.CheckObjExists(&testCtx, snapshotKey, &snapshotv1.VolumeSnapshot{}, false)).Should(Succeed()) - By("Checking updated sts replicas") - Eventually(func() int32 { - stsList = testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, clusterKey, comp.Name) - return *stsList.Items[0].Spec.Replicas - }).Should(BeEquivalentTo(updatedReplicas)) - } + checkUpdatedStsReplicas() + + By("Checking updated sts replicas' PVC") + for i := 0; i < updatedReplicas; i++ { + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(comp.Name, i), + } + Eventually(testapps.CheckObjExists(&testCtx, pvcKey, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) + } - horizontalScale := func(updatedReplicas int) { + By("Checking pod env config updated") + cmKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: fmt.Sprintf("%s-%s-env", clusterKey.Name, comp.Name), + } + Eventually(testapps.CheckObj(&testCtx, cmKey, func(g Gomega, cm *corev1.ConfigMap) { + match := func(key, prefix, suffix string) bool { + return strings.HasPrefix(key, prefix) && strings.HasSuffix(key, suffix) + } + foundN := "" + for k, v := range cm.Data { + if match(k, constant.KBPrefix, "_N") { + foundN = v + break + } + } + g.Expect(foundN).Should(Equal(strconv.Itoa(updatedReplicas))) + for i := 0; i < updatedReplicas; i++ { + foundPodHostname := "" + suffix := fmt.Sprintf("_%d_HOSTNAME", i) + for k, v := range cm.Data { + if match(k, constant.KBPrefix, suffix) { + foundPodHostname = v + break + } + } + g.Expect(foundPodHostname != "").Should(BeTrue()) + } + })).Should(Succeed()) + } + // @argument componentDefsWithHScalePolicy assign ClusterDefinition.spec.componentDefs[].horizontalScalePolicy for + // the matching names. If not provided, will set 1st ClusterDefinition.spec.componentDefs[0].horizontalScalePolicy. + horizontalScale := func(updatedReplicas int, componentDefsWithHScalePolicy ...string) { viper.Set("VOLUMESNAPSHOT", true) - cluster := &appsv1alpha1.Cluster{} Expect(testCtx.Cli.Get(testCtx.Ctx, clusterKey, cluster)).Should(Succeed()) initialGeneration := int(cluster.Status.ObservedGeneration) @@ -604,12 +620,27 @@ var _ = Describe("Cluster Controller", func() { By("Set HorizontalScalePolicy") Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(clusterDefObj), func(clusterDef *appsv1alpha1.ClusterDefinition) { - clusterDef.Spec.ComponentDefs[0].HorizontalScalePolicy = - &appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot} - })()).ShouldNot(HaveOccurred()) + // assign 1st component + if len(componentDefsWithHScalePolicy) == 0 && len(clusterDef.Spec.ComponentDefs) > 0 { + componentDefsWithHScalePolicy = []string{ + clusterDef.Spec.ComponentDefs[0].Name, + } + } + for i, compDef := range clusterDef.Spec.ComponentDefs { + if !slices.Contains(componentDefsWithHScalePolicy, compDef.Name) { + continue + } + + By("Checking backup policy created from backup policy template") + policyName := lifecycle.DeriveBackupPolicyName(clusterKey.Name, compDef.Name, "") + clusterDef.Spec.ComponentDefs[i].HorizontalScalePolicy = + &appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, + BackupPolicyTemplateName: backupPolicyTPLName} - By("Creating a BackupPolicyTemplate") - createBackupPolicyTpl(clusterDefObj) + Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKey{Name: policyName, Namespace: clusterKey.Namespace}, + &dataprotectionv1alpha1.BackupPolicy{}, true)).Should(Succeed()) + } + })()).ShouldNot(HaveOccurred()) By("Mocking all components' PVCs to bound") for _, comp := range clusterObj.Spec.ComponentSpecs { @@ -619,77 +650,189 @@ var _ = Describe("Cluster Controller", func() { Name: getPVCName(comp.Name, i), } createPVC(clusterKey.Name, pvcKey.Name, comp.Name) - Eventually(testapps.CheckObjExists(&testCtx, pvcKey, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, pvcKey, + &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) Eventually(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { pvc.Status.Phase = corev1.ClaimBound })).Should(Succeed()) } } - for i := range clusterObj.Spec.ComponentSpecs { - horizontalScaleComp(updatedReplicas, &clusterObj.Spec.ComponentSpecs[i]) - } - By("Checking cluster status and the number of replicas changed") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(initialGeneration + len(clusterObj.Spec.ComponentSpecs))) - for i := range clusterObj.Spec.ComponentSpecs { - stsList := testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, client.ObjectKeyFromObject(clusterObj), clusterObj.Spec.ComponentSpecs[i].Name) - for _, v := range stsList.Items { - Expect(testapps.ChangeObjStatus(&testCtx, &v, func() { - testk8s.MockStatefulSetReady(&v) - })).ShouldNot(HaveOccurred()) + hscalePolicy := func(comp appsv1alpha1.ClusterComponentSpec) *appsv1alpha1.HorizontalScalePolicy { + for _, componentDef := range clusterDefObj.Spec.ComponentDefs { + if componentDef.Name == comp.ComponentDefRef { + return componentDef.HorizontalScalePolicy + } } + return nil + } + + By("Get the latest cluster def") + Expect(k8sClient.Get(testCtx.Ctx, client.ObjectKeyFromObject(clusterDefObj), clusterDefObj)).Should(Succeed()) + for i, comp := range clusterObj.Spec.ComponentSpecs { + horizontalScaleComp(updatedReplicas, &clusterObj.Spec.ComponentSpecs[i], hscalePolicy(comp)) } + + By("Checking cluster status and the number of replicas changed") + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)). + Should(BeEquivalentTo(initialGeneration + len(clusterObj.Spec.ComponentSpecs))) } - testHorizontalScale := func() { + testHorizontalScale := func(compName, compDefName string) { initialReplicas := int32(1) updatedReplicas := int32(3) By("Creating a single component cluster with VolumeClaimTemplate") pvcSpec := testapps.NewPVCSpec("1Gi") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). SetReplicas(initialReplicas). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) // REVIEW: this test flow, wait for running phase? - horizontalScale(int(updatedReplicas)) + horizontalScale(int(updatedReplicas), compDefName) } - testMultiCompHScale := func() { - initialReplicas := int32(1) - updatedReplicas := int32(3) + testStorageExpansion := func(compName, compDefName string) { + var ( + storageClassName = "sc-mock" + replicas = 3 + volumeSize = "1Gi" + newVolumeSize = "2Gi" + volumeQuantity = resource.MustParse(volumeSize) + newVolumeQuantity = resource.MustParse(newVolumeSize) + ) + + By("Mock a StorageClass which allows resize") + allowVolumeExpansion := true + storageClass := &storagev1.StorageClass{ + ObjectMeta: metav1.ObjectMeta{ + Name: storageClassName, + }, + Provisioner: "kubernetes.io/no-provisioner", + AllowVolumeExpansion: &allowVolumeExpansion, + } + Expect(testCtx.CreateObj(testCtx.Ctx, storageClass)).Should(Succeed()) - secondMysqlCompName := mysqlCompName + "1" + By("Creating a cluster with VolumeClaimTemplate") + pvcSpec := testapps.NewPVCSpec(volumeSize) + pvcSpec.StorageClassName = &storageClass.Name - By("Creating a multi components cluster with VolumeClaimTemplate") - pvcSpec := testapps.NewPVCSpec("1Gi") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + By("Create cluster and waiting for the cluster initialized") + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). - AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). - SetReplicas(initialReplicas). - AddComponent(secondMysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). - SetReplicas(initialReplicas). + SetReplicas(int32(replicas)). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName, secondMysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) - // REVIEW: this test flow, wait for running phase? - horizontalScale(int(updatedReplicas)) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + + By("Checking the replicas") + stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) + sts := &stsList.Items[0] + Expect(*sts.Spec.Replicas).Should(BeEquivalentTo(replicas)) + + By("Mock PVCs in Bound Status") + for i := 0; i < replicas; i++ { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: getPVCName(compName, i), + Namespace: clusterKey.Namespace, + Labels: map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: clusterKey.Name, + constant.KBAppComponentLabelKey: compName, + }}, + Spec: pvcSpec.ToV1PersistentVolumeClaimSpec(), + } + Expect(testCtx.CreateObj(testCtx.Ctx, pvc)).Should(Succeed()) + pvc.Status.Phase = corev1.ClaimBound // only bound pvc allows resize + if pvc.Status.Capacity == nil { + pvc.Status.Capacity = corev1.ResourceList{} + } + pvc.Status.Capacity[corev1.ResourceStorage] = volumeQuantity + Expect(k8sClient.Status().Update(testCtx.Ctx, pvc)).Should(Succeed()) + } + + By("mock pods/sts of component are available") + switch compDefName { + case statelessCompDefName: + // ignore + case replicationCompDefName: + testapps.MockReplicationComponentPods(nil, testCtx, sts, clusterObj.Name, compDefName, nil) + case statefulCompDefName, consensusCompDefName: + testapps.MockConsensusComponentPods(&testCtx, sts, clusterObj.Name, compName) + } + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + testk8s.MockStatefulSetReady(sts) + })).ShouldNot(HaveOccurred()) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) + + By("Updating the PVC storage size") + Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { + comp := &cluster.Spec.ComponentSpecs[0] + comp.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = newVolumeQuantity + })()).ShouldNot(HaveOccurred()) + + By("Checking the resize operation in progress") + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + for i := 0; i < replicas; i++ { + pvc := &corev1.PersistentVolumeClaim{} + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(compName, i), + } + Expect(k8sClient.Get(testCtx.Ctx, pvcKey, pvc)).Should(Succeed()) + Expect(pvc.Spec.Resources.Requests[corev1.ResourceStorage]).To(Equal(newVolumeQuantity)) + Expect(pvc.Status.Capacity[corev1.ResourceStorage]).To(Equal(volumeQuantity)) + } + + By("Mock resizing of PVCs finished") + for i := 0; i < replicas; i++ { + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(compName, i), + } + Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Capacity[corev1.ResourceStorage] = newVolumeQuantity + })()).ShouldNot(HaveOccurred()) + } + + By("Checking the resize operation finished") + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) + + By("Checking PVCs are resized") + for i := 0; i < replicas; i++ { + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(compName, i), + } + Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, pvc *corev1.PersistentVolumeClaim) { + g.Expect(pvc.Status.Capacity[corev1.ResourceStorage]).To(Equal(newVolumeQuantity)) + })).Should(Succeed()) + } } - testVerticalScale := func() { - const storageClassName = "sc-mock" + testVolumeExpansionFailedAndRecover := func(compName, compDefName string) { + + const storageClassName = "test-sc" const replicas = 3 By("Mock a StorageClass which allows resize") @@ -708,16 +851,18 @@ var _ = Describe("Cluster Controller", func() { pvcSpec.StorageClassName = &storageClass.Name By("Create cluster and waiting for the cluster initialized") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). SetReplicas(replicas). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) + + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) By("Checking the replicas") stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) @@ -726,20 +871,54 @@ var _ = Describe("Cluster Controller", func() { By("Mock PVCs in Bound Status") for i := 0; i < replicas; i++ { + tmpSpec := pvcSpec.ToV1PersistentVolumeClaimSpec() + tmpSpec.VolumeName = getPVCName(compName, i) pvc := &corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ - Name: getPVCName(mysqlCompName, i), + Name: getPVCName(compName, i), Namespace: clusterKey.Namespace, Labels: map[string]string{ constant.AppInstanceLabelKey: clusterKey.Name, }}, - Spec: pvcSpec.ToV1PersistentVolumeClaimSpec(), + Spec: tmpSpec, } Expect(testCtx.CreateObj(testCtx.Ctx, pvc)).Should(Succeed()) pvc.Status.Phase = corev1.ClaimBound // only bound pvc allows resize Expect(k8sClient.Status().Update(testCtx.Ctx, pvc)).Should(Succeed()) } + By("mocking PVs") + for i := 0; i < replicas; i++ { + pv := &corev1.PersistentVolume{ + ObjectMeta: metav1.ObjectMeta{ + Name: getPVCName(compName, i), // use same name as pvc + Namespace: clusterKey.Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: clusterKey.Name, + }}, + Spec: corev1.PersistentVolumeSpec{ + Capacity: corev1.ResourceList{ + "storage": resource.MustParse("1Gi"), + }, + AccessModes: []corev1.PersistentVolumeAccessMode{ + "ReadWriteOnce", + }, + PersistentVolumeReclaimPolicy: corev1.PersistentVolumeReclaimDelete, + StorageClassName: storageClassName, + PersistentVolumeSource: corev1.PersistentVolumeSource{ + HostPath: &corev1.HostPathVolumeSource{ + Path: "/opt/volume/nginx", + Type: nil, + }, + }, + ClaimRef: &corev1.ObjectReference{ + Name: getPVCName(compName, i), + }, + }, + } + Expect(testCtx.CreateObj(testCtx.Ctx, pv)).Should(Succeed()) + } + By("Updating the PVC storage size") newStorageValue := resource.MustParse("2Gi") Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { @@ -747,16 +926,8 @@ var _ = Describe("Cluster Controller", func() { comp.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = newStorageValue })()).ShouldNot(HaveOccurred()) - By("mock pods/sts of component are available") - testapps.MockConsensusComponentPods(testCtx, sts, clusterObj.Name, mysqlCompName) - Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { - testk8s.MockStatefulSetReady(sts) - })).ShouldNot(HaveOccurred()) - By("Checking the resize operation finished") Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterKey.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) By("Checking PVCs are resized") stsList = testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) @@ -765,56 +936,103 @@ var _ = Describe("Cluster Controller", func() { pvc := &corev1.PersistentVolumeClaim{} pvcKey := types.NamespacedName{ Namespace: clusterKey.Namespace, - Name: getPVCName(mysqlCompName, int(i)), + Name: getPVCName(compName, int(i)), } Expect(k8sClient.Get(testCtx.Ctx, pvcKey, pvc)).Should(Succeed()) Expect(pvc.Spec.Resources.Requests[corev1.ResourceStorage]).To(Equal(newStorageValue)) } - } - testClusterAffinity := func() { - const topologyKey = "testTopologyKey" - const lableKey = "testNodeLabelKey" - const labelValue = "testLabelValue" + By("Updating the PVC storage size back") + originStorageValue := resource.MustParse("1Gi") + Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { + comp := &cluster.Spec.ComponentSpecs[0] + comp.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = originStorageValue + })()).ShouldNot(HaveOccurred()) + + By("Checking the resize operation finished") + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(3)) + + By("Checking PVCs are resized") + Eventually(func(g Gomega) { + stsList = testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) + sts = &stsList.Items[0] + for i := *sts.Spec.Replicas - 1; i >= 0; i-- { + pvc := &corev1.PersistentVolumeClaim{} + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(compName, int(i)), + } + g.Expect(k8sClient.Get(testCtx.Ctx, pvcKey, pvc)).Should(Succeed()) + g.Expect(pvc.Spec.Resources.Requests[corev1.ResourceStorage]).To(Equal(originStorageValue)) + } + }).Should(Succeed()) + } + + testClusterAffinity := func(compName, compDefName string) { + const topologyKey = "testTopologyKey" + const labelKey = "testNodeLabelKey" + const labelValue = "testLabelValue" By("Creating a cluster with Affinity") + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) + affinity := &appsv1alpha1.Affinity{ PodAntiAffinity: appsv1alpha1.Required, TopologyKeys: []string{topologyKey}, NodeLabels: map[string]string{ - lableKey: labelValue, + labelKey: labelValue, }, Tenancy: appsv1alpha1.SharedNode, } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(3). + AddComponent(compName, compDefName).SetReplicas(3). WithRandomName().SetClusterAffinity(affinity). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) By("Checking the Affinity and TopologySpreadConstraints") - Eventually(func(g Gomega) { - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - podSpec := stsList.Items[0].Spec.Template.Spec - g.Expect(podSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).To(Equal(lableKey)) + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + podSpec := getPodSpec(sts, deploy) + g.Expect(podSpec.Affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).To(Equal(labelKey)) g.Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.DoNotSchedule)) g.Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal(topologyKey)) g.Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution).Should(HaveLen(1)) g.Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).To(Equal(topologyKey)) - }).Should(Succeed()) + }) + } + + testClusterServiceAccount := func(compName, compDefName string) { + By("Creating a cluster with target service account name") + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) + + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name). + AddComponent(compName, compDefName).SetReplicas(3). + SetServiceAccountName("test-service-account"). + Create(&testCtx).GetObject() + clusterKey = client.ObjectKeyFromObject(clusterObj) + + By("Waiting for the cluster controller to create resources completely") + waitForCreatingResourceCompletely(clusterKey, compName) + By("Checking the podSpec.serviceAccountName") + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + podSpec := getPodSpec(sts, deploy) + g.Expect(podSpec.ServiceAccountName).To(Equal("test-service-account")) + }) } - testComponentAffinity := func() { + testComponentAffinity := func(compName, compDefName string) { const clusterTopologyKey = "testClusterTopologyKey" const compTopologyKey = "testComponentTopologyKey" By("Creating a cluster with Affinity") + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) affinity := &appsv1alpha1.Affinity{ PodAntiAffinity: appsv1alpha1.Required, TopologyKeys: []string{clusterTopologyKey}, @@ -825,97 +1043,95 @@ var _ = Describe("Cluster Controller", func() { TopologyKeys: []string{compTopologyKey}, Tenancy: appsv1alpha1.DedicatedNode, } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().SetClusterAffinity(affinity). - AddComponent(mysqlCompName, mysqlCompType).SetComponentAffinity(compAffinity). + AddComponent(compName, compDefName).SetComponentAffinity(compAffinity). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) By("Checking the Affinity and the TopologySpreadConstraints") - Eventually(func(g Gomega) { - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - podSpec := stsList.Items[0].Spec.Template.Spec + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + podSpec := getPodSpec(sts, deploy) g.Expect(podSpec.TopologySpreadConstraints[0].WhenUnsatisfiable).To(Equal(corev1.ScheduleAnyway)) g.Expect(podSpec.TopologySpreadConstraints[0].TopologyKey).To(Equal(compTopologyKey)) g.Expect(podSpec.Affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight).ShouldNot(BeNil()) g.Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution).Should(HaveLen(1)) g.Expect(podSpec.Affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).To(Equal(corev1.LabelHostname)) - }).Should(Succeed()) + }) } - testClusterToleration := func() { + testClusterToleration := func(compName, compDefName string) { const tolerationKey = "testClusterTolerationKey" const tolerationValue = "testClusterTolerationValue" By("Creating a cluster with Toleration") - toleration := corev1.Toleration{ - Key: tolerationKey, - Value: tolerationValue, - Operator: corev1.TolerationOpEqual, - Effect: corev1.TaintEffectNoSchedule, - } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(1). - AddClusterToleration(toleration). + AddComponent(compName, compDefName).SetReplicas(1). + AddClusterToleration(corev1.Toleration{ + Key: tolerationKey, + Value: tolerationValue, + Operator: corev1.TolerationOpEqual, + Effect: corev1.TaintEffectNoSchedule, + }). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) By("Checking the tolerations") - Eventually(func(g Gomega) { - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - podSpec := stsList.Items[0].Spec.Template.Spec + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + podSpec := getPodSpec(sts, deploy) g.Expect(podSpec.Tolerations).Should(HaveLen(2)) - toleration = podSpec.Tolerations[0] - g.Expect(toleration.Key).Should(BeEquivalentTo(tolerationKey)) - g.Expect(toleration.Value).Should(BeEquivalentTo(tolerationValue)) - g.Expect(toleration.Operator).Should(BeEquivalentTo(corev1.TolerationOpEqual)) - g.Expect(toleration.Effect).Should(BeEquivalentTo(corev1.TaintEffectNoSchedule)) - }).Should(Succeed()) + t := podSpec.Tolerations[0] + g.Expect(t.Key).Should(BeEquivalentTo(tolerationKey)) + g.Expect(t.Value).Should(BeEquivalentTo(tolerationValue)) + g.Expect(t.Operator).Should(BeEquivalentTo(corev1.TolerationOpEqual)) + g.Expect(t.Effect).Should(BeEquivalentTo(corev1.TaintEffectNoSchedule)) + }) } - testComponentToleration := func() { + testStsWorkloadComponentToleration := func(compName, compDefName string) { clusterTolerationKey := "testClusterTolerationKey" compTolerationKey := "testcompTolerationKey" compTolerationValue := "testcompTolerationValue" By("Creating a cluster with Toleration") - toleration := corev1.Toleration{ - Key: clusterTolerationKey, - Operator: corev1.TolerationOpExists, - Effect: corev1.TaintEffectNoExecute, - } + Expect(compDefName).Should(BeElementOf(statelessCompDefName, statefulCompDefName, replicationCompDefName, consensusCompDefName)) compToleration := corev1.Toleration{ Key: compTolerationKey, Value: compTolerationValue, Operator: corev1.TolerationOpEqual, Effect: corev1.TaintEffectNoSchedule, } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName().AddClusterToleration(toleration). - AddComponent(mysqlCompName, mysqlCompType).AddComponentToleration(compToleration). + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddClusterToleration(corev1.Toleration{ + Key: clusterTolerationKey, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + }). + AddComponent(compName, compDefName).AddComponentToleration(compToleration). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) By("Checking the tolerations") - Eventually(func(g Gomega) { - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - podSpec := stsList.Items[0].Spec.Template.Spec + checkSingleWorkload(compDefName, func(g Gomega, sts *appsv1.StatefulSet, deploy *appsv1.Deployment) { + podSpec := getPodSpec(sts, deploy) Expect(podSpec.Tolerations).Should(HaveLen(2)) - toleration = podSpec.Tolerations[0] - g.Expect(toleration.Key).Should(BeEquivalentTo(compTolerationKey)) - g.Expect(toleration.Value).Should(BeEquivalentTo(compTolerationValue)) - g.Expect(toleration.Operator).Should(BeEquivalentTo(corev1.TolerationOpEqual)) - g.Expect(toleration.Effect).Should(BeEquivalentTo(corev1.TaintEffectNoSchedule)) - }).Should(Succeed()) + t := podSpec.Tolerations[0] + g.Expect(t.Key).Should(BeEquivalentTo(compTolerationKey)) + g.Expect(t.Value).Should(BeEquivalentTo(compTolerationValue)) + g.Expect(t.Operator).Should(BeEquivalentTo(corev1.TolerationOpEqual)) + g.Expect(t.Effect).Should(BeEquivalentTo(corev1.TaintEffectNoSchedule)) + }) } mockRoleChangedEvent := func(key types.NamespacedName, sts *appsv1.StatefulSet) []corev1.Event { @@ -929,8 +1145,8 @@ var _ = Describe("Cluster Controller", func() { Name: pod.Name + "-event", Namespace: testCtx.DefaultNamespace, }, - Reason: "Unhealthy", - Message: `Readiness probe failed: {"event":"Success","originalRole":"Leader","role":"Follower"}`, + Reason: string(probeutil.CheckRoleOperation), + Message: `{"event":"Success","originalRole":"Leader","role":"Follower"}`, InvolvedObject: corev1.ObjectReference{ Name: pod.Name, Namespace: testCtx.DefaultNamespace, @@ -940,7 +1156,7 @@ var _ = Describe("Cluster Controller", func() { } events = append(events, event) } - events[0].Message = `Readiness probe failed: {"event":"Success","originalRole":"Leader","role":"Leader"}` + events[0].Message = `{"event":"Success","originalRole":"Leader","role":"Leader"}` return events } @@ -955,20 +1171,20 @@ var _ = Describe("Cluster Controller", func() { return names } - testThreeReplicas := func() { + testThreeReplicas := func(compName, compDefName string) { const replicas = 3 By("Mock a cluster obj") pvcSpec := testapps.NewPVCSpec("1Gi") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). SetReplicas(replicas).AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) var stsList *appsv1.StatefulSetList var sts *appsv1.StatefulSet @@ -978,9 +1194,10 @@ var _ = Describe("Cluster Controller", func() { sts = &stsList.Items[0] }).Should(Succeed()) - By("Creating mock pods in StatefulSet") - pods := mockPodsForConsensusTest(clusterObj, replicas) + By("Creating mock pods in StatefulSet, and set controller reference") + pods := mockPodsForTest(clusterObj, replicas) for _, pod := range pods { + Expect(controllerutil.SetControllerReference(sts, &pod, scheme.Scheme)).Should(Succeed()) Expect(testCtx.CreateObj(testCtx.Ctx, &pod)).Should(Succeed()) // mock the status to pass the isReady(pod) check in consensus_set pod.Status.Conditions = []corev1.PodCondition{{ @@ -1018,6 +1235,17 @@ var _ = Describe("Cluster Controller", func() { g.Expect(followerCount).Should(Equal(2)) }).Should(Succeed()) + By("Checking pods' annotations") + Eventually(func(g Gomega) { + pods, err := util.GetPodListByStatefulSet(ctx, k8sClient, sts) + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(pods).Should(HaveLen(int(*sts.Spec.Replicas))) + for _, pod := range pods { + g.Expect(pod.Annotations).ShouldNot(BeNil()) + g.Expect(pod.Annotations[constant.ComponentReplicasAnnotationKey]).Should(Equal(strconv.Itoa(int(*sts.Spec.Replicas)))) + } + }).Should(Succeed()) + stsPatch := client.MergeFrom(sts.DeepCopy()) By("Updating StatefulSet's status") sts.Status.UpdateRevision = "mock-version" sts.Status.Replicas = int32(replicas) @@ -1025,7 +1253,7 @@ var _ = Describe("Cluster Controller", func() { sts.Status.CurrentReplicas = int32(replicas) sts.Status.ReadyReplicas = int32(replicas) sts.Status.ObservedGeneration = sts.Generation - Expect(k8sClient.Status().Update(ctx, sts)).Should(Succeed()) + Expect(k8sClient.Status().Patch(ctx, sts, stsPatch)).Should(Succeed()) By("Checking consensus set pods' role are updated in cluster status") Eventually(func(g Gomega) { @@ -1045,74 +1273,47 @@ var _ = Describe("Cluster Controller", func() { }).Should(Succeed()) By("Waiting the component be running") - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterObj.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) - } - - mockPodsForReplicationTest := func(cluster *appsv1alpha1.Cluster, stsList []appsv1.StatefulSet) []corev1.Pod { - componentName := cluster.Spec.ComponentSpecs[0].Name - clusterName := cluster.Name - pods := make([]corev1.Pod, 0) - for _, sts := range stsList { - t := true - pod := &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Name: sts.Name + "-0", - Namespace: testCtx.DefaultNamespace, - Annotations: map[string]string{}, - Labels: map[string]string{ - constant.RoleLabelKey: sts.Labels[constant.RoleLabelKey], - constant.AppInstanceLabelKey: clusterName, - constant.KBAppComponentLabelKey: componentName, - appsv1.ControllerRevisionHashLabelKey: sts.Status.UpdateRevision, - }, - OwnerReferences: []metav1.OwnerReference{ - { - APIVersion: "apps/v1", - Kind: constant.StatefulSetKind, - Controller: &t, - BlockOwnerDeletion: &t, - Name: sts.Name, - UID: sts.GetUID(), - }, - }, - }, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{{ - Name: "mock-container", - Image: "mock-container", - }}, - }, - } - for k, v := range sts.Spec.Template.Labels { - pod.ObjectMeta.Labels[k] = v - } - for k, v := range sts.Spec.Template.Annotations { - pod.ObjectMeta.Annotations[k] = v - } - pods = append(pods, *pod) - } - return pods + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)). + Should(Equal(appsv1alpha1.RunningClusterCompPhase)) } - testBackupError := func() { + testBackupError := func(compName, compDefName string) { initialReplicas := int32(1) updatedReplicas := int32(3) + viper.Set("VOLUMESNAPSHOT", true) + + By("Set HorizontalScalePolicy") + Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(clusterDefObj), + func(clusterDef *appsv1alpha1.ClusterDefinition) { + for i, def := range clusterDef.Spec.ComponentDefs { + if def.Name != compDefName { + continue + } + clusterDef.Spec.ComponentDefs[i].HorizontalScalePolicy = + &appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, + BackupPolicyTemplateName: backupPolicyTPLName} + } + })()).ShouldNot(HaveOccurred()) By("Creating a cluster with VolumeClaimTemplate") pvcSpec := testapps.NewPVCSpec("1Gi") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). SetReplicas(initialReplicas). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, compName) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) - // REVIEW: this test flow, should wait/fake still Running phase? + By(fmt.Sprintf("Changing replicas to %d", updatedReplicas)) + changeCompReplicas(clusterKey, updatedReplicas, &clusterObj.Spec.ComponentSpecs[0]) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) + // REVIEW: this test flow, should wait/fake still Running phase? By("Creating backup") backupKey := types.NamespacedName{ Namespace: testCtx.DefaultNamespace, @@ -1124,107 +1325,353 @@ var _ = Describe("Cluster Controller", func() { Namespace: backupKey.Namespace, Labels: map[string]string{ constant.AppInstanceLabelKey: clusterKey.Name, - constant.KBAppComponentLabelKey: mysqlCompName, + constant.KBAppComponentLabelKey: compName, constant.KBManagedByKey: "cluster", }, }, Spec: dataprotectionv1alpha1.BackupSpec{ - BackupPolicyName: "test-backup-policy", + BackupPolicyName: lifecycle.DeriveBackupPolicyName(clusterKey.Name, compDefName, ""), BackupType: "snapshot", }, } Expect(testCtx.Create(ctx, &backup)).Should(Succeed()) - By("Checking backup status to failed, because VolumeSnapshot disabled") + By("Checking backup status to failed, because pvc not exist") Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, backup *dataprotectionv1alpha1.Backup) { g.Expect(backup.Status.Phase).Should(Equal(dataprotectionv1alpha1.BackupFailed)) })).Should(Succeed()) - By("Creating a BackupPolicyTemplate") - createBackupPolicyTpl(clusterDefObj) - By("Set HorizontalScalePolicy") Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(clusterDefObj), func(clusterDef *appsv1alpha1.ClusterDefinition) { clusterDef.Spec.ComponentDefs[0].HorizontalScalePolicy = - &appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, BackupTemplateSelector: map[string]string{ - clusterDefLabelKey: clusterDefObj.Name, - }} + &appsv1alpha1.HorizontalScalePolicy{Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, + BackupPolicyTemplateName: backupPolicyTPLName} })()).ShouldNot(HaveOccurred()) By(fmt.Sprintf("Changing replicas to %d", updatedReplicas)) changeCompReplicas(clusterKey, updatedReplicas, &clusterObj.Spec.ComponentSpecs[0]) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) By("Checking cluster status failed with backup error") Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { - hasBackupError := false + g.Expect(viper.GetBool("VOLUMESNAPSHOT")).Should(BeTrue()) + g.Expect(cluster.Status.Conditions).ShouldNot(BeEmpty()) + var err error for _, cond := range cluster.Status.Conditions { - if strings.Contains(cond.Message, "backup error") { - hasBackupError = true + if strings.Contains(cond.Message, "backup for horizontalScaling failed") { + g.Expect(cond.Message).Should(ContainSubstring("backup for horizontalScaling failed")) + err = errors.New("has backup error") break } } - g.Expect(hasBackupError).Should(BeTrue()) - + if err == nil { + // this expect is intended for print all cluster.Status.Conditions + g.Expect(cluster.Status.Conditions).Should(BeEmpty()) + } + g.Expect(err).Should(HaveOccurred()) })).Should(Succeed()) + + By("expect for backup error event") + Eventually(func(g Gomega) { + eventList := corev1.EventList{} + Expect(k8sClient.List(ctx, &eventList, client.InNamespace(testCtx.DefaultNamespace))).Should(Succeed()) + hasBackupErrorEvent := false + for _, v := range eventList.Items { + if v.Reason == string(intctrlutil.ErrorTypeBackupFailed) { + hasBackupErrorEvent = true + break + } + } + g.Expect(hasBackupErrorEvent).Should(BeTrue()) + }).Should(Succeed()) } updateClusterAnnotation := func(cluster *appsv1alpha1.Cluster) { - Expect(testapps.ChangeObj(&testCtx, cluster, func() { - cluster.Annotations = map[string]string{ + Expect(testapps.ChangeObj(&testCtx, cluster, func(lcluster *appsv1alpha1.Cluster) { + lcluster.Annotations = map[string]string{ "time": time.Now().Format(time.RFC3339), } })).ShouldNot(HaveOccurred()) } + // Test cases // Scenarios - + // TODO: add case: empty image in cd, should report applyResourceFailed condition Context("when creating cluster without clusterversion", func() { BeforeEach(func() { - By("Create a clusterDefinition obj") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - Create(&testCtx).GetObject() + createAllWorkloadTypesClusterDef(true) }) It("should reconcile to create cluster with no error", func() { By("Creating a cluster") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, ""). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(3). + AddComponent(statelessCompName, statelessCompDefName).SetReplicas(3). + AddComponent(statefulCompName, statefulCompDefName).SetReplicas(3). + AddComponent(consensusCompName, consensusCompDefName).SetReplicas(3). + AddComponent(replicationCompName, replicationCompDefName).SetReplicas(3). WithRandomName().Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + waitForCreatingResourceCompletely(clusterKey, statelessCompName, statefulCompName, consensusCompName, replicationCompName) }) }) Context("when creating cluster with multiple kinds of components", func() { BeforeEach(func() { - By("Create a clusterDefinition obj") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). - Create(&testCtx).GetObject() - - By("Create a clusterVersion obj") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - AddComponent(nginxCompType).AddContainerShort("nginx", testapps.NginxImage). - Create(&testCtx).GetObject() + createAllWorkloadTypesClusterDef() + createBackupPolicyTpl(clusterDefObj) }) - It("should create all sub-resources successfully", func() { - checkAllResourcesCreated() - }) + createNWaitClusterObj := func(components map[string]string, + addedComponentProcessor func(compName string, factory *testapps.MockClusterFactory), + withFixedName ...bool) { + Expect(components).ShouldNot(BeEmpty()) - It("should create corresponding services correctly", func() { - checkAllServicesCreate() - }) + By("Creating a cluster") + clusterBuilder := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name) + + compNames := make([]string, 0, len(components)) + for compName, compDefName := range components { + clusterBuilder = clusterBuilder.AddComponent(compName, compDefName) + if addedComponentProcessor != nil { + addedComponentProcessor(compName, clusterBuilder) + } + compNames = append(compNames, compName) + } + if len(withFixedName) == 0 || !withFixedName[0] { + clusterBuilder.WithRandomName() + } + clusterObj = clusterBuilder.Create(&testCtx).GetObject() + clusterKey = client.ObjectKeyFromObject(clusterObj) - It("should add and delete service correctly", func() { - testServiceAddAndDelete() + By("Waiting for the cluster controller to create resources completely") + waitForCreatingResourceCompletely(clusterKey, compNames...) + } + + checkAllResourcesCreated := func(compNameNDef map[string]string) { + createNWaitClusterObj(compNameNDef, func(compName string, factory *testapps.MockClusterFactory) { + factory.SetReplicas(3) + }, true) + + By("Check deployment workload has been created") + Eventually(testapps.List(&testCtx, generics.DeploymentSignature, + client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterKey.Name, + }, client.InNamespace(clusterKey.Namespace))).ShouldNot(HaveLen(0)) + + stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) + + By("Check statefulset pod's volumes") + for _, sts := range stsList.Items { + podSpec := sts.Spec.Template + volumeNames := map[string]struct{}{} + for _, v := range podSpec.Spec.Volumes { + volumeNames[v.Name] = struct{}{} + } + + for _, cc := range [][]corev1.Container{ + podSpec.Spec.Containers, + podSpec.Spec.InitContainers, + } { + for _, c := range cc { + for _, vm := range c.VolumeMounts { + _, ok := volumeNames[vm.Name] + Expect(ok).Should(BeTrue()) + } + } + } + } + + By("Check associated PDB has been created") + Eventually(testapps.List(&testCtx, generics.PodDisruptionBudgetSignature, + client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterKey.Name, + }, client.InNamespace(clusterKey.Namespace))).ShouldNot(BeEmpty()) + + podSpec := stsList.Items[0].Spec.Template.Spec + By("Checking created sts pods template with built-in toleration") + Expect(podSpec.Tolerations).Should(HaveLen(1)) + Expect(podSpec.Tolerations[0].Key).To(Equal(testDataPlaneTolerationKey)) + + By("Checking created sts pods template with built-in Affinity") + Expect(podSpec.Affinity.PodAntiAffinity == nil && podSpec.Affinity.PodAffinity == nil).Should(BeTrue()) + Expect(podSpec.Affinity.NodeAffinity).ShouldNot(BeNil()) + Expect(podSpec.Affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Preference.MatchExpressions[0].Key).To( + Equal(testDataPlaneNodeAffinityKey)) + + By("Checking created sts pods template without TopologySpreadConstraints") + Expect(podSpec.TopologySpreadConstraints).Should(BeEmpty()) + + By("Check should create env configmap") + Eventually(func(g Gomega) { + cmList := &corev1.ConfigMapList{} + Expect(k8sClient.List(testCtx.Ctx, cmList, client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterKey.Name, + constant.AppConfigTypeLabelKey: "kubeblocks-env", + }, client.InNamespace(clusterKey.Namespace))).Should(Succeed()) + Expect(cmList.Items).ShouldNot(BeEmpty()) + Expect(cmList.Items).Should(HaveLen(len(compNameNDef))) + }).Should(Succeed()) + + By("Checking stateless services") + statelessExpectServices := map[string]ExpectService{ + // TODO: fix me later, proxy should not have internal headless service + testapps.ServiceHeadlessName: {svcType: corev1.ServiceTypeClusterIP, headless: true}, + testapps.ServiceDefaultName: {svcType: corev1.ServiceTypeClusterIP, headless: false}, + } + Eventually(func(g Gomega) { + validateCompSvcList(g, statelessCompName, statelessCompDefName, statelessExpectServices) + }).Should(Succeed()) + + By("Checking stateful types services") + for compName, compNameNDef := range compNameNDef { + if compName == statelessCompName { + continue + } + consensusExpectServices := map[string]ExpectService{ + testapps.ServiceHeadlessName: {svcType: corev1.ServiceTypeClusterIP, headless: true}, + testapps.ServiceDefaultName: {svcType: corev1.ServiceTypeClusterIP, headless: false}, + } + Eventually(func(g Gomega) { + validateCompSvcList(g, compName, compNameNDef, consensusExpectServices) + }).Should(Succeed()) + } + } + + testMultiCompHScale := func() { + compNameNDef := map[string]string{ + statefulCompName: statefulCompDefName, + consensusCompName: consensusCompDefName, + replicationCompName: replicationCompDefName, + } + initialReplicas := int32(1) + updatedReplicas := int32(3) + + By("Creating a multi components cluster with VolumeClaimTemplate") + pvcSpec := testapps.NewPVCSpec("1Gi") + + createNWaitClusterObj(compNameNDef, func(compName string, factory *testapps.MockClusterFactory) { + factory.AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec).SetReplicas(initialReplicas) + }) + + By("Waiting for the cluster controller to create resources completely") + waitForCreatingResourceCompletely(clusterKey, statefulCompName, consensusCompName, replicationCompName) + + // statefulCompDefName not in componentDefsWithHScalePolicy, for nil backup policy test + // REVIEW: + // 1. this test flow, wait for running phase? + horizontalScale(int(updatedReplicas), consensusCompDefName, replicationCompDefName) + } + + It("should create all sub-resources successfully, with terminationPolicy=Halt lifecycle", func() { + compNameNDef := map[string]string{ + statelessCompName: statelessCompDefName, + consensusCompName: consensusCompDefName, + statefulCompName: statefulCompDefName, + replicationCompName: replicationCompDefName, + } + checkAllResourcesCreated(compNameNDef) + + By("Mocking components' PVCs to bound") + stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) + for _, sts := range stsList.Items { + compName, ok := sts.Labels[constant.KBAppComponentLabelKey] + Expect(ok).Should(BeTrue()) + for i := int(*sts.Spec.Replicas); i >= 0; i-- { + pvcKey := types.NamespacedName{ + Namespace: clusterKey.Namespace, + Name: getPVCName(compName, i), + } + createPVC(clusterKey.Name, pvcKey.Name, compName) + Eventually(testapps.CheckObjExists(&testCtx, pvcKey, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) + Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Phase = corev1.ClaimBound + })()).ShouldNot(HaveOccurred()) + } + } + + By("delete the cluster and should preserved PVC,Secret,CM resources") + deleteCluster := func(termPolicy appsv1alpha1.TerminationPolicyType) { + // TODO: would be better that cluster is created with terminationPolicy=Halt instead of + // reassign the value after created + Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { + cluster.Spec.TerminationPolicy = termPolicy + })()).ShouldNot(HaveOccurred()) + testapps.DeleteObject(&testCtx, clusterKey, &appsv1alpha1.Cluster{}) + Eventually(testapps.CheckObjExists(&testCtx, clusterKey, &appsv1alpha1.Cluster{}, false)).Should(Succeed()) + } + deleteCluster(appsv1alpha1.Halt) + + By("check should preserved PVC,Secret,CM resources") + + checkPreservedObjects := func(uid types.UID) (*corev1.PersistentVolumeClaimList, *corev1.SecretList, *corev1.ConfigMapList) { + checkObject := func(obj client.Object) { + clusterJSON, ok := obj.GetAnnotations()[constant.LastAppliedClusterAnnotationKey] + Expect(ok).Should(BeTrue()) + Expect(clusterJSON).ShouldNot(BeEmpty()) + lastAppliedCluster := &appsv1alpha1.Cluster{} + Expect(json.Unmarshal([]byte(clusterJSON), lastAppliedCluster)).ShouldNot(HaveOccurred()) + Expect(lastAppliedCluster.UID).Should(BeEquivalentTo(uid)) + } + listOptions := []client.ListOption{ + client.InNamespace(clusterKey.Namespace), + client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterKey.Name, + }, + } + pvcList := &corev1.PersistentVolumeClaimList{} + Expect(k8sClient.List(testCtx.Ctx, pvcList, listOptions...)).Should(Succeed()) + + cmList := &corev1.ConfigMapList{} + Expect(k8sClient.List(testCtx.Ctx, cmList, listOptions...)).Should(Succeed()) + + secretList := &corev1.SecretList{} + Expect(k8sClient.List(testCtx.Ctx, secretList, listOptions...)).Should(Succeed()) + if uid != "" { + By("check pvc resources preserved") + Expect(pvcList.Items).ShouldNot(BeEmpty()) + + for _, pvc := range pvcList.Items { + checkObject(&pvc) + } + By("check secret resources preserved") + Expect(cmList.Items).ShouldNot(BeEmpty()) + for _, secret := range secretList.Items { + checkObject(&secret) + } + By("check configmap resources preserved") + Expect(secretList.Items).ShouldNot(BeEmpty()) + for _, cm := range cmList.Items { + checkObject(&cm) + } + } + return pvcList, secretList, cmList + } + initPVCList, initSecretList, initCMList := checkPreservedObjects(clusterObj.UID) + + By("create recovering cluster") + lastClusterUID := clusterObj.UID + checkAllResourcesCreated(compNameNDef) + Expect(clusterObj.UID).ShouldNot(Equal(lastClusterUID)) + lastPVCList, lastSecretList, lastCMList := checkPreservedObjects("") + + Expect(outOfOrderEqualFunc(initPVCList.Items, lastPVCList.Items, func(i corev1.PersistentVolumeClaim, j corev1.PersistentVolumeClaim) bool { + return i.UID == j.UID + })).Should(BeTrue()) + Expect(outOfOrderEqualFunc(initSecretList.Items, lastSecretList.Items, func(i corev1.Secret, j corev1.Secret) bool { + return i.UID == j.UID + })).Should(BeTrue()) + Expect(outOfOrderEqualFunc(initCMList.Items, lastCMList.Items, func(i corev1.ConfigMap, j corev1.ConfigMap) bool { + return i.UID == j.UID + })).Should(BeTrue()) + + By("delete the cluster and should preserved PVC,Secret,CM resources but result updated the new last applied cluster UID") + deleteCluster(appsv1alpha1.Halt) + checkPreservedObjects(clusterObj.UID) }) It("should successfully h-scale with multiple components", func() { @@ -1232,122 +1679,133 @@ var _ = Describe("Cluster Controller", func() { }) }) - Context("when creating cluster with workloadType=stateful component", func() { - BeforeEach(func() { - By("Create a clusterDefinition obj") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - Create(&testCtx).GetObject() + When("creating cluster with all workloadTypes (being Stateless|Stateful|Consensus|Replication) component", func() { + compNameNDef := map[string]string{ + statelessCompName: statelessCompDefName, + statefulCompName: statefulCompDefName, + consensusCompName: consensusCompDefName, + replicationCompName: replicationCompDefName, + } - By("Create a clusterVersion obj") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - Create(&testCtx).GetObject() + BeforeEach(func() { + createAllWorkloadTypesClusterDef() }) - It("should delete cluster resources immediately if deleting cluster with WipeOut termination policy", func() { - testWipeOut() - }) + for compName, compDefName := range compNameNDef { + It(fmt.Sprintf("[comp: %s] should delete cluster resources immediately if deleting cluster with terminationPolicy=WipeOut", compName), func() { + testWipeOut(compName, compDefName) + }) - It("should not terminate immediately if deleting cluster with DoNotTerminate termination policy", func() { - testDoNotTermintate() - }) + It(fmt.Sprintf("[comp: %s] should not terminate immediately if deleting cluster with terminationPolicy=DoNotTerminate", compName), func() { + testDoNotTermintate(compName, compDefName) + }) - It("should create/delete pods to match the desired replica number if updating cluster's replica number to a valid value", func() { - testChangeReplicas() - }) + It(fmt.Sprintf("[comp: %s] should add and delete service correctly", compName), func() { + testServiceAddAndDelete(compName, compDefName) + }) - Context("and with cluster affinity set", func() { - It("should create pod with cluster affinity", func() { - testClusterAffinity() + It(fmt.Sprintf("[comp: %s] should create/delete pods to match the desired replica number if updating cluster's replica number to a valid value", compName), func() { + testChangeReplicas(compName, compDefName) }) - }) - Context("and with both cluster affinity and component affinity set", func() { - It("Should observe the component affinity will override the cluster affinity", func() { - testComponentAffinity() + It(fmt.Sprintf("[comp: %s] should add serviceAccountName correctly", compName), func() { + testClusterServiceAccount(compName, compDefName) }) - }) - Context("and with cluster tolerations set", func() { - It("Should create pods with cluster tolerations", func() { - testClusterToleration() + Context(fmt.Sprintf("[comp: %s] and with cluster affinity set", compName), func() { + It("should create pod with cluster affinity", func() { + testClusterAffinity(compName, compDefName) + }) }) - }) - Context("and with both cluster tolerations and component tolerations set", func() { - It("Should observe the component tolerations will override the cluster tolerations", func() { - testComponentToleration() + Context(fmt.Sprintf("[comp: %s] and with both cluster affinity and component affinity set", compName), func() { + It("Should observe the component affinity will override the cluster affinity", func() { + testComponentAffinity(compName, compDefName) + }) }) - }) - Context("with pvc", func() { - It("should trigger a backup process(snapshot) and create pvcs from backup for newly created replicas when horizontal scale the cluster from 1 to 3", func() { - testHorizontalScale() + Context(fmt.Sprintf("[comp: %s] and with cluster tolerations set", compName), func() { + It("Should create pods with cluster tolerations", func() { + testClusterToleration(compName, compDefName) + }) }) - }) - Context("with pvc and dynamic-provisioning storage class", func() { - It("should update PVC request storage size accordingly when vertical scale the cluster", func() { - testVerticalScale() + Context(fmt.Sprintf("[comp: %s] and with both cluster tolerations and component tolerations set", compName), func() { + It("Should observe the component tolerations will override the cluster tolerations", func() { + testStsWorkloadComponentToleration(compName, compDefName) + }) }) - }) + } }) - Context("when creating cluster with workloadType=consensus component", func() { - BeforeEach(func() { - By("Create a clusterDef obj") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). - Create(&testCtx).GetObject() + When("creating cluster with stateful workloadTypes (being Stateful|Consensus|Replication) component", func() { + compNameNDef := map[string]string{ + statefulCompName: statefulCompDefName, + consensusCompName: consensusCompDefName, + replicationCompName: replicationCompDefName, + } - By("Create a clusterVersion obj") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - Create(&testCtx).GetObject() + BeforeEach(func() { + createAllWorkloadTypesClusterDef() + createBackupPolicyTpl(clusterDefObj) }) - It("Should success with one leader pod and two follower pods", func() { - testThreeReplicas() - }) + for compName, compDefName := range compNameNDef { + Context(fmt.Sprintf("[comp: %s] with pvc", compName), func() { + It("should trigger a backup process(snapshot) and create pvcs from backup for newly created replicas when horizontal scale the cluster from 1 to 3", func() { + testHorizontalScale(compName, compDefName) + }) + }) - It("should create/delete pods to match the desired replica number if updating cluster's replica number to a valid value", func() { - testChangeReplicas() - }) + Context(fmt.Sprintf("[comp: %s] with pvc and dynamic-provisioning storage class", compName), func() { + It("should update PVC request storage size accordingly", func() { + testStorageExpansion(compName, compDefName) + }) + }) - Context("with pvc", func() { - It("should trigger a backup process(snapshot) and create pvcs from backup for newly created replicas when horizontal scale the cluster from 1 to 3", func() { - testHorizontalScale() + It(fmt.Sprintf("[comp: %s] should be able to recover if volume expansion fails", compName), func() { + testVolumeExpansionFailedAndRecover(compName, compDefName) }) - }) - Context("with pvc and dynamic-provisioning storage class", func() { - It("should update PVC request storage size accordingly when vertical scale the cluster", func() { - testVerticalScale() + It(fmt.Sprintf("[comp: %s] should report error if backup error during horizontal scale", compName), func() { + testBackupError(compName, compDefName) }) - }) - Context("with horizontalScale after verticalScale", func() { - It("should succeed", func() { - testVerticalScale() - horizontalScale(5) + Context(fmt.Sprintf("[comp: %s] with horizontal scale after storage expansion", compName), func() { + It("should succeed with horizontal scale to 5 replicas", func() { + testStorageExpansion(compName, compDefName) + horizontalScale(5, compDefName) + }) }) + } + }) + + When("creating cluster with workloadType=consensus component", func() { + const ( + compName = consensusCompName + compDefName = consensusCompDefName + ) + + BeforeEach(func() { + createAllWorkloadTypesClusterDef() + createBackupPolicyTpl(clusterDefObj) }) - It("should report error if backup error during h-scale", func() { - testBackupError() + It("Should success with one leader pod and two follower pods", func() { + testThreeReplicas(compName, compDefName) }) It("test restore cluster from backup", func() { - By("mock backup") + By("mock backuptool object") backupPolicyName := "test-backup-policy" backupName := "test-backup" backupTool := testapps.CreateCustomizedObj(&testCtx, "backup/backuptool.yaml", &dataprotectionv1alpha1.BackupTool{}, testapps.RandomizedObjName()) + By("creating backup") backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). SetBackupPolicyName(backupPolicyName). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() By("waiting for backup failed, because no backup policy exists") @@ -1355,86 +1813,94 @@ var _ = Describe("Cluster Controller", func() { func(g Gomega, tmpBackup *dataprotectionv1alpha1.Backup) { g.Expect(tmpBackup.Status.Phase).Should(Equal(dataprotectionv1alpha1.BackupFailed)) })).Should(Succeed()) + By("mocking backup status completed, we don't need backup reconcile here") - Expect(testapps.ChangeObjStatus(&testCtx, backup, func() { + Eventually(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(backup), func(backup *dataprotectionv1alpha1.Backup) { backup.Status.BackupToolName = backupTool.Name - backup.Status.RemoteVolume = &corev1.Volume{ - Name: "backup-pvc", - } + backup.Status.PersistentVolumeClaimName = "backup-pvc" backup.Status.Phase = dataprotectionv1alpha1.BackupCompleted - })).ShouldNot(HaveOccurred()) + })).Should(Succeed()) + By("checking backup status completed") Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(backup), func(g Gomega, tmpBackup *dataprotectionv1alpha1.Backup) { g.Expect(tmpBackup.Status.Phase).Should(Equal(dataprotectionv1alpha1.BackupCompleted)) })).Should(Succeed()) + By("creating cluster with backup") - restoreFromBackup := fmt.Sprintf(`{"%s":"%s"}`, mysqlCompName, backupName) - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + restoreFromBackup := fmt.Sprintf(`{"%s":"%s"}`, compName, backupName) + pvcSpec := testapps.NewPVCSpec("1Gi") + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(compName, compDefName). SetReplicas(3). + AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). AddAnnotations(constant.RestoreFromBackUpAnnotationKey, restoreFromBackup).Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) - By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, mysqlCompName) + By("mocking restore job completed") + patchK8sJobStatus := func(key types.NamespacedName, jobStatus batchv1.JobConditionType) { + Eventually(testapps.GetAndChangeObjStatus(&testCtx, key, func(fetched *batchv1.Job) { + jobCondition := batchv1.JobCondition{Type: jobStatus} + fetched.Status.Conditions = append(fetched.Status.Conditions, jobCondition) + })).Should(Succeed()) + } + for i := 0; i < 3; i++ { + restoreJobKey := client.ObjectKey{ + Name: fmt.Sprintf("base-%s-%s-%d", clusterObj.Name, compName, i), + Namespace: clusterKey.Namespace, + } + patchK8sJobStatus(restoreJobKey, batchv1.JobComplete) + } + By("Waiting for the cluster controller to create resources completely") + waitForCreatingResourceCompletely(clusterKey, compName) stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) sts := stsList.Items[0] - Expect(sts.Spec.Template.Spec.InitContainers).Should(HaveLen(1)) By("mock pod/sts are available and wait for component enter running phase") - testapps.MockConsensusComponentPods(testCtx, &sts, clusterObj.Name, mysqlCompName) + testapps.MockConsensusComponentPods(&testCtx, &sts, clusterObj.Name, compName) Expect(testapps.ChangeObjStatus(&testCtx, &sts, func() { testk8s.MockStatefulSetReady(&sts) })).ShouldNot(HaveOccurred()) - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterObj.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) - - By("remove init container after all components are Running") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, client.ObjectKeyFromObject(clusterObj))).Should(BeEquivalentTo(1)) - Expect(k8sClient.Get(ctx, client.ObjectKeyFromObject(clusterObj), clusterObj)).Should(Succeed()) - Expect(testapps.ChangeObjStatus(&testCtx, clusterObj, func() { - clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ - mysqlCompName: {Phase: appsv1alpha1.RunningClusterCompPhase}, - } - })).Should(Succeed()) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, compName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + + By("the restore container has been removed from init containers") Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(&sts), func(g Gomega, tmpSts *appsv1.StatefulSet) { g.Expect(tmpSts.Spec.Template.Spec.InitContainers).Should(BeEmpty()) })).Should(Succeed()) By("clean up annotations after cluster running") - Expect(testapps.ChangeObjStatus(&testCtx, clusterObj, func() { - clusterObj.Status.Phase = appsv1alpha1.RunningClusterPhase - })).ShouldNot(HaveOccurred()) + Expect(testapps.GetAndChangeObjStatus(&testCtx, clusterKey, func(tmpCluster *appsv1alpha1.Cluster) { + compStatus := tmpCluster.Status.Components[compName] + compStatus.Phase = appsv1alpha1.RunningClusterCompPhase + tmpCluster.Status.Components[compName] = compStatus + })()).Should(Succeed()) Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + g.Expect(tmpCluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) g.Expect(tmpCluster.Annotations[constant.RestoreFromBackUpAnnotationKey]).Should(BeEmpty()) })).Should(Succeed()) }) }) - Context("when creating cluster with workloadType=replication component", func() { + When("creating cluster with workloadType=replication component", func() { + const ( + compName = replicationCompName + compDefName = replicationCompDefName + ) BeforeEach(func() { - By("Create a clusterDefinition obj with replication componentDefRef.") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). - Create(&testCtx).GetObject() - - By("Create a clusterVersion obj with replication componentDefRef.") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(testapps.DefaultRedisCompType). - AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). - Create(&testCtx).GetObject() + createAllWorkloadTypesClusterDef() + createBackupPolicyTpl(clusterDefObj) }) // REVIEW/TODO: following test always failed at cluster.phase.observerGeneration=1 // with cluster.phase.phase=creating - It("Should success with primary sts and secondary sts", func() { + It("Should success with primary pod and secondary pod", func() { By("Mock a cluster obj with replication componentDefRef.") pvcSpec := testapps.NewPVCSpec("1Gi") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). + AddComponent(compName, compDefName). SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). SetReplicas(testapps.DefaultReplicationReplicas). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). @@ -1442,172 +1908,35 @@ var _ = Describe("Cluster Controller", func() { clusterKey = client.ObjectKeyFromObject(clusterObj) By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, testapps.DefaultRedisCompName) + waitForCreatingResourceCompletely(clusterKey, compDefName) By("Checking statefulSet number") - var stsList *appsv1.StatefulSetList - Eventually(func(g Gomega) { - stsList = testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) - g.Expect(stsList.Items).Should(HaveLen(testapps.DefaultReplicationReplicas)) - }).Should(Succeed()) + stsList := testk8s.ListAndCheckStatefulSetItemsCount(&testCtx, clusterKey, 1) + sts := &stsList.Items[0] - By("Checking statefulSet role label") - for _, sts := range stsList.Items { - if strings.HasSuffix(sts.Name, fmt.Sprintf("%s-%s", clusterObj.Name, testapps.DefaultRedisCompName)) { - Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Primary)) - } else { - Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Secondary)) - } - } - - By("Checking statefulSet template volumes mount") - for _, sts := range stsList.Items { - Expect(sts.Spec.VolumeClaimTemplates).Should(BeEmpty()) - for _, volume := range sts.Spec.Template.Spec.Volumes { - if volume.Name == testapps.DataVolumeName { - Expect(strings.HasPrefix(volume.VolumeSource.PersistentVolumeClaim.ClaimName, testapps.DataVolumeName+"-"+clusterKey.Name)).Should(BeTrue()) - } - } - Expect(testapps.ChangeObjStatus(&testCtx, &sts, func() { - testk8s.MockStatefulSetReady(&sts) - })).ShouldNot(HaveOccurred()) - podName := sts.Name + "-0" - testapps.MockReplicationComponentStsPod(testCtx, &sts, clusterObj.Name, testapps.DefaultRedisCompName, podName, sts.Labels[constant.RoleLabelKey]) - } - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) - }) - - It("Should successfully doing volume expansion", func() { - storageClassName := "test-storage" - pvcSpec := testapps.NewPVCSpec("1Gi") - pvcSpec.StorageClassName = &storageClassName - updatedPVCSpec := testapps.NewPVCSpec("2Gi") - updatedPVCSpec.StorageClassName = &storageClassName - - By("Mock a cluster obj with replication componentDefRef.") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). - SetReplicas(testapps.DefaultReplicationReplicas). - AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). - Create(&testCtx).GetObject() - clusterKey = client.ObjectKeyFromObject(clusterObj) - - By("Waiting for the cluster controller to create resources completely") - waitForCreatingResourceCompletely(clusterKey, testapps.DefaultRedisCompName) - - // REVIEW: this test flow, should wait/fake still Running phase? - - By("Checking statefulset count") - stsList := testk8s.ListAndCheckStatefulSetCount(&testCtx, clusterKey, testapps.DefaultReplicationReplicas) - - By("Creating mock pods in StatefulSet") - pods := mockPodsForReplicationTest(clusterObj, stsList.Items) - for _, pod := range pods { - Expect(testCtx.CreateObj(testCtx.Ctx, &pod)).Should(Succeed()) - pod.Status.Conditions = []corev1.PodCondition{{ - Type: corev1.PodReady, - Status: corev1.ConditionTrue, - }} - Expect(testCtx.Cli.Status().Update(testCtx.Ctx, &pod)).Should(Succeed()) - } - - By("Checking pod count and ready") - Eventually(func(g Gomega) { - podList := testk8s.ListAndCheckPodCountWithComponent(&testCtx, clusterKey, testapps.DefaultRedisCompName, testapps.DefaultReplicationReplicas) - for _, pod := range podList.Items { - g.Expect(len(pod.Status.Conditions) > 0).Should(BeTrue()) - g.Expect(pod.Status.Conditions[0].Status).Should(Equal(corev1.ConditionTrue)) - } - }).Should(Succeed()) - - By("Mocking statefulset status to ready") - for _, sts := range stsList.Items { - sts.Status.ObservedGeneration = sts.Generation - sts.Status.AvailableReplicas = 1 - sts.Status.Replicas = 1 - sts.Status.ReadyReplicas = 1 - err := testCtx.Cli.Status().Update(testCtx.Ctx, &sts) - Expect(err).ShouldNot(HaveOccurred()) + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + testk8s.MockStatefulSetReady(sts) + })).ShouldNot(HaveOccurred()) + for i := int32(0); i < *sts.Spec.Replicas; i++ { + podName := fmt.Sprintf("%s-%d", sts.Name, i) + testapps.MockReplicationComponentPod(nil, testCtx, sts, clusterObj.Name, + compDefName, podName, replication.DefaultRole(i)) } - - By("Checking reconcile succeeded") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) - - By("Creating storageclass") - _ = testapps.CreateStorageClass(testCtx, storageClassName, true) - - pvcList := &corev1.PersistentVolumeClaimList{} - - By("Mocking PVCs status to bound") - Expect(testCtx.Cli.List(testCtx.Ctx, pvcList, client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - constant.KBAppComponentLabelKey: testapps.DefaultRedisCompName, - }, client.InNamespace(clusterKey.Namespace))).Should(Succeed()) - Expect(pvcList.Items).Should(HaveLen(testapps.DefaultReplicationReplicas)) - for _, pvc := range pvcList.Items { - pvc.Status.Phase = corev1.ClaimBound - Expect(testCtx.Cli.Status().Update(testCtx.Ctx, &pvc)).Should(Succeed()) - } - - By("Checking PVCs status bound") - Eventually(func(g Gomega) { - g.Expect(testCtx.Cli.List(testCtx.Ctx, pvcList, client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - constant.KBAppComponentLabelKey: testapps.DefaultRedisCompName, - }, client.InNamespace(clusterKey.Namespace))).Should(Succeed()) - Expect(pvcList.Items).Should(HaveLen(testapps.DefaultReplicationReplicas)) - for _, pvc := range pvcList.Items { - g.Expect(pvc.Status.Phase).Should(Equal(corev1.ClaimBound)) - } - }).Should(Succeed()) - - By("Updating PVC volume size") - patch := client.MergeFrom(clusterObj.DeepCopy()) - componentSpec := clusterObj.Spec.GetComponentByName(testapps.DefaultRedisCompName) - componentSpec.VolumeClaimTemplates[0].Spec = updatedPVCSpec - Expect(testCtx.Cli.Patch(ctx, clusterObj, patch)).Should(Succeed()) - - By("Waiting cluster update reconcile succeed") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(2)) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) - - By("Checking pvc volume size") - Eventually(func(g Gomega) { - g.Expect(testCtx.Cli.List(testCtx.Ctx, pvcList, client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterKey.Name, - constant.KBAppComponentLabelKey: testapps.DefaultRedisCompName, - }, client.InNamespace(clusterKey.Namespace))).Should(Succeed()) - g.Expect(len(pvcList.Items) == testapps.DefaultReplicationReplicas).To(BeTrue()) - for _, pvc := range pvcList.Items { - g.Expect(pvc.Spec.Resources.Requests[corev1.ResourceStorage]).Should(BeEquivalentTo(updatedPVCSpec.Resources.Requests[corev1.ResourceStorage])) - } - }).Should(Succeed()) }) }) Context("test cluster Failed/Abnormal phase", func() { It("test cluster conditions", func() { By("init cluster") - cluster := testapps.CreateConsensusMysqlCluster(testCtx, clusterDefNameRand, - clusterVersionNameRand, clusterNameRand, consensusCompType, consensusCompName) + cluster := testapps.CreateConsensusMysqlCluster(&testCtx, clusterDefNameRand, + clusterVersionNameRand, clusterNameRand, consensusCompDefName, consensusCompName, + "2Gi") clusterKey := client.ObjectKeyFromObject(cluster) - By("mock pvc created") - for i := 0; i < 3; i++ { - pvcName := fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, consensusCompName, i) - pvc := testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterKey.Name, - consensusCompName, "data").SetStorage("2Gi").Create(&testCtx).GetObject() - // mock pvc bound - Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(pvc), func(pvc *corev1.PersistentVolumeClaim) { - pvc.Status.Phase = corev1.ClaimBound - })()).ShouldNot(HaveOccurred()) - } - By("test when clusterDefinition not found") Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + g.Expect(tmpCluster.Status.ObservedGeneration).Should(BeZero()) condition := meta.FindStatusCondition(tmpCluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) g.Expect(condition).ShouldNot(BeNil()) g.Expect(condition.Reason).Should(BeEquivalentTo(lifecycle.ReasonPreCheckFailed)) @@ -1626,8 +1955,8 @@ var _ = Describe("Cluster Controller", func() { // })).Should(Succeed()) By("test when clusterVersion not Available") - _ = testapps.CreateConsensusMysqlClusterDef(testCtx, clusterDefNameRand, consensusCompType) - clusterVersion := testapps.CreateConsensusMysqlClusterVersion(testCtx, clusterDefNameRand, clusterVersionNameRand, consensusCompType) + _ = testapps.CreateConsensusMysqlClusterDef(&testCtx, clusterDefNameRand, consensusCompDefName) + clusterVersion := testapps.CreateConsensusMysqlClusterVersion(&testCtx, clusterDefNameRand, clusterVersionNameRand, consensusCompDefName) clusterVersionKey := client.ObjectKeyFromObject(clusterVersion) // mock clusterVersion unavailable Expect(testapps.GetAndChangeObj(&testCtx, clusterVersionKey, func(clusterVersion *appsv1alpha1.ClusterVersion) { @@ -1635,7 +1964,7 @@ var _ = Describe("Cluster Controller", func() { })()).ShouldNot(HaveOccurred()) Eventually(testapps.CheckObj(&testCtx, clusterVersionKey, func(g Gomega, clusterVersion *appsv1alpha1.ClusterVersion) { - g.Expect(clusterVersion.Status.Phase == appsv1alpha1.UnavailablePhase).Should(BeTrue()) + g.Expect(clusterVersion.Status.Phase).Should(Equal(appsv1alpha1.UnavailablePhase)) })).Should(Succeed()) // trigger reconcile @@ -1646,6 +1975,7 @@ var _ = Describe("Cluster Controller", func() { Eventually(func(g Gomega) { updateClusterAnnotation(cluster) g.Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + g.Expect(cluster.Status.ObservedGeneration).Should(BeZero()) condition := meta.FindStatusCondition(cluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) g.Expect(condition).ShouldNot(BeNil()) g.Expect(condition.Reason).Should(BeEquivalentTo(lifecycle.ReasonPreCheckFailed)) @@ -1658,15 +1988,17 @@ var _ = Describe("Cluster Controller", func() { })()).ShouldNot(HaveOccurred()) Eventually(testapps.CheckObj(&testCtx, clusterVersionKey, func(g Gomega, clusterVersion *appsv1alpha1.ClusterVersion) { - g.Expect(clusterVersion.Status.Phase == appsv1alpha1.AvailablePhase).Should(BeTrue()) + g.Expect(clusterVersion.Status.Phase).Should(Equal(appsv1alpha1.AvailablePhase)) })).Should(Succeed()) // trigger reconcile updateClusterAnnotation(cluster) By("test preCheckFailed") Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + g.Expect(cluster.Status.ObservedGeneration).Should(BeZero()) condition := meta.FindStatusCondition(cluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) - g.Expect(condition != nil && condition.Reason == lifecycle.ReasonPreCheckFailed).Should(BeTrue()) + g.Expect(condition).ShouldNot(BeNil()) + g.Expect(condition.Reason).Should(Equal(lifecycle.ReasonPreCheckFailed)) })).Should(Succeed()) By("reset and waiting cluster to Creating") @@ -1679,15 +2011,31 @@ var _ = Describe("Cluster Controller", func() { g.Expect(tmpCluster.Status.ObservedGeneration).ShouldNot(BeZero()) })).Should(Succeed()) - By("test apply resources failed") + By("mock pvc of component to create") + for i := 0; i < testapps.ConsensusReplicas; i++ { + pvcName := fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, consensusCompName, i) + pvc := testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterKey.Name, + consensusCompName, "data").SetStorage("2Gi").Create(&testCtx).GetObject() + // mock pvc bound + Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(pvc), func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Phase = corev1.ClaimBound + pvc.Status.Capacity = corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("2Gi"), + } + })()).ShouldNot(HaveOccurred()) + } + + By("apply smaller PVC size will should failed") Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { tmpCluster.Spec.ComponentSpecs[0].VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = resource.MustParse("1Gi") })()).ShouldNot(HaveOccurred()) Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + // REVIEW/TODO: (wangyelei) following expects causing inconsistent behavior condition := meta.FindStatusCondition(tmpCluster.Status.Conditions, appsv1alpha1.ConditionTypeApplyResources) - g.Expect(condition != nil && condition.Reason == lifecycle.ReasonApplyResourcesFailed).Should(BeTrue()) + g.Expect(condition).ShouldNot(BeNil()) + g.Expect(condition.Reason).Should(Equal(lifecycle.ReasonApplyResourcesFailed)) })).Should(Succeed()) }) }) @@ -1695,17 +2043,36 @@ var _ = Describe("Cluster Controller", func() { func createBackupPolicyTpl(clusterDefObj *appsv1alpha1.ClusterDefinition) { By("Creating a BackupPolicyTemplate") - backupPolicyTplKey := types.NamespacedName{Name: "test-backup-policy-template-mysql"} - backupPolicyTpl := &dataprotectionv1alpha1.BackupPolicyTemplate{ - ObjectMeta: metav1.ObjectMeta{ - Name: backupPolicyTplKey.Name, - Labels: map[string]string{ - clusterDefLabelKey: clusterDefObj.Name, - }, - }, - Spec: dataprotectionv1alpha1.BackupPolicyTemplateSpec{ - BackupToolName: "mysql-xtrabackup", - }, + bpt := testapps.NewBackupPolicyTemplateFactory(backupPolicyTPLName). + AddLabels(clusterDefLabelKey, clusterDefObj.Name). + SetClusterDefRef(clusterDefObj.Name) + for _, v := range clusterDefObj.Spec.ComponentDefs { + bpt = bpt.AddBackupPolicy(v.Name).AddSnapshotPolicy() + switch v.WorkloadType { + case appsv1alpha1.Consensus: + bpt.SetTargetRole("leader") + case appsv1alpha1.Replication: + bpt.SetTargetRole("primary") + } + } + bpt.Create(&testCtx) +} + +func outOfOrderEqualFunc[E1, E2 any](s1 []E1, s2 []E2, eq func(E1, E2) bool) bool { + if l := len(s1); l != len(s2) { + return false + } + + for _, v1 := range s1 { + isEq := false + for _, v2 := range s2 { + if isEq = eq(v1, v2); isEq { + break + } + } + if !isEq { + return false + } } - Expect(testCtx.CreateObj(testCtx.Ctx, backupPolicyTpl)).Should(Succeed()) + return true } diff --git a/controllers/apps/cluster_status_event_handler.go b/controllers/apps/cluster_status_event_handler.go new file mode 100644 index 000000000..9216ddfb6 --- /dev/null +++ b/controllers/apps/cluster_status_event_handler.go @@ -0,0 +1,211 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + "context" + "regexp" + "time" + + "golang.org/x/exp/slices" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/controllers/k8score" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + probeutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" +) + +// EventTimeOut timeout of the event +const EventTimeOut = 30 * time.Second + +// ClusterStatusEventHandler is the event handler for the cluster status event +type ClusterStatusEventHandler struct{} + +var _ k8score.EventHandler = &ClusterStatusEventHandler{} + +func init() { + k8score.EventHandlerMap["cluster-status-handler"] = &ClusterStatusEventHandler{} +} + +// Handle handles the cluster status events. +func (r *ClusterStatusEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) error { + if event.Reason != string(probeutil.CheckRoleOperation) { + return handleEventForClusterStatus(reqCtx.Ctx, cli, recorder, event) + } + + // parse probe event message when field path is probe-role-changed-check + message := k8score.ParseProbeEventMessage(reqCtx, event) + if message == nil { + reqCtx.Log.Info("parse probe event message failed", "message", event.Message) + return nil + } + + // if probe message event is checkRoleFailed, it means the cluster is abnormal, need to handle the cluster status + if message.Event == probeutil.OperationFailed { + return handleEventForClusterStatus(reqCtx.Ctx, cli, recorder, event) + } + return nil +} + +// TODO: Unified cluster event processing +// handleEventForClusterStatus handles event for cluster Warning and Failed phase +func handleEventForClusterStatus(ctx context.Context, cli client.Client, recorder record.EventRecorder, event *corev1.Event) error { + + type predicateProcessor struct { + pred func() bool + processor func() error + } + + nilReturnHandler := func() error { return nil } + + pps := []predicateProcessor{ + { + // handle cronjob complete or fail event + pred: func() bool { + return event.InvolvedObject.Kind == constant.CronJobKind && + event.Reason == "SawCompletedJob" + }, + processor: func() error { + return handleDeletePVCCronJobEvent(ctx, cli, recorder, event) + }, + }, + { + pred: func() bool { + return event.Type != corev1.EventTypeWarning || + !isTargetKindForEvent(event) + }, + processor: nilReturnHandler, + }, + { + pred: func() bool { + // the error repeated several times, so we can be sure it's a real error to the cluster. + return !k8score.IsOvertimeEvent(event, EventTimeOut) + }, + processor: nilReturnHandler, + }, + { + // handle cluster workload error events such as pod/statefulset/deployment errors + // must be the last one + pred: func() bool { + return true + }, + processor: func() error { + return handleClusterStatusByEvent(ctx, cli, recorder, event) + }, + }, + } + + for _, pp := range pps { + if pp.pred() { + return pp.processor() + } + } + return nil +} + +func handleDeletePVCCronJobEvent(ctx context.Context, cli client.Client, recorder record.EventRecorder, event *corev1.Event) error { + re := regexp.MustCompile("status: Failed") + var ( + err error + object client.Object + ) + matches := re.FindStringSubmatch(event.Message) + if len(matches) == 0 { + // TODO(impl): introduce a one-shot delayed job to delete the pvc object. + // delete pvc succeeded, then delete cronjob + return checkedDeleteDeletePVCCronJob(ctx, cli, event.InvolvedObject.Name, event.InvolvedObject.Namespace) + } + // cronjob failed + if object, err = getEventInvolvedObject(ctx, cli, event); err != nil { + return err + } + return notifyClusterStatusChange(ctx, cli, recorder, object, event) +} + +func checkedDeleteDeletePVCCronJob(ctx context.Context, cli client.Client, name string, namespace string) error { + // label check + cronJob := batchv1.CronJob{} + if err := cli.Get(ctx, types.NamespacedName{ + Namespace: namespace, + Name: name, + }, &cronJob); err != nil { + return client.IgnoreNotFound(err) + } + if cronJob.ObjectMeta.Labels[constant.AppManagedByLabelKey] != constant.AppName { + return nil + } + // check the delete-pvc-cronjob annotation. + // the reason for this is that the backup policy also creates cronjobs, + // which need to be distinguished by the annotation. + if cronJob.ObjectMeta.Annotations[lifecycleAnnotationKey] != lifecycleDeletePVCAnnotation { + return nil + } + // if managed by kubeblocks, then it must be the cronjob used to delete pvc, delete it since it's completed + if err := cli.Delete(ctx, &cronJob); err != nil { + return client.IgnoreNotFound(err) + } + return nil +} + +// handleClusterStatusByEvent handles the cluster status when warning event happened +func handleClusterStatusByEvent(ctx context.Context, cli client.Client, recorder record.EventRecorder, event *corev1.Event) error { + object, err := getEventInvolvedObject(ctx, cli, event) + if err != nil { + return err + } + return notifyClusterStatusChange(ctx, cli, recorder, object, event) +} + +// getEventInvolvedObject gets event involved object for StatefulSet/Deployment/Pod workload +func getEventInvolvedObject(ctx context.Context, cli client.Client, event *corev1.Event) (client.Object, error) { + objectKey := client.ObjectKey{ + Name: event.InvolvedObject.Name, + Namespace: event.InvolvedObject.Namespace, + } + var err error + // If client.object interface object is used as a parameter, it will not return an error when the object is not found. + // so we should specify the object type to get the object. + switch event.InvolvedObject.Kind { + case constant.PodKind: + pod := &corev1.Pod{} + err = cli.Get(ctx, objectKey, pod) + return pod, err + case constant.StatefulSetKind: + sts := &appsv1.StatefulSet{} + err = cli.Get(ctx, objectKey, sts) + return sts, err + case constant.DeploymentKind: + deployment := &appsv1.Deployment{} + err = cli.Get(ctx, objectKey, deployment) + return deployment, err + } + return nil, err +} + +// isTargetKindForEvent checks the event involved object is one of the target resources +func isTargetKindForEvent(event *corev1.Event) bool { + return slices.Index([]string{constant.PodKind, constant.DeploymentKind, constant.StatefulSetKind}, event.InvolvedObject.Kind) != -1 +} diff --git a/controllers/apps/cluster_status_event_handler_test.go b/controllers/apps/cluster_status_event_handler_test.go new file mode 100644 index 000000000..bb7e4221b --- /dev/null +++ b/controllers/apps/cluster_status_event_handler_test.go @@ -0,0 +1,359 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" +) + +var _ = Describe("test cluster Failed/Abnormal phase", func() { + + var ( + ctx = context.Background() + clusterName = "" + clusterDefName = "" + clusterVersionName = "" + ) + + setupResourceNames := func() { + suffix := testCtx.GetRandomStr() + clusterName = "cluster-for-status-" + suffix + clusterDefName = "clusterdef-for-status-" + suffix + clusterVersionName = "cluster-version-for-status-" + suffix + } + + cleanEnv := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + if clusterName != "" { + testapps.ClearClusterResources(&testCtx) + + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + // testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) + // testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PodSignature, true, inNS, ml) + } + + // reset all resource names + setupResourceNames() + } + BeforeEach(cleanEnv) + + AfterEach(cleanEnv) + + const statefulMySQLCompType = "stateful" + const statefulMySQLCompName = "stateful" + + const consensusMySQLCompType = "consensus" + const consensusMySQLCompName = "consensus" + + const statelessCompType = "stateless" + const statelessCompName = "nginx" + + createClusterDef := func() { + _ = testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.StatefulMySQLComponent, statefulMySQLCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, consensusMySQLCompType). + AddComponentDef(testapps.StatelessNginxComponent, statelessCompType). + Create(&testCtx) + } + + createClusterVersion := func() { + _ = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). + AddComponentVersion(statefulMySQLCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(consensusMySQLCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(statelessCompType).AddContainerShort(testapps.DefaultNginxContainerName, testapps.NginxImage). + Create(&testCtx) + } + + createCluster := func() *appsv1alpha1.Cluster { + return testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). + AddComponent(statefulMySQLCompName, statefulMySQLCompType).SetReplicas(3). + AddComponent(consensusMySQLCompName, consensusMySQLCompType).SetReplicas(3). + AddComponent(statelessCompName, statelessCompType).SetReplicas(3). + Create(&testCtx).GetObject() + } + + // createStsPod := func(podName, podRole, componentName string) *corev1.Pod { + // return testapps.NewPodFactory(testCtx.DefaultNamespace, podName). + // AddAppInstanceLabel(clusterName). + // AddAppComponentLabel(componentName). + // AddRoleLabel(podRole). + // AddAppManangedByLabel(). + // AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). + // Create(&testCtx).GetObject() + // } + + // getDeployment := func(componentName string) *appsv1.Deployment { + // deployList := &appsv1.DeploymentList{} + // Eventually(func(g Gomega) { + // g.Expect(k8sClient.List(ctx, deployList, + // client.MatchingLabels{ + // constant.KBAppComponentLabelKey: componentName, + // constant.AppInstanceLabelKey: clusterName}, + // client.Limit(1))).ShouldNot(HaveOccurred()) + // g.Expect(deployList.Items).Should(HaveLen(1)) + // }).Should(Succeed()) + // return &deployList.Items[0] + // } + + // handleAndCheckComponentStatus := func(componentName string, event *corev1.Event, + // expectClusterPhase appsv1alpha1.ClusterPhase, + // expectCompPhase appsv1alpha1.ClusterComponentPhase, + // checkClusterPhase bool) { + // Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace}, + // func(g Gomega, newCluster *appsv1alpha1.Cluster) { + // g.Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) + // if checkClusterPhase { + // g.Expect(newCluster.Status.Phase).Should(Equal(expectClusterPhase)) + // } else { + // compStatus := newCluster.Status.Components[componentName] + // g.Expect(compStatus.Phase).Should(Equal(expectCompPhase)) + // } + // })).Should(Succeed()) + // } + + // setInvolvedObject := func(event *corev1.Event, kind, objectName string) { + // event.InvolvedObject.Kind = kind + // event.InvolvedObject.Name = objectName + // } + + Context("test cluster Failed/Abnormal phase", func() { + It("test cluster Failed/Abnormal phase", func() { + By("create cluster related resources") + createClusterDef() + createClusterVersion() + // cluster := createCluster() + createCluster() + + // wait for cluster's status to become stable so that it won't interfere with later tests + Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace}, + func(g Gomega, fetched *appsv1alpha1.Cluster) { + g.Expect(fetched.Generation).To(BeEquivalentTo(1)) + g.Expect(fetched.Status.ObservedGeneration).To(BeEquivalentTo(1)) + g.Expect(fetched.Status.Phase).To(Equal(appsv1alpha1.CreatingClusterPhase)) + })).Should(Succeed()) + + By("watch normal event") + event := &corev1.Event{ + Count: 1, + Type: corev1.EventTypeNormal, + Message: "create pod failed because the pvc is deleting", + } + Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) + + By("watch warning event from StatefulSet, but mismatch condition ") + // wait for StatefulSet created by cluster controller + stsName := clusterName + "-" + statefulMySQLCompName + Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: stsName, Namespace: testCtx.DefaultNamespace}, + func(g Gomega, fetched *appsv1.StatefulSet) { + g.Expect(fetched.Generation).To(BeEquivalentTo(1)) + })).Should(Succeed()) + stsInvolvedObject := corev1.ObjectReference{ + Name: stsName, + Kind: constant.StatefulSetKind, + Namespace: testCtx.DefaultNamespace, + } + event.InvolvedObject = stsInvolvedObject + event.Type = corev1.EventTypeWarning + Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) + + // TODO(refactor): mock event for stateful workload + // By("watch warning event from StatefulSet and component workload type is Stateful") + // podName0 := stsName + "-0" + // pod0 := createStsPod(podName0, "", statefulMySQLCompName) + // Expect(testapps.ChangeObjStatus(&testCtx, pod0, func() { + // pod0.Status.ContainerStatuses = []corev1.ContainerStatus{ + // { + // State: corev1.ContainerState{ + // Waiting: &corev1.ContainerStateWaiting{ + // Reason: "ImagePullBackOff", + // Message: "Back-off pulling image nginx:latest", + // }, + // }, + // }, + // } + // pod0.Status.Conditions = []corev1.PodCondition{ + // { + // Type: corev1.ContainersReady, + // Status: corev1.ConditionFalse, + // LastTransitionTime: metav1.NewTime(time.Now().Add(-2 * time.Minute)), // Timed-out + // }, + // } + // })).Should(Succeed()) + // event.Count = 3 + // event.FirstTimestamp = metav1.Time{Time: time.Now()} + // event.LastTimestamp = metav1.Time{Time: time.Now().Add(EventTimeOut + time.Second)} + // handleAndCheckComponentStatus(statefulMySQLCompName, event, + // appsv1alpha1.FailedClusterPhase, + // appsv1alpha1.FailedClusterCompPhase, + // false) + + // By("watch warning event from Pod and component workload type is Consensus") + //// wait for StatefulSet created by cluster controller + // stsName = clusterName + "-" + consensusMySQLCompName + // Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: stsName, Namespace: testCtx.DefaultNamespace}, + // func(g Gomega, fetched *appsv1.StatefulSet) { + // g.Expect(fetched.Generation).To(BeEquivalentTo(1)) + // })).Should(Succeed()) + //// create a failed pod + // podName := stsName + "-0" + // createStsPod(podName, "", consensusMySQLCompName) + // setInvolvedObject(event, constant.PodKind, podName) + // handleAndCheckComponentStatus(consensusMySQLCompName, event, + // appsv1alpha1.FailedClusterPhase, + // appsv1alpha1.FailedClusterCompPhase, + // false) + // By("test merge pod event message") + // event.Message = "0/1 nodes can scheduled, cpu insufficient" + // handleAndCheckComponentStatus(consensusMySQLCompName, event, + // appsv1alpha1.FailedClusterPhase, + // appsv1alpha1.FailedClusterCompPhase, + // false) + + // By("test Failed phase for consensus component when leader pod is not ready") + // setInvolvedObject(event, constant.StatefulSetKind, stsName) + // podName1 := stsName + "-1" + // pod := createStsPod(podName1, "leader", consensusMySQLCompName) + // handleAndCheckComponentStatus(consensusMySQLCompName, event, + // appsv1alpha1.FailedClusterPhase, + // appsv1alpha1.FailedClusterCompPhase, + // false) + + // By("test Abnormal phase for consensus component") + //// mock leader pod ready and sts.status.availableReplicas is 1 + // Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { + // testk8s.MockPodAvailable(pod, metav1.NewTime(time.Now())) + // })).ShouldNot(HaveOccurred()) + // Expect(testapps.GetAndChangeObjStatus(&testCtx, types.NamespacedName{Name: stsName, + // Namespace: testCtx.DefaultNamespace}, func(tmpSts *appsv1.StatefulSet) { + // testk8s.MockStatefulSetReady(tmpSts) + // tmpSts.Status.AvailableReplicas = *tmpSts.Spec.Replicas - 1 + // })()).ShouldNot(HaveOccurred()) + // handleAndCheckComponentStatus(consensusMySQLCompName, event, + // appsv1alpha1.AbnormalClusterPhase, + // appsv1alpha1.AbnormalClusterCompPhase, + // false) + + // By("watch warning event from Deployment and component workload type is Stateless") + // deploy := getDeployment(statelessCompName) + // setInvolvedObject(event, constant.DeploymentKind, deploy.Name) + // handleAndCheckComponentStatus(statelessCompName, event, + // appsv1alpha1.FailedClusterPhase, + // appsv1alpha1.FailedClusterCompPhase, + // false) + // mock cluster is running. + // Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { + // tmpCluster.Status.Phase = appsv1alpha1.RunningClusterPhase + // for name, compStatus := range tmpCluster.Status.Components { + // compStatus.Phase = appsv1alpha1.RunningClusterCompPhase + // tmpCluster.Status.SetComponentStatus(name, compStatus) + // } + // })()).ShouldNot(HaveOccurred()) + + // By("test the cluster phase when stateless component is Failed and other components are Running") + //// set nginx component phase to Failed + // Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { + // compStatus := tmpCluster.Status.Components[statelessCompName] + // compStatus.Phase = appsv1alpha1.FailedClusterCompPhase + // tmpCluster.Status.SetComponentStatus(statelessCompName, compStatus) + // })()).ShouldNot(HaveOccurred()) + + // expect cluster phase is Abnormal by cluster controller. + // Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), + // func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + // g.Expect(tmpCluster.Status.Phase).Should(Equal(appsv1alpha1.AbnormalClusterPhase)) + // })).Should(Succeed()) + }) + + It("test the consistency of status.components and spec.components", func() { + By("create cluster related resources") + createClusterDef() + createClusterVersion() + cluster := createCluster() + // REVIEW: follow expects is rather inaccurate + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + g.Expect(tmpCluster.Status.ObservedGeneration).Should(Equal(tmpCluster.Generation)) + // g.Expect(tmpCluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) + g.Expect(tmpCluster.Status.Components).Should(HaveLen(len(tmpCluster.Spec.ComponentSpecs))) + })).Should(Succeed()) + + changeAndCheckComponents := func(changeFunc func(cluster2 *appsv1alpha1.Cluster), expectObservedGeneration int64, checkFun func(Gomega, *appsv1alpha1.Cluster)) { + Expect(testapps.ChangeObj(&testCtx, cluster, func(lcluster *appsv1alpha1.Cluster) { + changeFunc(lcluster) + })).ShouldNot(HaveOccurred()) + // wait for cluster controller reconciles to complete. + Eventually(testapps.GetClusterObservedGeneration(&testCtx, client.ObjectKeyFromObject(cluster))).Should(Equal(expectObservedGeneration)) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), checkFun)).Should(Succeed()) + } + + By("delete consensus component") + consensusClusterComponent := cluster.Spec.ComponentSpecs[2] + changeAndCheckComponents( + func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs = lcluster.Spec.ComponentSpecs[:2] + }, 2, + func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + g.Expect(tmpCluster.Status.Components).Should(HaveLen(2)) + }) + + // TODO check when delete and add the same component, wait for deleting related workloads when delete component in lifecycle. + By("add consensus component") + consensusClusterComponent.Name = "consensus1" + changeAndCheckComponents( + func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs = append(lcluster.Spec.ComponentSpecs, consensusClusterComponent) + }, 3, + func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + _, isExist := tmpCluster.Status.Components[consensusClusterComponent.Name] + g.Expect(tmpCluster.Status.Components).Should(HaveLen(3)) + g.Expect(isExist).Should(BeTrue()) + }) + + By("modify consensus component name") + modifyConsensusName := "consensus2" + changeAndCheckComponents( + func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs[2].Name = modifyConsensusName + }, 4, + func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + _, isExist := tmpCluster.Status.Components[modifyConsensusName] + g.Expect(isExist).Should(BeTrue()) + }) + }) + }) +}) diff --git a/controllers/apps/cluster_status_utils.go b/controllers/apps/cluster_status_utils.go deleted file mode 100644 index 7d16d3a3f..000000000 --- a/controllers/apps/cluster_status_utils.go +++ /dev/null @@ -1,413 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 apps - -import ( - "context" - "fmt" - "regexp" - "strings" - "time" - - "golang.org/x/exp/slices" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" - "github.com/apecloud/kubeblocks/controllers/k8score" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// EventTimeOut timeout of the event -const EventTimeOut = 30 * time.Second - -// isTargetKindForEvent checks the event involve object is the target resources -func isTargetKindForEvent(event *corev1.Event) bool { - return slices.Index([]string{constant.PodKind, constant.DeploymentKind, constant.StatefulSetKind}, event.InvolvedObject.Kind) != -1 -} - -// getFinalEventMessageForRecorder gets final event message by event involved object kind for recorded it -func getFinalEventMessageForRecorder(event *corev1.Event) string { - if event.InvolvedObject.Kind == constant.PodKind { - return fmt.Sprintf("Pod %s: %s", event.InvolvedObject.Name, event.Message) - } - return event.Message -} - -// isExistsEventMsg checks whether the event is exists -func isExistsEventMsg(compStatusMessage map[string]string, event *corev1.Event) bool { - if compStatusMessage == nil { - return false - } - messageKey := util.GetComponentStatusMessageKey(event.InvolvedObject.Kind, event.InvolvedObject.Name) - if message, ok := compStatusMessage[messageKey]; !ok { - return false - } else { - return strings.Contains(message, event.Message) - } - -} - -// updateComponentStatusMessage updates component status message map -func updateComponentStatusMessage(cluster *appsv1alpha1.Cluster, - compName string, - compStatus *appsv1alpha1.ClusterComponentStatus, - event *corev1.Event) { - var ( - kind = event.InvolvedObject.Kind - name = event.InvolvedObject.Name - ) - message := compStatus.GetObjectMessage(kind, name) - // if the event message is not exists in message map, merge them. - if !strings.Contains(message, event.Message) { - message += event.Message + ";" - } - compStatus.SetObjectMessage(kind, name, message) - cluster.Status.SetComponentStatus(compName, *compStatus) -} - -// needSyncComponentStatusForEvent checks whether the component status needs to be synchronized the cluster status by event -func needSyncComponentStatusForEvent(cluster *appsv1alpha1.Cluster, componentName string, phase appsv1alpha1.ClusterComponentPhase, event *corev1.Event) bool { - if phase == "" { - return false - } - compStatus, ok := cluster.Status.Components[componentName] - if !ok { - compStatus = appsv1alpha1.ClusterComponentStatus{Phase: phase} - updateComponentStatusMessage(cluster, componentName, &compStatus, event) - return true - } - if compStatus.Phase != phase { - compStatus.Phase = phase - updateComponentStatusMessage(cluster, componentName, &compStatus, event) - return true - } - // check whether it is a new warning event and the component phase is running - if !isExistsEventMsg(compStatus.Message, event) && phase != appsv1alpha1.RunningClusterCompPhase { - updateComponentStatusMessage(cluster, componentName, &compStatus, event) - return true - } - return false -} - -// getEventInvolvedObject gets event involved object for StatefulSet/Deployment/Pod workload -func getEventInvolvedObject(ctx context.Context, cli client.Client, event *corev1.Event) (client.Object, error) { - objectKey := client.ObjectKey{ - Name: event.InvolvedObject.Name, - Namespace: event.InvolvedObject.Namespace, - } - var err error - // If client.object interface object is used as a parameter, it will not return an error when the object is not found. - // so we should specify the object type to get the object. - switch event.InvolvedObject.Kind { - case constant.PodKind: - pod := &corev1.Pod{} - err = cli.Get(ctx, objectKey, pod) - return pod, err - case constant.StatefulSetKind: - sts := &appsv1.StatefulSet{} - err = cli.Get(ctx, objectKey, sts) - return sts, err - case constant.DeploymentKind: - deployment := &appsv1.Deployment{} - err = cli.Get(ctx, objectKey, deployment) - return deployment, err - } - return nil, err -} - -// handleClusterPhaseWhenCompsNotReady handles the Cluster.status.phase when some components are Abnormal or Failed. -// TODO: Clear definitions need to be added to determine whether components will affect cluster availability in ClusterDefinition. -func handleClusterPhaseWhenCompsNotReady(cluster *appsv1alpha1.Cluster, - componentMap map[string]string, - clusterAvailabilityEffectMap map[string]bool) { - var ( - clusterIsFailed bool - failedCompCount int - isVolumeExpanding bool - ) - opsRecords, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) - if len(opsRecords) != 0 && opsRecords[0].Type == appsv1alpha1.VolumeExpansionType { - isVolumeExpanding = true - } - for k, v := range cluster.Status.Components { - // determine whether other components are still doing operation, i.e., create/restart/scaling. - // waiting for operation to complete except for volumeExpansion operation. - // because this operation will not affect cluster availability. - if !slices.Contains(appsv1alpha1.GetComponentTerminalPhases(), v.Phase) && !isVolumeExpanding { - return - } - if v.Phase == appsv1alpha1.FailedClusterCompPhase { - failedCompCount += 1 - componentDefName := componentMap[k] - // if the component can affect cluster availability, set Cluster.status.phase to Failed - if clusterAvailabilityEffectMap[componentDefName] { - clusterIsFailed = true - break - } - } - } - // If all components fail or there are failed components that affect the availability of the cluster, set phase to Failed - if failedCompCount == len(cluster.Status.Components) || clusterIsFailed { - cluster.Status.Phase = appsv1alpha1.FailedClusterPhase - } else { - cluster.Status.Phase = appsv1alpha1.AbnormalClusterPhase - } -} - -// getClusterAvailabilityEffect whether the component will affect the cluster availability. -// if the component can affect and be Failed, the cluster will be Failed too. -func getClusterAvailabilityEffect(componentDef *appsv1alpha1.ClusterComponentDefinition) bool { - switch componentDef.WorkloadType { - case appsv1alpha1.Consensus: - return true - case appsv1alpha1.Replication: - return true - default: - return componentDef.MaxUnavailable != nil - } -} - -// getComponentRelatedInfo gets componentMap, clusterAvailabilityMap and component definition information -func getComponentRelatedInfo(cluster *appsv1alpha1.Cluster, clusterDef *appsv1alpha1.ClusterDefinition, - componentName string) (map[string]string, map[string]bool, *appsv1alpha1.ClusterComponentDefinition) { - var ( - compDefName string - componentMap = map[string]string{} - componentDef *appsv1alpha1.ClusterComponentDefinition - ) - for _, v := range cluster.Spec.ComponentSpecs { - if v.Name == componentName { - compDefName = v.ComponentDefRef - } - componentMap[v.Name] = v.ComponentDefRef - } - clusterAvailabilityEffectMap := map[string]bool{} - for i, v := range clusterDef.Spec.ComponentDefs { - clusterAvailabilityEffectMap[v.Name] = getClusterAvailabilityEffect(&v) - if v.Name == compDefName { - componentDef = &clusterDef.Spec.ComponentDefs[i] - } - } - return componentMap, clusterAvailabilityEffectMap, componentDef -} - -// handleClusterStatusByEvent handles the cluster status when warning event happened -func handleClusterStatusByEvent(ctx context.Context, cli client.Client, recorder record.EventRecorder, event *corev1.Event) error { - var ( - cluster = &appsv1alpha1.Cluster{} - clusterDef = &appsv1alpha1.ClusterDefinition{} - phase appsv1alpha1.ClusterComponentPhase - err error - ) - object, err := getEventInvolvedObject(ctx, cli, event) - if err != nil { - return err - } - if object == nil || !intctrlutil.WorkloadFilterPredicate(object) { - return nil - } - labels := object.GetLabels() - if err = cli.Get(ctx, client.ObjectKey{Name: labels[constant.AppInstanceLabelKey], Namespace: object.GetNamespace()}, cluster); err != nil { - return err - } - if err = cli.Get(ctx, client.ObjectKey{Name: cluster.Spec.ClusterDefRef}, clusterDef); err != nil { - return err - } - componentName := labels[constant.KBAppComponentLabelKey] - // get the component phase by component name and sync to Cluster.status.components - patch := client.MergeFrom(cluster.DeepCopy()) - componentMap, clusterAvailabilityEffectMap, componentDef := getComponentRelatedInfo(cluster, clusterDef, componentName) - clusterComponent := cluster.Spec.GetComponentByName(componentName) - if clusterComponent == nil { - return nil - } - // get the component status by event and check whether the component status needs to be synchronized to the cluster - component, err := components.NewComponentByType(cli, cluster, clusterComponent, *componentDef) - if err != nil { - return err - } - phase, err = component.GetPhaseWhenPodsNotReady(ctx, componentName) - if err != nil { - return err - } - if !needSyncComponentStatusForEvent(cluster, componentName, phase, event) { - return nil - } - // handle Cluster.status.phase when some components are not ready. - handleClusterPhaseWhenCompsNotReady(cluster, componentMap, clusterAvailabilityEffectMap) - if err = cli.Status().Patch(ctx, cluster, patch); err != nil { - return err - } - recorder.Eventf(cluster, corev1.EventTypeWarning, event.Reason, getFinalEventMessageForRecorder(event)) - return opsutil.MarkRunningOpsRequestAnnotation(ctx, cli, cluster) -} - -// TODO: Unified cluster event processing -// handleEventForClusterStatus handles event for cluster Warning and Failed phase -func handleEventForClusterStatus(ctx context.Context, cli client.Client, recorder record.EventRecorder, event *corev1.Event) error { - - type predicateProcessor struct { - pred func() bool - processor func() error - } - - nilReturnHandler := func() error { return nil } - - pps := []predicateProcessor{ - { - // handle cronjob complete or fail event - pred: func() bool { - return event.InvolvedObject.Kind == constant.CronJobKind && - event.Reason == "SawCompletedJob" - }, - processor: func() error { - return handleDeletePVCCronJobEvent(ctx, cli, recorder, event) - }, - }, - { - pred: func() bool { - return event.Type != corev1.EventTypeWarning || - !isTargetKindForEvent(event) - }, - processor: nilReturnHandler, - }, - { - pred: func() bool { - // the error repeated several times, so we can sure it's a real error to the cluster. - return !k8score.IsOvertimeEvent(event, EventTimeOut) - }, - processor: nilReturnHandler, - }, - { - // handle cluster workload error events such as pod/statefulset/deployment errors - // must be the last one - pred: func() bool { - return true - }, - processor: func() error { - return handleClusterStatusByEvent(ctx, cli, recorder, event) - }, - }, - } - - for _, pp := range pps { - if pp.pred() { - return pp.processor() - } - } - return nil -} - -func handleDeletePVCCronJobEvent(ctx context.Context, - cli client.Client, - recorder record.EventRecorder, - event *corev1.Event) error { - re := regexp.MustCompile("status: Failed") - var ( - err error - object client.Object - ) - matches := re.FindStringSubmatch(event.Message) - if len(matches) == 0 { - // delete pvc success, then delete cronjob - return checkedDeleteDeletePVCCronJob(ctx, cli, event.InvolvedObject.Name, event.InvolvedObject.Namespace) - } - // cronjob failed - if object, err = getEventInvolvedObject(ctx, cli, event); err != nil { - return err - } - if object == nil { - return nil - } - labels := object.GetLabels() - cluster := appsv1alpha1.Cluster{} - if err = cli.Get(ctx, client.ObjectKey{Name: labels[constant.AppInstanceLabelKey], - Namespace: object.GetNamespace()}, &cluster); err != nil { - return err - } - componentName := labels[constant.KBAppComponentLabelKey] - // update component phase to abnormal - if err = updateComponentStatusPhase(cli, - ctx, - &cluster, - componentName, - appsv1alpha1.AbnormalClusterCompPhase, - event.Message, - object); err != nil { - return err - } - recorder.Eventf(&cluster, corev1.EventTypeWarning, event.Reason, event.Message) - return nil -} - -func checkedDeleteDeletePVCCronJob(ctx context.Context, cli client.Client, name string, namespace string) error { - // label check - cronJob := batchv1.CronJob{} - if err := cli.Get(ctx, types.NamespacedName{ - Namespace: namespace, - Name: name, - }, &cronJob); err != nil { - return client.IgnoreNotFound(err) - } - if cronJob.ObjectMeta.Labels[constant.AppManagedByLabelKey] != constant.AppName { - return nil - } - // check the delete-pvc-cronjob annotation. - // the reason for this is that the backup policy also creates cronjobs, - // which need to be distinguished by the annotation. - if cronJob.ObjectMeta.Annotations[lifecycleAnnotationKey] != lifecycleDeletePVCAnnotation { - return nil - } - // if managed by kubeblocks, then it must be the cronjob used to delete pvc, delete it since it's completed - if err := cli.Delete(ctx, &cronJob); err != nil { - return client.IgnoreNotFound(err) - } - return nil -} - -func updateComponentStatusPhase(cli client.Client, - ctx context.Context, - cluster *appsv1alpha1.Cluster, - componentName string, - phase appsv1alpha1.ClusterComponentPhase, - message string, - object client.Object) error { - c, ok := cluster.Status.Components[componentName] - if ok && c.Phase == phase { - return nil - } - c.SetObjectMessage(object.GetObjectKind().GroupVersionKind().Kind, object.GetName(), message) - patch := client.MergeFrom(cluster.DeepCopy()) - cluster.Status.SetComponentStatus(componentName, c) - return cli.Status().Patch(ctx, cluster, patch) -} - -// existsOperations checks if the cluster is doing operations -func existsOperations(cluster *appsv1alpha1.Cluster) bool { - opsRequestMap, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) - _, isRestoring := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] - return len(opsRequestMap) > 0 || isRestoring -} diff --git a/controllers/apps/cluster_status_utils_test.go b/controllers/apps/cluster_status_utils_test.go deleted file mode 100644 index 374c24ae7..000000000 --- a/controllers/apps/cluster_status_utils_test.go +++ /dev/null @@ -1,350 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 apps - -import ( - "context" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" - testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" -) - -var _ = Describe("test cluster Failed/Abnormal phase", func() { - - var ( - ctx = context.Background() - clusterName = "cluster-for-status-" + testCtx.GetRandomStr() - clusterDefName = "clusterdef-for-status-" + testCtx.GetRandomStr() - clusterVersionName = "cluster-version-for-status-" + testCtx.GetRandomStr() - ) - - cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - - testapps.ClearClusterResources(&testCtx) - - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) - // testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml) - } - BeforeEach(cleanEnv) - - AfterEach(cleanEnv) - - const statefulMySQLCompType = "stateful" - const statefulMySQLCompName = "mysql1" - - const consensusMySQLCompType = "consensus" - const consensusMySQLCompName = "mysql2" - - const nginxCompType = "stateless" - const nginxCompName = "nginx" - - createClusterDef := func() { - _ = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulMySQLCompType). - AddComponent(testapps.ConsensusMySQLComponent, consensusMySQLCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). - Create(&testCtx) - } - - createClusterVersion := func() { - _ = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(statefulMySQLCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). - AddComponent(consensusMySQLCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). - AddComponent(nginxCompType).AddContainerShort(testapps.DefaultNginxContainerName, testapps.NginxImage). - Create(&testCtx) - } - - createCluster := func() *appsv1alpha1.Cluster { - return testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(statefulMySQLCompName, statefulMySQLCompType).SetReplicas(3). - AddComponent(consensusMySQLCompName, consensusMySQLCompType).SetReplicas(3). - AddComponent(nginxCompName, nginxCompType).SetReplicas(3). - Create(&testCtx).GetObject() - } - - createStsPod := func(podName, podRole, componentName string) *corev1.Pod { - return testapps.NewPodFactory(testCtx.DefaultNamespace, podName). - AddAppInstanceLabel(clusterName). - AddAppComponentLabel(componentName). - AddRoleLabel(podRole). - AddAppManangedByLabel(). - AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). - Create(&testCtx).GetObject() - } - - getDeployment := func(componentName string) *appsv1.Deployment { - deployList := &appsv1.DeploymentList{} - Eventually(func(g Gomega) { - g.Expect(k8sClient.List(ctx, deployList, - client.MatchingLabels{ - constant.KBAppComponentLabelKey: componentName, - constant.AppInstanceLabelKey: clusterName}, - client.Limit(1))).ShouldNot(HaveOccurred()) - g.Expect(deployList.Items).Should(HaveLen(1)) - }).Should(Succeed()) - return &deployList.Items[0] - } - - handleAndCheckComponentStatus := func(componentName string, event *corev1.Event, - expectClusterPhase appsv1alpha1.ClusterPhase, - expectCompPhase appsv1alpha1.ClusterComponentPhase, - checkClusterPhase bool) { - Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace}, - func(g Gomega, newCluster *appsv1alpha1.Cluster) { - g.Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) - if checkClusterPhase { - g.Expect(newCluster.Status.Phase == expectClusterPhase).Should(BeTrue()) - return - } - compStatus := newCluster.Status.Components[componentName] - g.Expect(compStatus.Phase == expectCompPhase).Should(BeTrue()) - })).Should(Succeed()) - } - - setInvolvedObject := func(event *corev1.Event, kind, objectName string) { - event.InvolvedObject.Kind = kind - event.InvolvedObject.Name = objectName - } - - testHandleClusterPhaseWhenCompsNotReady := func(clusterObj *appsv1alpha1.Cluster, - compPhase appsv1alpha1.ClusterComponentPhase, - expectClusterPhase appsv1alpha1.ClusterPhase, - ) { - // mock Stateful component is Abnormal - clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ - statefulMySQLCompName: { - Phase: compPhase, - }, - } - handleClusterPhaseWhenCompsNotReady(clusterObj, nil, nil) - Expect(clusterObj.Status.Phase).Should(Equal(expectClusterPhase)) - } - - Context("test cluster Failed/Abnormal phase", func() { - It("test cluster Failed/Abnormal phase", func() { - By("create cluster related resources") - createClusterDef() - createClusterVersion() - cluster := createCluster() - - // wait for cluster's status to become stable so that it won't interfere with later tests - Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace}, - func(g Gomega, fetched *appsv1alpha1.Cluster) { - g.Expect(fetched.Generation).To(BeEquivalentTo(1)) - g.Expect(fetched.Status.ObservedGeneration).To(BeEquivalentTo(1)) - g.Expect(fetched.Status.Phase).To(Equal(appsv1alpha1.CreatingClusterPhase)) - })).Should(Succeed()) - - By("watch normal event") - event := &corev1.Event{ - Count: 1, - Type: corev1.EventTypeNormal, - Message: "create pod failed because the pvc is deleting", - } - Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) - - By("watch warning event from StatefulSet, but mismatch condition ") - // wait for StatefulSet created by cluster controller - stsName := clusterName + "-" + statefulMySQLCompName - Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: stsName, Namespace: testCtx.DefaultNamespace}, - func(g Gomega, fetched *appsv1.StatefulSet) { - g.Expect(fetched.Generation).To(BeEquivalentTo(1)) - })).Should(Succeed()) - stsInvolvedObject := corev1.ObjectReference{ - Name: stsName, - Kind: constant.StatefulSetKind, - Namespace: testCtx.DefaultNamespace, - } - event.InvolvedObject = stsInvolvedObject - event.Type = corev1.EventTypeWarning - Expect(handleEventForClusterStatus(ctx, k8sClient, clusterRecorder, event)).Should(Succeed()) - - By("watch warning event from StatefulSet and component workload type is Stateful") - event.Count = 3 - event.FirstTimestamp = metav1.Time{Time: time.Now()} - event.LastTimestamp = metav1.Time{Time: time.Now().Add(31 * time.Second)} - handleAndCheckComponentStatus(statefulMySQLCompName, event, - appsv1alpha1.FailedClusterPhase, - appsv1alpha1.FailedClusterCompPhase, - false) - - By("watch warning event from Pod and component workload type is Consensus") - // wait for StatefulSet created by cluster controller - stsName = clusterName + "-" + consensusMySQLCompName - Eventually(testapps.CheckObj(&testCtx, client.ObjectKey{Name: stsName, Namespace: testCtx.DefaultNamespace}, - func(g Gomega, fetched *appsv1.StatefulSet) { - g.Expect(fetched.Generation).To(BeEquivalentTo(1)) - })).Should(Succeed()) - // create a failed pod - podName := stsName + "-0" - createStsPod(podName, "", consensusMySQLCompName) - setInvolvedObject(event, constant.PodKind, podName) - handleAndCheckComponentStatus(consensusMySQLCompName, event, - appsv1alpha1.FailedClusterPhase, - appsv1alpha1.FailedClusterCompPhase, - false) - By("test merge pod event message") - event.Message = "0/1 nodes can scheduled, cpu insufficient" - handleAndCheckComponentStatus(consensusMySQLCompName, event, - appsv1alpha1.FailedClusterPhase, - appsv1alpha1.FailedClusterCompPhase, - false) - - By("test Failed phase for consensus component when leader pod is not ready") - setInvolvedObject(event, constant.StatefulSetKind, stsName) - podName1 := stsName + "-1" - pod := createStsPod(podName1, "leader", consensusMySQLCompName) - handleAndCheckComponentStatus(consensusMySQLCompName, event, - appsv1alpha1.FailedClusterPhase, - appsv1alpha1.FailedClusterCompPhase, - false) - - By("test Abnormal phase for consensus component") - // mock leader pod ready and sts.status.availableReplicas is 1 - Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { - testk8s.MockPodAvailable(pod, metav1.NewTime(time.Now())) - })).ShouldNot(HaveOccurred()) - Expect(testapps.GetAndChangeObjStatus(&testCtx, types.NamespacedName{Name: stsName, - Namespace: testCtx.DefaultNamespace}, func(tmpSts *appsv1.StatefulSet) { - testk8s.MockStatefulSetReady(tmpSts) - tmpSts.Status.AvailableReplicas = *tmpSts.Spec.Replicas - 1 - })()).ShouldNot(HaveOccurred()) - handleAndCheckComponentStatus(consensusMySQLCompName, event, - appsv1alpha1.AbnormalClusterPhase, - appsv1alpha1.AbnormalClusterCompPhase, - false) - - By("watch warning event from Deployment and component workload type is Stateless") - deploy := getDeployment(nginxCompName) - setInvolvedObject(event, constant.DeploymentKind, deploy.Name) - handleAndCheckComponentStatus(nginxCompName, event, - appsv1alpha1.FailedClusterPhase, - appsv1alpha1.FailedClusterCompPhase, - false) - // mock cluster is running. - Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { - tmpCluster.Status.Phase = appsv1alpha1.RunningClusterPhase - for name, compStatus := range tmpCluster.Status.Components { - compStatus.Phase = appsv1alpha1.RunningClusterCompPhase - tmpCluster.Status.SetComponentStatus(name, compStatus) - } - })()).ShouldNot(HaveOccurred()) - - By("test the cluster phase when stateless component is Failed and other components are Running") - // set nginx component phase to Failed - Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { - compStatus := tmpCluster.Status.Components[nginxCompName] - compStatus.Phase = appsv1alpha1.FailedClusterCompPhase - tmpCluster.Status.SetComponentStatus(nginxCompName, compStatus) - })()).ShouldNot(HaveOccurred()) - - // expect cluster phase is Abnormal by cluster controller. - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), - func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - g.Expect(tmpCluster.Status.Phase).Should(Equal(appsv1alpha1.AbnormalClusterPhase)) - })).Should(Succeed()) - - By("test the cluster phase when cluster only contains a component of Stateful workload, and the component is Failed or Abnormal") - clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(statefulMySQLCompName, statefulMySQLCompType).SetReplicas(3).GetObject() - // mock Stateful component is Failed and expect cluster phase is FailedPhase - testHandleClusterPhaseWhenCompsNotReady(clusterObj, appsv1alpha1.FailedClusterCompPhase, appsv1alpha1.FailedClusterPhase) - - // mock Stateful component is Abnormal and expect cluster phase is Abnormal - testHandleClusterPhaseWhenCompsNotReady(clusterObj, appsv1alpha1.AbnormalClusterCompPhase, appsv1alpha1.AbnormalClusterPhase) - }) - - It("test the consistency of status.components and spec.components", func() { - By("create cluster related resources") - createClusterDef() - createClusterVersion() - cluster := createCluster() - // REVIEW: follow expects is rather inaccurate - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - g.Expect(tmpCluster.Status.ObservedGeneration).Should(Equal(tmpCluster.Generation)) - // g.Expect(tmpCluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) - g.Expect(tmpCluster.Status.Components).Should(HaveLen(len(tmpCluster.Spec.ComponentSpecs))) - })).Should(Succeed()) - - changeAndCheckComponents := func(changeFunc func(), expectObservedGeneration int64, checkFun func(Gomega, *appsv1alpha1.Cluster)) { - Expect(testapps.ChangeObj(&testCtx, cluster, func() { - changeFunc() - })).ShouldNot(HaveOccurred()) - // wait for cluster controller reconciles to complete. - Eventually(testapps.GetClusterObservedGeneration(&testCtx, client.ObjectKeyFromObject(cluster))).Should(Equal(expectObservedGeneration)) - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), checkFun)).Should(Succeed()) - } - - By("delete consensus component") - consensusClusterComponent := cluster.Spec.ComponentSpecs[2] - changeAndCheckComponents( - func() { - cluster.Spec.ComponentSpecs = cluster.Spec.ComponentSpecs[:2] - }, 2, - func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - g.Expect(tmpCluster.Status.Components).Should(HaveLen(2)) - }) - - // TODO check when delete and add the same component, wait for deleting related workloads when delete component in lifecycle. - By("add consensus component") - consensusClusterComponent.Name = "consensus1" - changeAndCheckComponents( - func() { - cluster.Spec.ComponentSpecs = append(cluster.Spec.ComponentSpecs, consensusClusterComponent) - }, 3, - func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - _, isExist := tmpCluster.Status.Components[consensusClusterComponent.Name] - g.Expect(tmpCluster.Status.Components).Should(HaveLen(3)) - g.Expect(isExist).Should(BeTrue()) - }) - - By("modify consensus component name") - modifyConsensusName := "consensus2" - changeAndCheckComponents( - func() { - cluster.Spec.ComponentSpecs[2].Name = modifyConsensusName - }, 4, - func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - _, isExist := tmpCluster.Status.Components[modifyConsensusName] - g.Expect(isExist).Should(BeTrue()) - }) - - }) - }) - -}) diff --git a/controllers/apps/clusterdefinition_controller.go b/controllers/apps/clusterdefinition_controller.go index c6775d30c..17a206a6d 100644 --- a/controllers/apps/clusterdefinition_controller.go +++ b/controllers/apps/clusterdefinition_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/controllers/apps/clusterdefinition_controller_test.go b/controllers/apps/clusterdefinition_controller_test.go index 8def6cde6..c3637838b 100644 --- a/controllers/apps/clusterdefinition_controller_test.go +++ b/controllers/apps/clusterdefinition_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -36,14 +39,14 @@ var _ = Describe("ClusterDefinition Controller", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" - const statefulCompType = "replicasets" + const statefulCompDefName = "replicasets" const configVolumeName = "mysql-config" const cmName = "mysql-tree-node-template-8.0" cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -80,12 +83,12 @@ var _ = Describe("ClusterDefinition Controller", func() { BeforeEach(func() { By("Create a clusterDefinition obj") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(statefulCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) @@ -133,7 +136,7 @@ var _ = Describe("ClusterDefinition Controller", func() { cfgTpl := testapps.CreateCustomizedObj(&testCtx, "config/config-constraint.yaml", &appsv1alpha1.ConfigConstraint{}) Expect(testapps.ChangeObjStatus(&testCtx, cfgTpl, func() { - cfgTpl.Status.Phase = appsv1alpha1.AvailablePhase + cfgTpl.Status.Phase = appsv1alpha1.CCAvailablePhase })).Should(Succeed()) return cm } @@ -142,13 +145,13 @@ var _ = Describe("ClusterDefinition Controller", func() { BeforeEach(func() { By("Create a clusterDefinition obj") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). AddConfigTemplate(cmName, cmName, cmName, testCtx.DefaultNamespace, configVolumeName). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(statefulCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) diff --git a/controllers/apps/clusterversion_controller.go b/controllers/apps/clusterversion_controller.go index b261f3581..eff70b96c 100644 --- a/controllers/apps/clusterversion_controller.go +++ b/controllers/apps/clusterversion_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/controllers/apps/clusterversion_controller_test.go b/controllers/apps/clusterversion_controller_test.go index 35fe7538d..7cc838afa 100644 --- a/controllers/apps/clusterversion_controller_test.go +++ b/controllers/apps/clusterversion_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -34,10 +37,10 @@ var _ = Describe("test clusterVersion controller", func() { clusterDefName = "mysql-definition-" + randomStr ) - const statefulCompType = "stateful" + const statefulCompDefName = "stateful" cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -54,7 +57,7 @@ var _ = Describe("test clusterVersion controller", func() { It("test clusterVersion controller", func() { By("create a clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(statefulCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(statefulCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() By("wait for clusterVersion phase is unavailable when clusterDef is not found") @@ -65,7 +68,7 @@ var _ = Describe("test clusterVersion controller", func() { By("create a clusterDefinition obj") testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). Create(&testCtx).GetObject() By("wait for clusterVersion phase is available") diff --git a/controllers/apps/components/component.go b/controllers/apps/components/component.go index 213c9e67a..1c7fa052e 100644 --- a/controllers/apps/components/component.go +++ b/controllers/apps/components/component.go @@ -1,183 +1,102 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package components import ( - "context" - "time" + "fmt" - "golang.org/x/exp/slices" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" + corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/consensusset" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" + "github.com/apecloud/kubeblocks/controllers/apps/components/consensus" + "github.com/apecloud/kubeblocks/controllers/apps/components/replication" "github.com/apecloud/kubeblocks/controllers/apps/components/stateful" "github.com/apecloud/kubeblocks/controllers/apps/components/stateless" "github.com/apecloud/kubeblocks/controllers/apps/components/types" "github.com/apecloud/kubeblocks/controllers/apps/components/util" - opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" - "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// componentContext wrapper for handling component status procedure context parameters. -type componentContext struct { - reqCtx intctrlutil.RequestCtx - cli client.Client - recorder record.EventRecorder - component types.Component - obj client.Object - componentSpec *appsv1alpha1.ClusterComponentSpec +// PodIsAvailable checks whether a pod is available with respect to the workload type. +// Deprecated: provide for ops request using, remove this interface later. +func PodIsAvailable(workloadType appsv1alpha1.WorkloadType, pod *corev1.Pod, minReadySeconds int32) bool { + return util.PodIsAvailable(workloadType, pod, minReadySeconds) } -// NewComponentByType creates a component object. -func NewComponentByType( +func NewComponent(reqCtx intctrlutil.RequestCtx, cli client.Client, + definition *appsv1alpha1.ClusterDefinition, + version *appsv1alpha1.ClusterVersion, cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec, - componentDef appsv1alpha1.ClusterComponentDefinition, -) (types.Component, error) { - if err := util.ComponentRuntimeReqArgsCheck(cli, cluster, component); err != nil { - return nil, err - } - switch componentDef.WorkloadType { - case appsv1alpha1.Consensus: - return consensusset.NewConsensusSet(cli, cluster, component, componentDef) - case appsv1alpha1.Replication: - return replicationset.NewReplicationSet(cli, cluster, component, componentDef) - case appsv1alpha1.Stateful: - return stateful.NewStateful(cli, cluster, component, componentDef) - case appsv1alpha1.Stateless: - return stateless.NewStateless(cli, cluster, component, componentDef) - default: - panic("unknown workload type") - } -} - -// newComponentContext creates a componentContext object. -func newComponentContext( - reqCtx intctrlutil.RequestCtx, - cli client.Client, - recorder record.EventRecorder, - component types.Component, - obj client.Object, - componentSpec *appsv1alpha1.ClusterComponentSpec) componentContext { - - return componentContext{ - reqCtx: reqCtx, - cli: cli, - recorder: recorder, - component: component, - obj: obj, - componentSpec: componentSpec, + compName string, + dag *graph.DAG) (types.Component, error) { + var compDef *appsv1alpha1.ClusterComponentDefinition + var compVer *appsv1alpha1.ClusterComponentVersion + compSpec := cluster.Spec.GetComponentByName(compName) + if compSpec != nil { + compDef = definition.GetComponentDefByName(compSpec.ComponentDefRef) + if compDef == nil { + return nil, fmt.Errorf("referenced component definition does not exist, cluster: %s, component: %s, component definition ref:%s", + cluster.Name, compSpec.Name, compSpec.ComponentDefRef) + } + if version != nil { + compVer = version.Spec.GetDefNameMappingComponents()[compSpec.ComponentDefRef] + } } -} -// updateComponentStatusInClusterStatus updates cluster.Status.Components if the component status changed -func updateComponentStatusInClusterStatus( - compCtx componentContext, - cluster *appsv1alpha1.Cluster) (time.Duration, error) { - componentStatusSynchronizer, err := newClusterStatusSynchronizer(compCtx.reqCtx.Ctx, compCtx.cli, cluster, - compCtx.componentSpec, compCtx.component) - if err != nil { - return 0, err - } - if componentStatusSynchronizer == nil { - return 0, nil + if compSpec == nil || compDef == nil { + return nil, nil } - wait, err := componentStatusSynchronizer.Update(compCtx.reqCtx.Ctx, compCtx.obj, &compCtx.reqCtx.Log, - compCtx.recorder) + synthesizedComp, err := composeSynthesizedComponent(reqCtx, cli, cluster, definition, compDef, compSpec, compVer) if err != nil { - return 0, err - } - - var requeueAfter time.Duration - if wait { - requeueAfter = time.Minute - } - return requeueAfter, opsutil.MarkRunningOpsRequestAnnotation(compCtx.reqCtx.Ctx, compCtx.cli, cluster) -} - -func workloadCompClusterReconcile( - reqCtx intctrlutil.RequestCtx, - cli client.Client, - operand client.Object, - processor func(*appsv1alpha1.Cluster, *appsv1alpha1.ClusterComponentSpec, types.Component) (ctrl.Result, error), -) (ctrl.Result, error) { - var err error - var cluster *appsv1alpha1.Cluster - - if cluster, err = util.GetClusterByObject(reqCtx.Ctx, cli, operand); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } else if cluster == nil { - return intctrlutil.Reconciled() - } - - clusterDef := &appsv1alpha1.ClusterDefinition{} - if err = cli.Get(reqCtx.Ctx, client.ObjectKey{Name: cluster.Spec.ClusterDefRef}, clusterDef); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + return nil, err } - // create a component object - componentName := operand.GetLabels()[constant.KBAppComponentLabelKey] - componentSpec := cluster.Spec.GetComponentByName(componentName) - if componentSpec == nil { - return intctrlutil.Reconciled() - } - componentDef := clusterDef.GetComponentDefByName(componentSpec.ComponentDefRef) - component, err := NewComponentByType(cli, cluster, componentSpec, *componentDef) - if err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + switch compDef.WorkloadType { + case appsv1alpha1.Replication: + return replication.NewReplicationComponent(cli, reqCtx.Recorder, cluster, version, synthesizedComp, dag), nil + case appsv1alpha1.Consensus: + return consensus.NewConsensusComponent(cli, reqCtx.Recorder, cluster, version, synthesizedComp, dag), nil + case appsv1alpha1.Stateful: + return stateful.NewStatefulComponent(cli, reqCtx.Recorder, cluster, version, synthesizedComp, dag), nil + case appsv1alpha1.Stateless: + return stateless.NewStatelessComponent(cli, reqCtx.Recorder, cluster, version, synthesizedComp, dag), nil } - return processor(cluster, componentSpec, component) + panic(fmt.Sprintf("unknown workload type: %s, cluster: %s, component: %s, component definition ref: %s", + compDef.WorkloadType, cluster.Name, compSpec.Name, compSpec.ComponentDefRef)) } -// patchWorkloadCustomLabel patches workload custom labels. -func patchWorkloadCustomLabel( - ctx context.Context, +func composeSynthesizedComponent(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster *appsv1alpha1.Cluster, - componentSpec *appsv1alpha1.ClusterComponentSpec) error { - if cluster == nil || componentSpec == nil { - return nil - } - compDef, err := util.GetComponentDefByCluster(ctx, cli, *cluster, componentSpec.ComponentDefRef) + clusterDef *appsv1alpha1.ClusterDefinition, + compDef *appsv1alpha1.ClusterComponentDefinition, + compSpec *appsv1alpha1.ClusterComponentSpec, + compVer *appsv1alpha1.ClusterComponentVersion) (*component.SynthesizedComponent, error) { + synthesizedComp, err := component.BuildSynthesizedComponent(reqCtx, cli, *cluster, *clusterDef, *compDef, *compSpec, compVer) if err != nil { - return err - } - for _, customLabelSpec := range compDef.CustomLabelSpecs { - // TODO if the customLabelSpec.Resources is empty, we should add the label to the workload resources under the component. - for _, resource := range customLabelSpec.Resources { - gvk, err := util.ParseCustomLabelPattern(resource.GVK) - if err != nil { - return err - } - // only handle workload kind - if !slices.Contains(util.GetCustomLabelWorkloadKind(), gvk.Kind) { - continue - } - if err := util.PatchGVRCustomLabels(ctx, cli, cluster, resource, componentSpec.Name, customLabelSpec.Key, customLabelSpec.Value); err != nil { - return err - } - } + return nil, err } - return nil + return synthesizedComp, nil } diff --git a/controllers/apps/components/component_status.go b/controllers/apps/components/component_status.go deleted file mode 100644 index 8a90270e6..000000000 --- a/controllers/apps/components/component_status.go +++ /dev/null @@ -1,257 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "context" - "reflect" - "time" - - "github.com/go-logr/logr" - "golang.org/x/exp/slices" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// ComponentStatusSynchronizer gathers running status from Cluster, component's Workload and Pod objects, -// then fills status of component (e.g. abnormalities or failures) into the Cluster.Status.Components map. -// -// Although it works to use warning event to determine whether the component is abnormal or failed. -// In some conditions, the warning events are possible to be throttled and dropped due to K8s event rate control. -// For example, after the kubelet fails to pull the image, it will put the image into the backoff cache -// and send the BackOff (Normal) event. If it has already consumed the 25 burst quota to send event, event can only be -// sent in the rate of once per 300s, in this way, the subsequent warning events of ImagePullError would be dropped. -type ComponentStatusSynchronizer struct { - cli client.Client - cluster *appsv1alpha1.Cluster - component types.Component - componentSpec *appsv1alpha1.ClusterComponentSpec - podList *corev1.PodList -} - -// newClusterStatusSynchronizer creates and initializes a ComponentStatusSynchronizer objects. -// It represents a snapshot of cluster status, including workloads and pods. -func newClusterStatusSynchronizer(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster, - componentSpec *appsv1alpha1.ClusterComponentSpec, - component types.Component, -) (*ComponentStatusSynchronizer, error) { - podList, err := util.GetComponentPodList(ctx, cli, *cluster, componentSpec.Name) - if err != nil { - return nil, err - } - return &ComponentStatusSynchronizer{ - cli: cli, - cluster: cluster, - component: component, - componentSpec: componentSpec, - podList: podList, - }, nil -} - -func (cs *ComponentStatusSynchronizer) Update(ctx context.Context, obj client.Object, logger *logr.Logger, - recorder record.EventRecorder) (bool, error) { - var ( - component = cs.component - wait = false - ) - - if component == nil { - return false, nil - } - // handle the components changes - err := component.HandleUpdate(ctx, obj) - if err != nil { - return false, nil - } - - isRunning, err := component.IsRunning(ctx, obj) - if err != nil { - return false, err - } - - var podsReady *bool - if cs.componentSpec.Replicas > 0 { - podsReadyForComponent, err := component.PodsReady(ctx, obj) - if err != nil { - return false, err - } - podsReady = &podsReadyForComponent - } - - cluster := cs.cluster - hasFailedAndTimedOutPod := false - clusterDeepCopy := cluster.DeepCopy() - if !isRunning { - if podsReady != nil && *podsReady { - // check if the role probe timed out when component phase is not Running but all pods of component are ready. - if requeueWhenPodsReady, err := component.HandleProbeTimeoutWhenPodsReady(ctx, recorder); err != nil { - return false, err - } else if requeueWhenPodsReady { - wait = true - } - } else { - // check whether there is a failed pod of component that has timed out - var hasFailedPod bool - hasFailedAndTimedOutPod, hasFailedPod = cs.hasFailedAndTimedOutPod() - if !hasFailedAndTimedOutPod && hasFailedPod { - wait = true - } - } - } - - if err = cs.updateComponentsPhase(ctx, isRunning, - podsReady, hasFailedAndTimedOutPod); err != nil { - return wait, err - } - - componentName := cs.componentSpec.Name - oldComponentStatus := clusterDeepCopy.Status.Components[componentName] - - // REVIEW: this function was refactored by Caowei, the original function create need to review this part - if componentStatus, ok := cluster.Status.Components[componentName]; ok && !reflect.DeepEqual(oldComponentStatus, componentStatus) { - logger.Info("component status changed", "componentName", componentName, "phase", - componentStatus.Phase, "componentIsRunning", isRunning, "podsAreReady", podsReady) - patch := client.MergeFrom(clusterDeepCopy) - if err = cs.cli.Status().Patch(ctx, cluster, patch); err != nil { - return false, err - } - } - return wait, nil -} - -// hasFailedAndTimedOutPod returns whether the pod of components is still failed after a PodFailedTimeout period. -// if return ture, component phase will be set to Failed/Abnormal. -func (cs *ComponentStatusSynchronizer) hasFailedAndTimedOutPod() (hasFailedAndTimedoutPod bool, hasFailedPod bool) { - // init a new ComponentMessageMap to store the message of failed pods - message := appsv1alpha1.ComponentMessageMap{} - for _, pod := range cs.podList.Items { - isFailed, isTimedOut, messageStr := isPodFailedAndTimedOut(&pod) - if !isFailed { - continue - } - hasFailedPod = true - - if isTimedOut { - hasFailedAndTimedoutPod = true - message.SetObjectMessage(pod.Kind, pod.Name, messageStr) - } - } - if hasFailedAndTimedoutPod { - cs.updateMessage(message) - } - return -} - -// updateComponentsPhase updates the component status Phase etc. into the cluster.Status.Components map. -func (cs *ComponentStatusSynchronizer) updateComponentsPhase( - ctx context.Context, - componentIsRunning bool, - podsAreReady *bool, - hasFailedPodTimedOut bool) error { - var ( - status = &cs.cluster.Status - podsReadyTime *metav1.Time - componentName = cs.componentSpec.Name - ) - if podsAreReady != nil && *podsAreReady { - podsReadyTime = &metav1.Time{Time: time.Now()} - } - componentStatus := cs.cluster.Status.Components[cs.componentSpec.Name] - if !componentIsRunning { - // if no operation is running in cluster or failed pod timed out, - // means the component is Failed or Abnormal. - if slices.Contains(appsv1alpha1.GetClusterUpRunningPhases(), cs.cluster.Status.Phase) || hasFailedPodTimedOut { - if phase, err := cs.component.GetPhaseWhenPodsNotReady(ctx, componentName); err != nil { - return err - } else if phase != "" { - componentStatus.Phase = phase - } - } - } else { - if cs.componentSpec.Replicas == 0 { - // if replicas number of component is zero, the component has stopped. - // 'Stopped' is a special 'Running' for workload(StatefulSet/Deployment). - componentStatus.Phase = appsv1alpha1.StoppedClusterCompPhase - } else { - // change component phase to Running when workloads of component are running. - componentStatus.Phase = appsv1alpha1.RunningClusterCompPhase - } - componentStatus.SetMessage(nil) - } - componentStatus.PodsReadyTime = podsReadyTime - componentStatus.PodsReady = podsAreReady - status.SetComponentStatus(componentName, componentStatus) - return nil -} - -// updateMessage is an internal helper method which updates the component status message in the Cluster.Status.Components map. -func (cs *ComponentStatusSynchronizer) updateMessage(message appsv1alpha1.ComponentMessageMap) { - compStatus := cs.cluster.Status.Components[cs.componentSpec.Name] - compStatus.Message = message - cs.setStatus(compStatus) -} - -// setStatus is an internal helper method which sets component status in Cluster.Status.Components map. -func (cs *ComponentStatusSynchronizer) setStatus(compStatus appsv1alpha1.ClusterComponentStatus) { - cs.cluster.Status.SetComponentStatus(cs.componentSpec.Name, compStatus) -} - -// isPodFailedAndTimedOut checks if the pod is failed and timed out. -func isPodFailedAndTimedOut(pod *corev1.Pod) (bool, bool, string) { - initContainerFailed, message := isAnyContainerFailed(pod.Status.InitContainerStatuses) - if initContainerFailed { - return initContainerFailed, isContainerFailedAndTimedOut(pod, corev1.PodInitialized), message - } - containerFailed, message := isAnyContainerFailed(pod.Status.ContainerStatuses) - if containerFailed { - return containerFailed, isContainerFailedAndTimedOut(pod, corev1.ContainersReady), message - } - return false, false, "" -} - -// isAnyContainerFailed checks whether any container in the list is failed. -func isAnyContainerFailed(containersStatus []corev1.ContainerStatus) (bool, string) { - for _, v := range containersStatus { - waitingState := v.State.Waiting - if waitingState != nil && waitingState.Message != "" { - return true, waitingState.Message - } - terminatedState := v.State.Terminated - if terminatedState != nil && terminatedState.Message != "" { - return true, terminatedState.Message - } - } - return false, "" -} - -// isContainerFailedAndTimedOut checks whether the failed container has timed out. -func isContainerFailedAndTimedOut(pod *corev1.Pod, podConditionType corev1.PodConditionType) bool { - containerReadyCondition := intctrlutil.GetPodCondition(&pod.Status, podConditionType) - if containerReadyCondition == nil || containerReadyCondition.LastTransitionTime.IsZero() { - return false - } - return time.Now().After(containerReadyCondition.LastTransitionTime.Add(types.PodContainerFailedTimeout)) -} diff --git a/controllers/apps/components/component_status_test.go b/controllers/apps/components/component_status_test.go index 38bfa79ef..1280ea68d 100644 --- a/controllers/apps/components/component_status_test.go +++ b/controllers/apps/components/component_status_test.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package components import ( "fmt" + "strconv" "time" . "github.com/onsi/ginkgo/v2" @@ -27,20 +31,23 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/controllers/apps/components/stateless" "github.com/apecloud/kubeblocks/controllers/apps/components/types" "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + "github.com/apecloud/kubeblocks/internal/controller/graph" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" ) var _ = Describe("ComponentStatusSynchronizer", func() { const ( - compName = "comp" - compType = "comp" + compName = "comp" + compDefName = "comp" ) var ( @@ -51,7 +58,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { ) cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -62,13 +69,13 @@ var _ = Describe("ComponentStatusSynchronizer", func() { ml := client.HasLabels{testCtx.TestObjLabelKey} // non-namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.ClusterDefinitionSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.ClusterDefinitionSignature, inNS, ml) // namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.ClusterSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) + testapps.ClearResources(&testCtx, generics.ClusterSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.StatefulSetSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.DeploymentSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) } BeforeEach(cleanAll) @@ -80,37 +87,33 @@ var _ = Describe("ComponentStatusSynchronizer", func() { clusterDef *appsv1alpha1.ClusterDefinition cluster *appsv1alpha1.Cluster component types.Component + reqCtx *intctrlutil.RequestCtx + dag *graph.DAG err error ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatelessNginxComponent, compType). + AddComponentDef(testapps.StatelessNginxComponent, compDefName). GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(compName, compType). + AddComponent(compName, compDefName). SetReplicas(1). GetObject() - component, err = NewComponentByType(testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), *clusterDef.GetComponentDefByName(compName)) + reqCtx = &intctrlutil.RequestCtx{ + Ctx: ctx, + Log: log.FromContext(ctx).WithValues("cluster", clusterDef.Name), + } + dag = graph.NewDAG() + component, err = NewComponent(*reqCtx, testCtx.Cli, clusterDef, nil, cluster, compName, dag) Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) }) It("should not change component if no deployment or pod exists", func() { - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - podsAreReady := false - Expect(synchronizer.updateComponentsPhase(ctx, false, &podsAreReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(BeEmpty()) }) @@ -123,6 +126,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { BeforeEach(func() { deploymentName := clusterName + "-" + compName deployment = testapps.NewDeploymentFactory(testCtx.DefaultNamespace, deploymentName, clusterName, compName). + AddAnnotations(constant.KubeBlocksGenerationKey, strconv.FormatInt(cluster.Generation, 10)). SetMinReadySeconds(int32(10)). SetReplicas(int32(1)). AddContainer(corev1.Container{Name: testapps.DefaultNginxContainerName, Image: testapps.NginxImage}). @@ -141,23 +145,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { It("should set component status to failed if container is not ready and have error message", func() { Expect(mockContainerError(pod)).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeTrue()) - Expect(hasFailedPod).Should(BeTrue()) - - isPodReady, err := component.PodsReady(ctx, deployment) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeFalse()) - isRunning, err := component.IsRunning(ctx, deployment) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeFalse()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) }) @@ -166,23 +154,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { testk8s.MockDeploymentReady(deployment, stateless.NewRSAvailableReason, deployment.Name) })).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - isPodReady, err := component.PodsReady(ctx, deployment) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeTrue()) - isRunning, err := component.IsRunning(ctx, deployment) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeTrue()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) }) }) @@ -193,37 +165,33 @@ var _ = Describe("ComponentStatusSynchronizer", func() { clusterDef *appsv1alpha1.ClusterDefinition cluster *appsv1alpha1.Cluster component types.Component + reqCtx *intctrlutil.RequestCtx + dag *graph.DAG err error ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, compType). + AddComponentDef(testapps.StatefulMySQLComponent, compDefName). GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(compName, compType). + AddComponent(compName, compDefName). SetReplicas(int32(3)). GetObject() - component, err = NewComponentByType(testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), *clusterDef.GetComponentDefByName(compName)) + reqCtx = &intctrlutil.RequestCtx{ + Ctx: ctx, + Log: log.FromContext(ctx).WithValues("cluster", clusterDef.Name), + } + dag = graph.NewDAG() + component, err = NewComponent(*reqCtx, testCtx.Cli, clusterDef, nil, cluster, compName, dag) Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) }) It("should not change component if no statefulset or pod exists", func() { - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - podsAreReady := false - Expect(synchronizer.updateComponentsPhase(ctx, false, &podsAreReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(BeEmpty()) }) @@ -236,6 +204,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { BeforeEach(func() { stsName := clusterName + "-" + compName statefulset = testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, stsName, clusterName, compName). + AddAnnotations(constant.KubeBlocksGenerationKey, strconv.FormatInt(cluster.Generation, 10)). SetReplicas(int32(3)). AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). Create(&testCtx).GetObject() @@ -265,25 +234,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { Expect(mockContainerError(pods[0])).Should(Succeed()) Expect(mockContainerError(pods[1])).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeTrue()) - // two pod failed message - Expect(len(cluster.Status.Components[compName].Message)).Should(Equal(2)) - Expect(hasFailedPod).Should(BeTrue()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeFalse()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeFalse()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) }) @@ -292,23 +243,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { testk8s.MockStatefulSetReady(statefulset) })).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeTrue()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeTrue()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) }) }) @@ -319,37 +254,33 @@ var _ = Describe("ComponentStatusSynchronizer", func() { clusterDef *appsv1alpha1.ClusterDefinition cluster *appsv1alpha1.Cluster component types.Component + reqCtx *intctrlutil.RequestCtx + dag *graph.DAG err error ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, compType). + AddComponentDef(testapps.ConsensusMySQLComponent, compDefName). Create(&testCtx).GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(compName, compType). + AddComponent(compName, compDefName). SetReplicas(int32(3)). Create(&testCtx).GetObject() - component, err = NewComponentByType(testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), *clusterDef.GetComponentDefByName(compName)) + reqCtx = &intctrlutil.RequestCtx{ + Ctx: ctx, + Log: log.FromContext(ctx).WithValues("cluster", clusterDef.Name), + } + dag = graph.NewDAG() + component, err = NewComponent(*reqCtx, testCtx.Cli, clusterDef, nil, cluster, compName, dag) Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) }) It("should not change component if no statefulset or pod exists", func() { - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - podsAreReady := false - Expect(synchronizer.updateComponentsPhase(ctx, false, &podsAreReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(BeEmpty()) }) @@ -362,6 +293,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { BeforeEach(func() { stsName := clusterName + "-" + compName statefulset = testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, stsName, clusterName, compName). + AddAnnotations(constant.KubeBlocksGenerationKey, strconv.FormatInt(cluster.Generation, 10)). SetReplicas(int32(3)). AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). Create(&testCtx).GetObject() @@ -389,23 +321,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { It("should set component status to failed if container is not ready and have error message", func() { Expect(mockContainerError(pods[0])).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeTrue()) - Expect(hasFailedPod).Should(BeTrue()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeFalse()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeFalse()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) }) @@ -418,23 +334,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { Expect(setPodRole(pods[1], "follower")).Should(Succeed()) Expect(setPodRole(pods[2], "follower")).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeTrue()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeTrue()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) }) }) @@ -445,41 +345,40 @@ var _ = Describe("ComponentStatusSynchronizer", func() { clusterDef *appsv1alpha1.ClusterDefinition cluster *appsv1alpha1.Cluster component types.Component + reqCtx *intctrlutil.RequestCtx + dag *graph.DAG err error ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, compType). + AddComponentDef(testapps.ReplicationRedisComponent, compDefName). GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(compName, compType). + AddComponent(compName, compDefName). SetReplicas(2). GetObject() - component, err = NewComponentByType(testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), *clusterDef.GetComponentDefByName(compName)) + reqCtx = &intctrlutil.RequestCtx{ + Ctx: ctx, + Log: log.FromContext(ctx).WithValues("cluster", clusterDef.Name), + } + dag = graph.NewDAG() + component, err = NewComponent(*reqCtx, testCtx.Cli, clusterDef, nil, cluster, compName, dag) Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) }) It("should not change component if no deployment or pod exists", func() { - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - podsAreReady := false - Expect(synchronizer.updateComponentsPhase(ctx, false, &podsAreReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(BeEmpty()) }) Context("and with mocked statefulset & pod", func() { + const ( + replicas = 2 + ) var ( statefulset *appsv1.StatefulSet pods []*corev1.Pod @@ -488,18 +387,23 @@ var _ = Describe("ComponentStatusSynchronizer", func() { BeforeEach(func() { stsName := clusterName + "-" + compName statefulset = testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, stsName, clusterName, compName). - SetReplicas(int32(2)). + AddAnnotations(constant.KubeBlocksGenerationKey, strconv.FormatInt(cluster.Generation, 10)). + SetReplicas(int32(replicas)). AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). Create(&testCtx).GetObject() testk8s.InitStatefulSetStatus(testCtx, statefulset, controllerRevision) - for i := 0; i < 2; i++ { - podName := fmt.Sprintf("%s-%s-%d", clusterName, compName, i) + for i := 0; i < replicas; i++ { + podName := fmt.Sprintf("%s-%d", stsName, i) + podRole := "primary" + if i > 0 { + podRole = "secondary" + } pod := testapps.NewPodFactory(testCtx.DefaultNamespace, podName). SetOwnerReferences("apps/v1", constant.StatefulSetKind, statefulset). AddAppInstanceLabel(clusterName). AddAppComponentLabel(compName). AddAppManangedByLabel(). - AddRoleLabel("leader"). + AddRoleLabel(podRole). AddControllerRevisionHashLabel(controllerRevision). AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). Create(&testCtx).GetObject() @@ -518,23 +422,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { It("should set component status to failed if container is not ready and have error message", func() { Expect(mockContainerError(pods[0])).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeTrue()) - Expect(hasFailedPod).Should(BeTrue()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeFalse()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeFalse()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) }) @@ -543,23 +431,7 @@ var _ = Describe("ComponentStatusSynchronizer", func() { testk8s.MockStatefulSetReady(statefulset) })).Should(Succeed()) - synchronizer, err := newClusterStatusSynchronizer(testCtx.Ctx, testCtx.Cli, cluster, - cluster.Spec.GetComponentByName(compName), component) - Expect(err).Should(Succeed()) - Expect(synchronizer).ShouldNot(BeNil()) - - hasFailedAndTimeoutPod, hasFailedPod := synchronizer.hasFailedAndTimedOutPod() - Expect(hasFailedAndTimeoutPod).Should(BeFalse()) - Expect(hasFailedPod).Should(BeFalse()) - - isPodReady, err := component.PodsReady(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isPodReady).Should(BeTrue()) - isRunning, err := component.IsRunning(ctx, statefulset) - Expect(err).ShouldNot(HaveOccurred()) - Expect(isRunning).Should(BeTrue()) - - Expect(synchronizer.updateComponentsPhase(ctx, isRunning, &isPodReady, hasFailedAndTimeoutPod)).Should(Succeed()) + Expect(component.Status(*reqCtx, testCtx.Cli)).Should(Succeed()) Expect(cluster.Status.Components[compName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) }) }) @@ -589,7 +461,7 @@ func mockContainerError(pod *corev1.Pod) error { } func setPodRole(pod *corev1.Pod, role string) error { - return testapps.ChangeObj(&testCtx, pod, func() { - pod.Labels[constant.RoleLabelKey] = role + return testapps.ChangeObj(&testCtx, pod, func(lpod *corev1.Pod) { + lpod.Labels[constant.RoleLabelKey] = role }) } diff --git a/controllers/apps/components/component_workload_controller.go b/controllers/apps/components/component_workload_controller.go new file mode 100644 index 000000000..40cf86790 --- /dev/null +++ b/controllers/apps/components/component_workload_controller.go @@ -0,0 +1,214 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package components + +import ( + "context" + "strconv" + + "golang.org/x/exp/slices" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" +) + +func NewDeploymentReconciler(mgr ctrl.Manager) error { + return newComponentWorkloadReconciler(mgr, "deployment-controller", generics.DeploymentSignature, generics.ReplicaSetSignature) +} + +func NewStatefulSetReconciler(mgr ctrl.Manager) error { + return newComponentWorkloadReconciler(mgr, "statefulset-controller", generics.StatefulSetSignature, generics.PodSignature) +} + +func newComponentWorkloadReconciler[T generics.Object, PT generics.PObject[T], LT generics.ObjList[T], S generics.Object, PS generics.PObject[S], LS generics.ObjList[S]]( + mgr ctrl.Manager, name string, _ func(T, LT), _ func(S, LS)) error { + return (&componentWorkloadReconciler[T, PT, S, PS]{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + Recorder: mgr.GetEventRecorderFor(name), + }).SetupWithManager(mgr, name) +} + +// componentWorkloadReconciler reconciles a component workload object +type componentWorkloadReconciler[T generics.Object, PT generics.PObject[T], S generics.Object, PS generics.PObject[S]] struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get +// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update + +// 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.14.4/pkg/reconcile +func (r *componentWorkloadReconciler[T, PT, S, PS]) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + reqCtx := intctrlutil.RequestCtx{ + Ctx: ctx, + Req: req, + Log: log.FromContext(ctx).WithValues("deployment", req.NamespacedName), + } + + var obj T + pObj := PT(&obj) + if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, pObj); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + // skip if workload is being deleted + if !pObj.GetDeletionTimestamp().IsZero() { + return intctrlutil.Reconciled() + } + + handler := func(cluster *appsv1alpha1.Cluster, compSpec *appsv1alpha1.ClusterComponentSpec, + compDef *appsv1alpha1.ClusterComponentDefinition) (ctrl.Result, error) { + // update component info to pods' annotations + if err := updateComponentInfoToPods(reqCtx.Ctx, r.Client, cluster, compSpec); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + // patch the current componentSpec workload's custom labels + if err := patchWorkloadCustomLabel(reqCtx.Ctx, r.Client, cluster, compSpec); err != nil { + reqCtx.Event(cluster, corev1.EventTypeWarning, "Component Workload Controller PatchWorkloadCustomLabelFailed", err.Error()) + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + return intctrlutil.Reconciled() + } + return workloadCompClusterReconcile(reqCtx, r.Client, pObj, handler) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *componentWorkloadReconciler[T, PT, S, PS]) SetupWithManager(mgr ctrl.Manager, name string) error { + var ( + obj1 T + obj2 S + ) + return ctrl.NewControllerManagedBy(mgr). + For(PT(&obj1)). + Owns(PS(&obj2)). + WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.WorkloadFilterPredicate)). + Named(name). + Complete(r) +} + +func workloadCompClusterReconcile(reqCtx intctrlutil.RequestCtx, + cli client.Client, + operand client.Object, + processor func(*appsv1alpha1.Cluster, *appsv1alpha1.ClusterComponentSpec, *appsv1alpha1.ClusterComponentDefinition) (ctrl.Result, error)) (ctrl.Result, error) { + var err error + var cluster *appsv1alpha1.Cluster + + if cluster, err = util.GetClusterByObject(reqCtx.Ctx, cli, operand); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } else if cluster == nil { + return intctrlutil.Reconciled() + } + + clusterDef := &appsv1alpha1.ClusterDefinition{} + if err = cli.Get(reqCtx.Ctx, client.ObjectKey{Name: cluster.Spec.ClusterDefRef}, clusterDef); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + componentName := operand.GetLabels()[constant.KBAppComponentLabelKey] + componentSpec := cluster.Spec.GetComponentByName(componentName) + if componentSpec == nil { + return intctrlutil.Reconciled() + } + componentDef := clusterDef.GetComponentDefByName(componentSpec.ComponentDefRef) + + return processor(cluster, componentSpec, componentDef) +} + +func updateComponentInfoToPods( + ctx context.Context, + cli client.Client, + cluster *appsv1alpha1.Cluster, + componentSpec *appsv1alpha1.ClusterComponentSpec) error { + if cluster == nil || componentSpec == nil { + return nil + } + ml := client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.GetName(), + constant.KBAppComponentLabelKey: componentSpec.Name, + } + podList := corev1.PodList{} + if err := cli.List(ctx, &podList, ml); err != nil { + return err + } + replicasStr := strconv.Itoa(int(componentSpec.Replicas)) + for _, pod := range podList.Items { + if pod.Annotations != nil && + pod.Annotations[constant.ComponentReplicasAnnotationKey] == replicasStr { + continue + } + patch := client.MergeFrom(pod.DeepCopy()) + if pod.Annotations == nil { + pod.Annotations = make(map[string]string) + } + pod.Annotations[constant.ComponentReplicasAnnotationKey] = replicasStr + if err := cli.Patch(ctx, &pod, patch); err != nil { + return err + } + } + return nil +} + +// patchWorkloadCustomLabel patches workload custom labels. +func patchWorkloadCustomLabel(ctx context.Context, + cli client.Client, + cluster *appsv1alpha1.Cluster, + componentSpec *appsv1alpha1.ClusterComponentSpec) error { + if cluster == nil || componentSpec == nil { + return nil + } + compDef, err := util.GetComponentDefByCluster(ctx, cli, *cluster, componentSpec.ComponentDefRef) + if err != nil { + return err + } + for _, customLabelSpec := range compDef.CustomLabelSpecs { + // TODO if the customLabelSpec.Resources is empty, we should add the label to the workload resources under the component. + for _, resource := range customLabelSpec.Resources { + gvk, err := util.ParseCustomLabelPattern(resource.GVK) + if err != nil { + return err + } + // only handle workload kind + if !slices.Contains(util.GetCustomLabelWorkloadKind(), gvk.Kind) { + continue + } + if err := util.PatchGVRCustomLabels(ctx, cli, cluster, resource, componentSpec.Name, customLabelSpec.Key, customLabelSpec.Value); err != nil { + return err + } + } + } + return nil +} diff --git a/controllers/apps/components/component_workload_controller_test.go b/controllers/apps/components/component_workload_controller_test.go new file mode 100644 index 000000000..a23bd1f0f --- /dev/null +++ b/controllers/apps/components/component_workload_controller_test.go @@ -0,0 +1,451 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package components + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/stateless" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" +) + +// var _ = Describe("Deployment Controller", func() { +// var ( +// randomStr = testCtx.GetRandomStr() +// clusterDefName = "stateless-definition1-" + randomStr +// clusterVersionName = "stateless-cluster-version1-" + randomStr +// clusterName = "stateless1-" + randomStr +// ) +// +// const ( +// namespace = "default" +// statelessCompName = "stateless" +// statelessCompDefName = "stateless" +// ) +// +// cleanAll := func() { +// // must wait until resources deleted and no longer exist before the testcases start, +// // otherwise if later it needs to create some new resource objects with the same name, +// // in race conditions, it will find the existence of old objects, resulting failure to +// // create the new objects. +// By("clean resources") +// +// // delete cluster(and all dependent sub-resources), clusterversion and clusterdef +// testapps.ClearClusterResources(&testCtx) +// +// // clear rest resources +// inNS := client.InNamespace(testCtx.DefaultNamespace) +// ml := client.HasLabels{testCtx.TestObjLabelKey} +// // namespaced resources +// testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) +// testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) +// } +// +// BeforeEach(cleanAll) +// +// AfterEach(cleanAll) +// +// Context("test controller", func() { +// It("", func() { +// testapps.NewClusterDefFactory(clusterDefName). +// AddComponentDef(testapps.StatelessNginxComponent, statelessCompDefName). +// Create(&testCtx).GetObject() +// +// cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). +// AddComponent(statelessCompName, statelessCompDefName).SetReplicas(2).Create(&testCtx).GetObject() +// +// By("patch cluster to Running") +// Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { +// cluster.Status.Phase = appsv1alpha1.RunningClusterPhase +// })) +// +// By("create the deployment of the stateless component") +// deploy := testapps.MockStatelessComponentDeploy(testCtx, clusterName, statelessCompName) +// newDeploymentKey := client.ObjectKey{Name: deploy.Name, Namespace: namespace} +// Eventually(testapps.CheckObj(&testCtx, newDeploymentKey, func(g Gomega, deploy *appsv1.Deployment) { +// g.Expect(deploy.Generation == 1).Should(BeTrue()) +// })).Should(Succeed()) +// +// By("check stateless component phase is Failed") +// Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, statelessCompName)).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) +// +// By("mock error message and PodCondition about some pod's failure") +// podName := fmt.Sprintf("%s-%s-%s", clusterName, statelessCompName, testCtx.GetRandomStr()) +// pod := testapps.MockStatelessPod(testCtx, deploy, clusterName, statelessCompName, podName) +// // mock pod container is failed +// errMessage := "Back-off pulling image nginx:latest" +// Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { +// pod.Status.ContainerStatuses = []corev1.ContainerStatus{ +// { +// State: corev1.ContainerState{ +// Waiting: &corev1.ContainerStateWaiting{ +// Reason: "ImagePullBackOff", +// Message: errMessage, +// }, +// }, +// }, +// } +// })).Should(Succeed()) +// // mock failed container timed out +// Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { +// pod.Status.Conditions = []corev1.PodCondition{ +// { +// Type: corev1.ContainersReady, +// Status: corev1.ConditionFalse, +// LastTransitionTime: metav1.NewTime(time.Now().Add(-2 * time.Minute)), +// }, +// } +// })).Should(Succeed()) +// // mark deployment to reconcile +// Expect(testapps.ChangeObj(&testCtx, deploy, func(ldeploy *appsv1.Deployment) { +// ldeploy.Annotations = map[string]string{ +// "reconcile": "1", +// } +// })).Should(Succeed()) +// +// By("check component.Status.Message contains pod error message") +// Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { +// compStatus := tmpCluster.Status.Components[statelessCompName] +// g.Expect(compStatus.GetObjectMessage("Pod", pod.Name)).Should(Equal(errMessage)) +// })).Should(Succeed()) +// +// By("mock deployment is ready") +// Expect(testapps.ChangeObjStatus(&testCtx, deploy, func() { +// testk8s.MockDeploymentReady(deploy, stateless.NewRSAvailableReason, deploy.Name+"-5847cb795c") +// })).Should(Succeed()) +// +// By("waiting for the component to be running") +// Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, statelessCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) +// }) +// }) +// }) + +var _ = Describe("Deployment Controller", func() { + var ( + randomStr = testCtx.GetRandomStr() + clusterDefName = "stateless-definition1-" + randomStr + clusterVersionName = "stateless-cluster-version1-" + randomStr + clusterName = "stateless1-" + randomStr + ) + + const ( + namespace = "default" + statelessCompName = "stateless" + statelessCompType = "stateless" + ) + + cleanAll := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete cluster(and all dependent sub-resources), clusterversion and clusterdef + testapps.ClearClusterResources(&testCtx) + + // clear rest resources + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + // namespaced resources + testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) + testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) + } + + BeforeEach(cleanAll) + + AfterEach(cleanAll) + + // TODO: Should review the purpose of these test cases + PContext("test controller", func() { + It("", func() { + testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.StatelessNginxComponent, statelessCompType). + Create(&testCtx).GetObject() + + testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). + AddComponentVersion(statelessCompType).AddContainerShort(testapps.DefaultNginxContainerName, testapps.NginxImage). + Create(&testCtx).GetObject() + + cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). + AddComponent(statelessCompName, statelessCompType).SetReplicas(2).Create(&testCtx).GetObject() + clusterKey := client.ObjectKeyFromObject(cluster) + + By("patch cluster to Running") + Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { + cluster.Status.Phase = appsv1alpha1.RunningClusterPhase + })) + + By("create the deployment of the stateless component") + deploy := testapps.MockStatelessComponentDeploy(&testCtx, clusterName, statelessCompName) + newDeploymentKey := client.ObjectKey{Name: deploy.Name, Namespace: namespace} + Eventually(testapps.CheckObj(&testCtx, newDeploymentKey, func(g Gomega, deploy *appsv1.Deployment) { + g.Expect(deploy.Generation == 1).Should(BeTrue()) + })).Should(Succeed()) + + By("check stateless component phase is Creating") + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, statelessCompName)).Should(Equal(appsv1alpha1.CreatingClusterCompPhase)) + + By("mock error message and PodCondition about some pod's failure") + podName := fmt.Sprintf("%s-%s-%s", clusterName, statelessCompName, testCtx.GetRandomStr()) + pod := testapps.MockStatelessPod(&testCtx, deploy, clusterName, statelessCompName, podName) + // mock pod container is failed + errMessage := "Back-off pulling image nginx:latest" + Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { + pod.Status.ContainerStatuses = []corev1.ContainerStatus{ + { + State: corev1.ContainerState{ + Waiting: &corev1.ContainerStateWaiting{ + Reason: "ImagePullBackOff", + Message: errMessage, + }, + }, + }, + } + })).Should(Succeed()) + // mock failed container timed out + Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { + pod.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.ContainersReady, + Status: corev1.ConditionFalse, + LastTransitionTime: metav1.NewTime(time.Now().Add(-2 * time.Minute)), + }, + } + })).Should(Succeed()) + // mark deployment to reconcile + Expect(testapps.ChangeObj(&testCtx, deploy, func(lobj *appsv1.Deployment) { + lobj.Annotations = map[string]string{ + "reconcile": "1", + } + })).Should(Succeed()) + + By("check stateless component phase is Failed") + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, statelessCompName)).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) + + By("check component.Status.Message contains pod error message") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + compStatus := tmpCluster.Status.Components[statelessCompName] + g.Expect(compStatus.GetObjectMessage("Pod", pod.Name)).Should(Equal(errMessage)) + })).Should(Succeed()) + + By("mock deployment is ready") + Expect(testapps.ChangeObjStatus(&testCtx, deploy, func() { + testk8s.MockDeploymentReady(deploy, stateless.NewRSAvailableReason, deploy.Name+"-5847cb795c") + })).Should(Succeed()) + + By("waiting for the component to be running") + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, statelessCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + }) + }) +}) + +var _ = Describe("StatefulSet Controller", func() { + + var ( + randomStr = testCtx.GetRandomStr() + clusterName = "mysql-" + randomStr + clusterDefName = "cluster-definition-consensus-" + randomStr + clusterVersionName = "cluster-version-operations-" + randomStr + opsRequestName = "wesql-restart-test-" + randomStr + ) + + const ( + revisionID = "6fdd48d9cd" + consensusCompName = "consensus" + consensusCompType = "consensus" + ) + + cleanAll := func() { + // must wait until resources deleted and no longer exist before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete cluster(and all dependent sub-resources), clusterversion and clusterdef + testapps.ClearClusterResources(&testCtx) + + // clear rest resources + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + // namespaced resources + testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.StatefulSetSignature, true, inNS, ml) + testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) + } + + BeforeEach(cleanAll) + + AfterEach(cleanAll) + + testUpdateStrategy := func(updateStrategy appsv1alpha1.UpdateStrategy, componentName string, index int) { + Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKey{Name: clusterDefName}, + func(clusterDef *appsv1alpha1.ClusterDefinition) { + clusterDef.Spec.ComponentDefs[0].ConsensusSpec.UpdateStrategy = appsv1alpha1.SerialStrategy + })()).Should(Succeed()) + + // mock consensus component is not ready + objectKey := client.ObjectKey{Name: clusterName + "-" + componentName, Namespace: testCtx.DefaultNamespace} + Expect(testapps.GetAndChangeObjStatus(&testCtx, objectKey, func(newSts *appsv1.StatefulSet) { + newSts.Status.UpdateRevision = fmt.Sprintf("%s-%s-%dfdd48d8cd", clusterName, componentName, index) + newSts.Status.ObservedGeneration = newSts.Generation - 1 + })()).Should(Succeed()) + } + + testUsingEnvTest := func(sts *appsv1.StatefulSet) []*corev1.Pod { + By("mock statefulset update completed") + updateRevision := fmt.Sprintf("%s-%s-%s", clusterName, consensusCompName, revisionID) + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + sts.Status.UpdateRevision = updateRevision + testk8s.MockStatefulSetReady(sts) + sts.Status.ObservedGeneration = 2 + })).Should(Succeed()) + + By("create pods of statefulset") + pods := testapps.MockConsensusComponentPods(&testCtx, sts, clusterName, consensusCompName) + + By("Mock a pod without role label and it will wait for HandleProbeTimeoutWhenPodsReady") + leaderPod := pods[0] + Expect(testapps.ChangeObj(&testCtx, leaderPod, func(lpod *corev1.Pod) { + delete(lpod.Labels, constant.RoleLabelKey) + })).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(leaderPod), func(g Gomega, pod *corev1.Pod) { + g.Expect(pod.Labels[constant.RoleLabelKey] == "").Should(BeTrue()) + })).Should(Succeed()) + + By("mock restart component to trigger reconcile of StatefulSet controller") + Expect(testapps.ChangeObj(&testCtx, sts, func(lsts *appsv1.StatefulSet) { + lsts.Spec.Template.Annotations = map[string]string{ + constant.RestartAnnotationKey: time.Now().Format(time.RFC3339), + } + })).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(sts), + func(g Gomega, fetched *appsv1.StatefulSet) { + g.Expect(fetched.Status.UpdateRevision).To(Equal(updateRevision)) + })).Should(Succeed()) + + By("wait for component podsReady to be true and phase to be 'Failed'") + clusterKey := client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace} + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + compStatus := cluster.Status.Components[consensusCompName] + g.Expect(compStatus.Phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) // original expecting value RebootingPhase + g.Expect(compStatus.PodsReady).ShouldNot(BeNil()) + g.Expect(*compStatus.PodsReady).Should(BeTrue()) + // REVIEW/TODO: ought add extra condition check for RebootingPhase + })).Should(Succeed()) + + By("add leader role label for leaderPod and update sts as ready to mock consensus component to be Running") + Expect(testapps.ChangeObj(&testCtx, leaderPod, func(lpod *corev1.Pod) { + lpod.Labels[constant.RoleLabelKey] = "leader" + })).Should(Succeed()) + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + sts.Status.UpdateRevision = updateRevision + testk8s.MockStatefulSetReady(sts) + sts.Status.ObservedGeneration = 3 + })).Should(Succeed()) + + By("check the component phase becomes Running") + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, consensusCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + + return pods + } + + // TODO: Should review the purpose of these test cases + PContext("test controller", func() { + It("test statefulSet controller", func() { + By("mock cluster object") + _, _, cluster := testapps.InitConsensusMysql(&testCtx, clusterDefName, + clusterVersionName, clusterName, consensusCompType, consensusCompName) + clusterKey := client.ObjectKeyFromObject(cluster) + + // REVIEW/TODO: "Rebooting" got refactored + By("mock cluster phase is 'Rebooting' and restart operation is running on cluster") + Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { + cluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase + cluster.Status.ObservedGeneration = 1 + cluster.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ + consensusCompName: { + Phase: appsv1alpha1.SpecReconcilingClusterCompPhase, + }, + } + })).Should(Succeed()) + _ = testapps.CreateRestartOpsRequest(&testCtx, clusterName, opsRequestName, []string{consensusCompName}) + Expect(testapps.ChangeObj(&testCtx, cluster, func(lcluster *appsv1alpha1.Cluster) { + lcluster.Annotations = map[string]string{ + constant.OpsRequestAnnotationKey: fmt.Sprintf(`[{"name":"%s","clusterPhase":"Updating"}]`, opsRequestName), + } + })).Should(Succeed()) + + // trigger statefulset controller Reconcile + sts := testapps.MockConsensusComponentStatefulSet(&testCtx, clusterName, consensusCompName) + + By("mock the StatefulSet and pods are ready") + // mock statefulSet available and consensusSet component is running + pods := testUsingEnvTest(sts) + + By("mock component of cluster is stopping") + Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { + tmpCluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase + tmpCluster.Status.SetComponentStatus(consensusCompName, appsv1alpha1.ClusterComponentStatus{ + Phase: appsv1alpha1.SpecReconcilingClusterCompPhase, + }) + })()).Should(Succeed()) + + By("mock stop operation and processed successfully") + Expect(testapps.ChangeObj(&testCtx, cluster, func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs[0].Replicas = 0 + })).Should(Succeed()) + Expect(testapps.ChangeObj(&testCtx, sts, func(lsts *appsv1.StatefulSet) { + replicas := int32(0) + lsts.Spec.Replicas = &replicas + })).Should(Succeed()) + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + testk8s.MockStatefulSetReady(sts) + })).Should(Succeed()) + // delete all pods of components + for _, v := range pods { + testapps.DeleteObject(&testCtx, client.ObjectKeyFromObject(v), v) + } + + By("check the component phase becomes Stopped") + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, consensusCompName)).Should(Equal(appsv1alpha1.StoppedClusterCompPhase)) + + By("test updateStrategy with Serial") + testUpdateStrategy(appsv1alpha1.SerialStrategy, consensusCompName, 1) + + By("test updateStrategy with Parallel") + testUpdateStrategy(appsv1alpha1.ParallelStrategy, consensusCompName, 2) + }) + }) +}) diff --git a/controllers/apps/components/consensus/component_consensus.go b/controllers/apps/components/consensus/component_consensus.go new file mode 100644 index 000000000..3cb38d37e --- /dev/null +++ b/controllers/apps/components/consensus/component_consensus.go @@ -0,0 +1,111 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensus + +import ( + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/controllers/apps/components/stateful" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +func NewConsensusComponent(cli client.Client, + recorder record.EventRecorder, + cluster *appsv1alpha1.Cluster, + clusterVersion *appsv1alpha1.ClusterVersion, + synthesizedComponent *component.SynthesizedComponent, + dag *graph.DAG) *consensusComponent { + comp := &consensusComponent{ + StatefulComponentBase: internal.StatefulComponentBase{ + ComponentBase: internal.ComponentBase{ + Client: cli, + Recorder: recorder, + Cluster: cluster, + ClusterVersion: clusterVersion, + Component: synthesizedComponent, + ComponentSet: &ConsensusSet{ + Stateful: stateful.Stateful{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: nil, + ComponentDef: nil, + Component: nil, + }, + }, + }, + Dag: dag, + WorkloadVertex: nil, + }, + }, + } + comp.ComponentSet.SetComponent(comp) + return comp +} + +type consensusComponent struct { + internal.StatefulComponentBase +} + +var _ types.Component = &consensusComponent{} + +func (c *consensusComponent) newBuilder(reqCtx intctrlutil.RequestCtx, cli client.Client, + action *ictrltypes.LifecycleAction) internal.ComponentWorkloadBuilder { + builder := &consensusComponentWorkloadBuilder{ + ComponentWorkloadBuilderBase: internal.ComponentWorkloadBuilderBase{ + ReqCtx: reqCtx, + Client: cli, + Comp: c, + DefaultAction: action, + Error: nil, + EnvConfig: nil, + Workload: nil, + }, + } + builder.ConcreteBuilder = builder + return builder +} + +func (c *consensusComponent) GetWorkloadType() appsv1alpha1.WorkloadType { + return appsv1alpha1.Consensus +} + +func (c *consensusComponent) GetBuiltObjects(reqCtx intctrlutil.RequestCtx, cli client.Client) ([]client.Object, error) { + return c.StatefulComponentBase.GetBuiltObjects(c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *consensusComponent) Create(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Create(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *consensusComponent) Update(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Update(reqCtx, cli, c.newBuilder(reqCtx, cli, nil)) +} + +func (c *consensusComponent) Status(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Status(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionNoopPtr())) +} diff --git a/controllers/apps/components/consensus/component_consensus_workload.go b/controllers/apps/components/consensus/component_consensus_workload.go new file mode 100644 index 000000000..56bc2eb28 --- /dev/null +++ b/controllers/apps/components/consensus/component_consensus_workload.go @@ -0,0 +1,57 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensus + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" +) + +type consensusComponentWorkloadBuilder struct { + internal.ComponentWorkloadBuilderBase +} + +var _ internal.ComponentWorkloadBuilder = &consensusComponentWorkloadBuilder{} + +func (b *consensusComponentWorkloadBuilder) BuildWorkload() internal.ComponentWorkloadBuilder { + return b.BuildWorkload4StatefulSet("consensus") +} + +func (b *consensusComponentWorkloadBuilder) BuildService() internal.ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + svcList, err := builder.BuildSvcListLow(b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + if err != nil { + return nil, err + } + objs := make([]client.Object, 0, len(svcList)) + leader := b.Comp.GetConsensusSpec().Leader + for _, svc := range svcList { + if len(leader.Name) > 0 { + svc.Spec.Selector[constant.RoleLabelKey] = leader.Name + } + objs = append(objs, svc) + } + return objs, err + } + return b.BuildWrapper(buildfn) +} diff --git a/controllers/apps/components/consensus/consensus.go b/controllers/apps/components/consensus/consensus.go new file mode 100644 index 000000000..95687bc33 --- /dev/null +++ b/controllers/apps/components/consensus/consensus.go @@ -0,0 +1,308 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensus + +import ( + "context" + + "github.com/google/go-cmp/cmp" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/stateful" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type ConsensusSet struct { + stateful.Stateful +} + +var _ types.ComponentSet = &ConsensusSet{} + +func (r *ConsensusSet) getName() string { + if r.Component != nil { + return r.Component.GetName() + } + return r.ComponentSpec.Name +} + +func (r *ConsensusSet) getDefName() string { + if r.Component != nil { + return r.Component.GetDefinitionName() + } + return r.ComponentDef.Name +} + +func (r *ConsensusSet) getWorkloadType() appsv1alpha1.WorkloadType { + if r.Component != nil { + return r.Component.GetWorkloadType() + } + return r.ComponentDef.WorkloadType +} + +func (r *ConsensusSet) getReplicas() int32 { + if r.Component != nil { + return r.Component.GetReplicas() + } + return r.ComponentSpec.Replicas +} + +func (r *ConsensusSet) getConsensusSpec() *appsv1alpha1.ConsensusSetSpec { + if r.Component != nil { + return r.Component.GetConsensusSpec() + } + return r.ComponentDef.ConsensusSpec +} + +func (r *ConsensusSet) getProbes() *appsv1alpha1.ClusterDefinitionProbes { + if r.Component != nil { + return r.Component.GetSynthesizedComponent().Probes + } + return r.ComponentDef.Probes +} + +func (r *ConsensusSet) SetComponent(comp types.Component) { + r.Component = comp +} + +func (r *ConsensusSet) IsRunning(ctx context.Context, obj client.Object) (bool, error) { + if obj == nil { + return false, nil + } + sts := util.ConvertToStatefulSet(obj) + isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, r.Cli, sts) + if err != nil { + return false, err + } + pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, sts) + if err != nil { + return false, err + } + for _, pod := range pods { + if !intctrlutil.PodIsReadyWithLabel(pod) { + return false, nil + } + } + + targetReplicas := r.getReplicas() + return util.StatefulSetOfComponentIsReady(sts, isRevisionConsistent, &targetReplicas), nil +} + +func (r *ConsensusSet) PodsReady(ctx context.Context, obj client.Object) (bool, error) { + return r.Stateful.PodsReady(ctx, obj) +} + +func (r *ConsensusSet) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { + if pod == nil { + return false + } + return intctrlutil.PodIsReadyWithLabel(*pod) +} + +func (r *ConsensusSet) GetPhaseWhenPodsReadyAndProbeTimeout(pods []*corev1.Pod) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap) { + var ( + isAbnormal bool + isFailed = true + statusMessages appsv1alpha1.ComponentMessageMap + ) + compStatus, ok := r.Cluster.Status.Components[r.getName()] + if !ok || compStatus.PodsReadyTime == nil { + return "", nil + } + if !util.IsProbeTimeout(r.getProbes(), compStatus.PodsReadyTime) { + return "", nil + } + for _, pod := range pods { + role := pod.Labels[constant.RoleLabelKey] + if role == r.getConsensusSpec().Leader.Name { + isFailed = false + } + if role == "" { + isAbnormal = true + statusMessages.SetObjectMessage(pod.Kind, pod.Name, "Role probe timeout, check whether the application is available") + } + // TODO clear up the message of ready pod in component.message. + } + if isFailed { + return appsv1alpha1.FailedClusterCompPhase, statusMessages + } + if isAbnormal { + return appsv1alpha1.AbnormalClusterCompPhase, statusMessages + } + return "", statusMessages +} + +func (r *ConsensusSet) GetPhaseWhenPodsNotReady(ctx context.Context, + componentName string) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) { + stsList := &appsv1.StatefulSetList{} + podList, err := util.GetCompRelatedObjectList(ctx, r.Cli, *r.Cluster, + componentName, stsList) + if err != nil || len(stsList.Items) == 0 { + return "", nil, err + } + stsObj := stsList.Items[0] + podCount := len(podList.Items) + componentReplicas := r.getReplicas() + if podCount == 0 || stsObj.Status.AvailableReplicas == 0 { + return util.GetPhaseWithNoAvailableReplicas(componentReplicas), nil, nil + } + // get the statefulSet of component + var ( + existLatestRevisionFailedPod bool + leaderIsReady bool + consensusSpec = r.getConsensusSpec() + ) + for _, v := range podList.Items { + // if the pod is terminating, ignore it + if v.DeletionTimestamp != nil { + return "", nil, nil + } + labelValue := v.Labels[constant.RoleLabelKey] + if consensusSpec != nil && labelValue == consensusSpec.Leader.Name && intctrlutil.PodIsReady(&v) { + leaderIsReady = true + continue + } + if !intctrlutil.PodIsReady(&v) && intctrlutil.PodIsControlledByLatestRevision(&v, &stsObj) { + existLatestRevisionFailedPod = true + } + } + return util.GetCompPhaseByConditions(existLatestRevisionFailedPod, leaderIsReady, + componentReplicas, int32(podCount), stsObj.Status.AvailableReplicas), nil, nil +} + +func (r *ConsensusSet) HandleRestart(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + if r.getWorkloadType() != appsv1alpha1.Consensus { + return nil, nil + } + + stsObj := util.ConvertToStatefulSet(obj) + pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, stsObj) + if err != nil { + return nil, err + } + + // prepare to do pods Deletion, that's the only thing we should do, + // the statefulset reconciler will do the rest. + // to simplify the process, we do pods Deletion after statefulset reconciliation done, + // it is when stsObj.Generation == stsObj.Status.ObservedGeneration + if stsObj.Generation != stsObj.Status.ObservedGeneration { + return nil, nil + } + + // then we wait for all pods' presence when len(pods) == stsObj.Spec.Replicas + // at that point, we have enough info about the previous pods before deleting the current one + if len(pods) != int(*stsObj.Spec.Replicas) { + return nil, nil + } + + // we don't check whether pod role label is present: prefer stateful set's Update done than role probing ready + + // generate the pods Deletion plan + podsToDelete := make([]*corev1.Pod, 0) + plan := generateRestartPodPlan(ctx, r.Cli, stsObj, pods, r.getConsensusSpec(), &podsToDelete) + // execute plan + if _, err := plan.WalkOneStep(); err != nil { + return nil, err + } + + vertexes := make([]graph.Vertex, 0) + for _, pod := range podsToDelete { + vertexes = append(vertexes, &ictrltypes.LifecycleVertex{ + Obj: pod, + Action: ictrltypes.ActionDeletePtr(), + Orphan: true, + }) + } + return vertexes, nil +} + +func (r *ConsensusSet) HandleRoleChange(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + if r.getWorkloadType() != appsv1alpha1.Consensus { + return nil, nil + } + + stsObj := util.ConvertToStatefulSet(obj) + pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, stsObj) + if err != nil { + return nil, err + } + + // update cluster.status.component.consensusSetStatus based on the existences for all pods + componentName := r.getName() + + // first, get the old status + var oldConsensusSetStatus *appsv1alpha1.ConsensusSetStatus + if v, ok := r.Cluster.Status.Components[componentName]; ok { + oldConsensusSetStatus = v.ConsensusSetStatus + } + // create the initial status + newConsensusSetStatus := &appsv1alpha1.ConsensusSetStatus{ + Leader: appsv1alpha1.ConsensusMemberStatus{ + Name: "", + Pod: constant.ComponentStatusDefaultPodName, + AccessMode: appsv1alpha1.None, + }, + } + // then, set the new status + setConsensusSetStatusRoles(newConsensusSetStatus, r.getConsensusSpec(), pods) + // if status changed, do update + if !cmp.Equal(newConsensusSetStatus, oldConsensusSetStatus) { + if err = util.InitClusterComponentStatusIfNeed(r.Cluster, componentName, r.getWorkloadType()); err != nil { + return nil, err + } + componentStatus := r.Cluster.Status.Components[componentName] + componentStatus.ConsensusSetStatus = newConsensusSetStatus + r.Cluster.Status.SetComponentStatus(componentName, componentStatus) + + // TODO: does the update order between cluster and env configmap matter? + + // add consensus role info to pod env + return updateConsensusRoleInfo(ctx, r.Cli, r.Cluster, r.getConsensusSpec(), r.getDefName(), componentName, pods) + } + return nil, nil +} + +func (r *ConsensusSet) HandleHA(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + return nil, nil +} + +func newConsensusSet(cli client.Client, + cluster *appsv1alpha1.Cluster, + spec *appsv1alpha1.ClusterComponentSpec, + def appsv1alpha1.ClusterComponentDefinition) *ConsensusSet { + return &ConsensusSet{ + Stateful: stateful.Stateful{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: spec, + ComponentDef: &def, + Component: nil, + }, + }, + } +} diff --git a/controllers/apps/components/consensusset/consensus_test.go b/controllers/apps/components/consensus/consensus_test.go similarity index 68% rename from controllers/apps/components/consensusset/consensus_test.go rename to controllers/apps/components/consensus/consensus_test.go index 59e7afcee..ce8abdd5d 100644 --- a/controllers/apps/components/consensusset/consensus_test.go +++ b/controllers/apps/components/consensus/consensus_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package consensusset +package consensus import ( "fmt" @@ -23,6 +26,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -85,30 +89,23 @@ var _ = Describe("Consensus Component", func() { })).Should(Succeed()) } - validateComponentStatus := func(cluster *appsv1alpha1.Cluster) { - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - g.Expect(tmpCluster.Status.Components[consensusCompName].Phase == appsv1alpha1.FailedClusterCompPhase).Should(BeTrue()) - })).Should(Succeed()) - } - Context("Consensus Component test", func() { It("Consensus Component test", func() { By(" init cluster, statefulSet, pods") - clusterDef, _, cluster := testapps.InitConsensusMysql(testCtx, clusterDefName, + clusterDef, _, cluster := testapps.InitConsensusMysql(&testCtx, clusterDefName, clusterVersionName, clusterName, "consensus", consensusCompName) - sts := testapps.MockConsensusComponentStatefulSet(testCtx, clusterName, consensusCompName) + sts := testapps.MockConsensusComponentStatefulSet(&testCtx, clusterName, consensusCompName) componentName := consensusCompName compDefName := cluster.Spec.GetComponentDefRefName(componentName) componentDef := clusterDef.GetComponentDefByName(compDefName) component := cluster.Spec.GetComponentByName(componentName) By("test pods are not ready") - consensusComponent, err := NewConsensusSet(k8sClient, cluster, component, *componentDef) - Expect(err).Should(Succeed()) + consensusComponent := newConsensusSet(k8sClient, cluster, component, *componentDef) sts.Status.AvailableReplicas = *sts.Spec.Replicas - 1 podsReady, _ := consensusComponent.PodsReady(ctx, sts) - Expect(podsReady == false).Should(BeTrue()) + Expect(podsReady).Should(BeFalse()) By("test pods are ready") // mock sts is ready @@ -120,41 +117,42 @@ var _ = Describe("Consensus Component", func() { })).Should(Succeed()) podsReady, _ = consensusComponent.PodsReady(ctx, sts) - Expect(podsReady == true).Should(BeTrue()) + Expect(podsReady).Should(BeTrue()) By("test component is running") isRunning, _ := consensusComponent.IsRunning(ctx, sts) - Expect(isRunning == false).Should(BeTrue()) + Expect(isRunning).Should(BeFalse()) podName := sts.Name + "-0" - podList := testapps.MockConsensusComponentPods(testCtx, sts, clusterName, consensusCompName) + podList := testapps.MockConsensusComponentPods(&testCtx, sts, clusterName, consensusCompName) By("expect for pod is available") Expect(consensusComponent.PodIsAvailable(podList[0], defaultMinReadySeconds)).Should(BeTrue()) By("test handle probe timed out") mockClusterStatusProbeTimeout(cluster) // mock leader pod is not ready - testk8s.UpdatePodStatusNotReady(ctx, testCtx, podName) + testk8s.UpdatePodStatusScheduleFailed(ctx, testCtx, podName, testCtx.DefaultNamespace) testk8s.DeletePodLabelKey(ctx, testCtx, podName, constant.RoleLabelKey) - requeue, _ := consensusComponent.HandleProbeTimeoutWhenPodsReady(ctx, nil) - Expect(requeue).ShouldNot(BeTrue()) - validateComponentStatus(cluster) + pod := &corev1.Pod{} + Expect(testCtx.Cli.Get(ctx, client.ObjectKey{Name: podName, Namespace: testCtx.DefaultNamespace}, pod)).Should(Succeed()) + phase, _ := consensusComponent.GetPhaseWhenPodsReadyAndProbeTimeout([]*corev1.Pod{pod}) + Expect(phase == appsv1alpha1.FailedClusterCompPhase).Should(BeTrue()) By("test component is running") isRunning, _ = consensusComponent.IsRunning(ctx, sts) - Expect(isRunning == false).Should(BeTrue()) + Expect(isRunning).Should(BeFalse()) By("expect component phase is Failed when pod of component is failed") - phase, _ := consensusComponent.GetPhaseWhenPodsNotReady(ctx, consensusCompName) + phase, _, _ = consensusComponent.GetPhaseWhenPodsNotReady(ctx, consensusCompName) Expect(phase == appsv1alpha1.FailedClusterCompPhase).Should(BeTrue()) - By("not ready pod is not controlled by latest revision, should return empty string") + By("unready pod is not controlled by latest revision, should return empty string") // mock pod is not controlled by latest revision Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { sts.Status.UpdateRevision = fmt.Sprintf("%s-%s-%s", clusterName, consensusCompName, "6fdd48d9cd1") })).Should(Succeed()) - phase, _ = consensusComponent.GetPhaseWhenPodsNotReady(ctx, consensusCompName) - Expect(len(phase) == 0).Should(BeTrue()) + phase, _, _ = consensusComponent.GetPhaseWhenPodsNotReady(ctx, consensusCompName) + Expect(string(phase)).Should(Equal("")) }) }) }) diff --git a/controllers/apps/components/consensusset/consensus_set_utils.go b/controllers/apps/components/consensus/consensus_utils.go similarity index 68% rename from controllers/apps/components/consensusset/consensus_set_utils.go rename to controllers/apps/components/consensus/consensus_utils.go index 8c5dc181a..86e8628ea 100644 --- a/controllers/apps/components/consensusset/consensus_set_utils.go +++ b/controllers/apps/components/consensus/consensus_utils.go @@ -1,35 +1,39 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package consensusset +package consensus import ( "context" + "errors" "sort" "strings" - "github.com/pkg/errors" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) @@ -60,40 +64,18 @@ const ( // unknownPriority = 0 ) -// SortPods sorts pods by their role priority -func SortPods(pods []corev1.Pod, rolePriorityMap map[string]int) { - // make a Serial pod list, - // e.g.: unknown -> empty -> learner -> follower1 -> follower2 -> leader, with follower1.Name < follower2.Name - sort.SliceStable(pods, func(i, j int) bool { - roleI := pods[i].Labels[constant.RoleLabelKey] - roleJ := pods[j].Labels[constant.RoleLabelKey] - - if rolePriorityMap[roleI] == rolePriorityMap[roleJ] { - _, ordinal1 := intctrlutil.GetParentNameAndOrdinal(&pods[i]) - _, ordinal2 := intctrlutil.GetParentNameAndOrdinal(&pods[j]) - return ordinal1 < ordinal2 - } - - return rolePriorityMap[roleI] < rolePriorityMap[roleJ] - }) -} - -// generateConsensusUpdatePlan generates Update plan based on UpdateStrategy -func generateConsensusUpdatePlan(ctx context.Context, cli client.Client, stsObj *appsv1.StatefulSet, pods []corev1.Pod, - component appsv1alpha1.ClusterComponentDefinition) *util.Plan { - plan := &util.Plan{} - plan.Start = &util.Step{} - plan.WalkFunc = func(obj interface{}) (bool, error) { +// generateRestartPodPlan generates update plan to restart pods based on UpdateStrategy +func generateRestartPodPlan(ctx context.Context, cli client.Client, stsObj *appsv1.StatefulSet, pods []corev1.Pod, + consensusSpec *appsv1alpha1.ConsensusSetSpec, podsToDelete *[]*corev1.Pod) *util.Plan { + restartPod := func(obj interface{}) (bool, error) { pod, ok := obj.(corev1.Pod) if !ok { return false, errors.New("wrong type: obj not Pod") } - // if DeletionTimestamp is not nil, it is terminating. if pod.DeletionTimestamp != nil { return true, nil } - // if pod is the latest version, we do nothing if intctrlutil.GetPodRevision(&pod) == stsObj.Status.UpdateRevision { // wait until ready @@ -101,26 +83,32 @@ func generateConsensusUpdatePlan(ctx context.Context, cli client.Client, stsObj } // delete the pod to trigger associate StatefulSet to re-create it - if err := cli.Delete(ctx, &pod); err != nil && !apierrors.IsNotFound(err) { - return false, err - } + *podsToDelete = append(*podsToDelete, &pod) return true, nil } + return generateConsensusUpdatePlanLow(ctx, cli, stsObj, pods, consensusSpec, restartPod) +} + +// generateConsensusUpdatePlanLow generates Update plan based on UpdateStrategy +func generateConsensusUpdatePlanLow(ctx context.Context, cli client.Client, stsObj *appsv1.StatefulSet, pods []corev1.Pod, + consensusSpec *appsv1alpha1.ConsensusSetSpec, restartPod func(obj any) (bool, error)) *util.Plan { + plan := &util.Plan{} + plan.Start = &util.Step{} + plan.WalkFunc = restartPod - rolePriorityMap := ComposeRolePriorityMap(component) - SortPods(pods, rolePriorityMap) + rolePriorityMap := ComposeRolePriorityMap(consensusSpec) + util.SortPods(pods, rolePriorityMap, constant.RoleLabelKey) // generate plan by UpdateStrategy - switch component.ConsensusSpec.UpdateStrategy { + switch consensusSpec.UpdateStrategy { case appsv1alpha1.SerialStrategy: - generateConsensusSerialPlan(plan, pods) + generateConsensusSerialPlan(plan, pods, rolePriorityMap) case appsv1alpha1.ParallelStrategy: - generateConsensusParallelPlan(plan, pods) + generateConsensusParallelPlan(plan, pods, rolePriorityMap) case appsv1alpha1.BestEffortParallelStrategy: generateConsensusBestEffortParallelPlan(plan, pods, rolePriorityMap) } - return plan } @@ -181,7 +169,7 @@ func generateConsensusBestEffortParallelPlan(plan *util.Plan, pods []corev1.Pod, } // unknown & empty & leader & followers & learner -func generateConsensusParallelPlan(plan *util.Plan, pods []corev1.Pod) { +func generateConsensusParallelPlan(plan *util.Plan, pods []corev1.Pod, rolePriorityMap map[string]int) { start := plan.Start for _, pod := range pods { nextStep := &util.Step{} @@ -191,7 +179,7 @@ func generateConsensusParallelPlan(plan *util.Plan, pods []corev1.Pod) { } // unknown -> empty -> learner -> followers(none->readonly->readwrite) -> leader -func generateConsensusSerialPlan(plan *util.Plan, pods []corev1.Pod) { +func generateConsensusSerialPlan(plan *util.Plan, pods []corev1.Pod, rolePriorityMap map[string]int) { start := plan.Start for _, pod := range pods { nextStep := &util.Step{} @@ -202,18 +190,17 @@ func generateConsensusSerialPlan(plan *util.Plan, pods []corev1.Pod) { } // ComposeRolePriorityMap generates a priority map based on roles. -func ComposeRolePriorityMap(component appsv1alpha1.ClusterComponentDefinition) map[string]int { - if component.ConsensusSpec == nil { - component.ConsensusSpec = &appsv1alpha1.ConsensusSetSpec{Leader: appsv1alpha1.DefaultLeader} +func ComposeRolePriorityMap(consensusSpec *appsv1alpha1.ConsensusSetSpec) map[string]int { + if consensusSpec == nil { + consensusSpec = appsv1alpha1.NewConsensusSetSpec() } - rolePriorityMap := make(map[string]int, 0) rolePriorityMap[""] = emptyPriority - rolePriorityMap[component.ConsensusSpec.Leader.Name] = leaderPriority - if component.ConsensusSpec.Learner != nil { - rolePriorityMap[component.ConsensusSpec.Learner.Name] = learnerPriority + rolePriorityMap[consensusSpec.Leader.Name] = leaderPriority + if consensusSpec.Learner != nil { + rolePriorityMap[consensusSpec.Learner.Name] = learnerPriority } - for _, follower := range component.ConsensusSpec.Followers { + for _, follower := range consensusSpec.Followers { switch follower.AccessMode { case appsv1alpha1.None: rolePriorityMap[follower.Name] = followerNonePriority @@ -223,7 +210,6 @@ func ComposeRolePriorityMap(component appsv1alpha1.ClusterComponentDefinition) m rolePriorityMap[follower.Name] = followerReadWritePriority } } - return rolePriorityMap } @@ -232,11 +218,18 @@ func UpdateConsensusSetRoleLabel(cli client.Client, reqCtx intctrlutil.RequestCtx, componentDef *appsv1alpha1.ClusterComponentDefinition, pod *corev1.Pod, role string) error { - ctx := reqCtx.Ctx if componentDef == nil { return nil } - roleMap := composeConsensusRoleMap(componentDef) + return updateConsensusSetRoleLabel(cli, reqCtx, componentDef.ConsensusSpec, pod, role) +} + +func updateConsensusSetRoleLabel(cli client.Client, + reqCtx intctrlutil.RequestCtx, + consensusSpec *appsv1alpha1.ConsensusSetSpec, + pod *corev1.Pod, role string) error { + ctx := reqCtx.Ctx + roleMap := composeConsensusRoleMap(consensusSpec) // role not defined in CR, ignore it if _, ok := roleMap[role]; !ok { return nil @@ -267,25 +260,25 @@ func putConsensusMemberExt(roleMap map[string]consensusMemberExt, name string, r roleMap[name] = memberExt } -func composeConsensusRoleMap(componentDef *appsv1alpha1.ClusterComponentDefinition) map[string]consensusMemberExt { +func composeConsensusRoleMap(consensusSpec *appsv1alpha1.ConsensusSetSpec) map[string]consensusMemberExt { roleMap := make(map[string]consensusMemberExt, 0) putConsensusMemberExt(roleMap, - componentDef.ConsensusSpec.Leader.Name, + consensusSpec.Leader.Name, roleLeader, - componentDef.ConsensusSpec.Leader.AccessMode) + consensusSpec.Leader.AccessMode) - for _, follower := range componentDef.ConsensusSpec.Followers { + for _, follower := range consensusSpec.Followers { putConsensusMemberExt(roleMap, follower.Name, roleFollower, follower.AccessMode) } - if componentDef.ConsensusSpec.Learner != nil { + if consensusSpec.Learner != nil { putConsensusMemberExt(roleMap, - componentDef.ConsensusSpec.Learner.Name, + consensusSpec.Learner.Name, roleLearner, - componentDef.ConsensusSpec.Learner.AccessMode) + consensusSpec.Learner.AccessMode) } return roleMap @@ -337,7 +330,7 @@ func setConsensusSetStatusLearner(consensusSetStatus *appsv1alpha1.ConsensusSetS func resetConsensusSetStatusRole(consensusSetStatus *appsv1alpha1.ConsensusSetStatus, podName string) { // reset leader if consensusSetStatus.Leader.Pod == podName { - consensusSetStatus.Leader.Pod = util.ComponentStatusDefaultPodName + consensusSetStatus.Leader.Pod = constant.ComponentStatusDefaultPodName consensusSetStatus.Leader.AccessMode = appsv1alpha1.None consensusSetStatus.Leader.Name = "" } @@ -357,7 +350,7 @@ func resetConsensusSetStatusRole(consensusSetStatus *appsv1alpha1.ConsensusSetSt func setConsensusSetStatusRoles( consensusSetStatus *appsv1alpha1.ConsensusSetStatus, - componentDef *appsv1alpha1.ClusterComponentDefinition, + consensusSpec *appsv1alpha1.ConsensusSetSpec, pods []corev1.Pod) { for _, pod := range pods { if !intctrlutil.PodIsReadyWithLabel(pod) { @@ -365,16 +358,16 @@ func setConsensusSetStatusRoles( } role := pod.Labels[constant.RoleLabelKey] - _ = setConsensusSetStatusRole(consensusSetStatus, componentDef, role, pod.Name) + _ = setConsensusSetStatusRole(consensusSetStatus, consensusSpec, role, pod.Name) } } func setConsensusSetStatusRole( consensusSetStatus *appsv1alpha1.ConsensusSetStatus, - componentDef *appsv1alpha1.ClusterComponentDefinition, + consensusSpec *appsv1alpha1.ConsensusSetSpec, role, podName string) bool { // mapping role label to consensus member - roleMap := composeConsensusRoleMap(componentDef) + roleMap := composeConsensusRoleMap(consensusSpec) memberExt, ok := roleMap[role] if !ok { return false @@ -397,15 +390,61 @@ func setConsensusSetStatusRole( func updateConsensusRoleInfo(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, - componentDef *appsv1alpha1.ClusterComponentDefinition, + consensusSpec *appsv1alpha1.ConsensusSetSpec, componentName string, - pods []corev1.Pod) error { - leader := "" - followers := "" + compDefName string, + pods []corev1.Pod) ([]graph.Vertex, error) { + leader, followers := composeRoleEnv(consensusSpec, pods) + ml := client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.GetName(), + constant.KBAppComponentLabelKey: componentName, + constant.AppConfigTypeLabelKey: "kubeblocks-env", + } + configList := &corev1.ConfigMapList{} + if err := cli.List(ctx, configList, ml); err != nil { + return nil, err + } + + vertexes := make([]graph.Vertex, 0) + for idx := range configList.Items { + config := configList.Items[idx] + config.Data["KB_LEADER"] = leader + config.Data["KB_FOLLOWERS"] = followers + // TODO: need to deprecate 'compDefName' being part of variable name, as it's redundant + // and introduce env/cm key naming reference complexity + config.Data["KB_"+strings.ToUpper(compDefName)+"_LEADER"] = leader + config.Data["KB_"+strings.ToUpper(compDefName)+"_FOLLOWERS"] = followers + vertexes = append(vertexes, &ictrltypes.LifecycleVertex{ + Obj: &config, + Action: ictrltypes.ActionUpdatePtr(), + }) + } + + // patch pods' annotations + for idx := range pods { + pod := pods[idx] + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[constant.LeaderAnnotationKey] = leader + vertexes = append(vertexes, &ictrltypes.LifecycleVertex{ + Obj: &pod, + Action: ictrltypes.ActionUpdatePtr(), + }) + } + + return vertexes, nil +} + +func composeRoleEnv(consensusSpec *appsv1alpha1.ConsensusSetSpec, pods []corev1.Pod) (leader, followers string) { + leader, followers = "", "" for _, pod := range pods { + if !intctrlutil.PodIsReadyWithLabel(pod) { + continue + } role := pod.Labels[constant.RoleLabelKey] // mapping role label to consensus member - roleMap := composeConsensusRoleMap(componentDef) + roleMap := composeConsensusRoleMap(consensusSpec) memberExt, ok := roleMap[role] if !ok { continue @@ -422,39 +461,5 @@ func updateConsensusRoleInfo(ctx context.Context, // TODO: CT } } - - ml := client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.GetName(), - constant.KBAppComponentLabelKey: componentName, - constant.AppConfigTypeLabelKey: "kubeblocks-env", - } - - configList := &corev1.ConfigMapList{} - if err := cli.List(ctx, configList, ml); err != nil { - return err - } - - if len(configList.Items) > 0 { - for _, config := range configList.Items { - patch := client.MergeFrom(config.DeepCopy()) - config.Data["KB_"+strings.ToUpper(componentName)+"_LEADER"] = leader - config.Data["KB_"+strings.ToUpper(componentName)+"_FOLLOWERS"] = followers - if err := cli.Patch(ctx, &config, patch); err != nil { - return err - } - } - } - // patch pods' annotations - for _, pod := range pods { - patch := client.MergeFrom(pod.DeepCopy()) - if pod.Annotations == nil { - pod.Annotations = map[string]string{} - } - pod.Annotations[constant.LeaderAnnotationKey] = leader - if err := cli.Patch(ctx, &pod, patch); err != nil { - return err - } - } - - return nil + return } diff --git a/controllers/apps/components/consensusset/consensus_set_utils_test.go b/controllers/apps/components/consensus/consensus_utils_test.go similarity index 62% rename from controllers/apps/components/consensusset/consensus_set_utils_test.go rename to controllers/apps/components/consensus/consensus_utils_test.go index c2ea47a42..014bd9886 100644 --- a/controllers/apps/components/consensusset/consensus_set_utils_test.go +++ b/controllers/apps/components/consensus/consensus_utils_test.go @@ -1,24 +1,28 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package consensusset +package consensus import ( "strconv" "testing" + "time" "github.com/stretchr/testify/assert" apps "k8s.io/api/apps/v1" @@ -60,15 +64,12 @@ func TestInitClusterComponentStatusIfNeed(t *testing.T) { }, }, } - component := &appsv1alpha1.ClusterComponentDefinition{ - WorkloadType: appsv1alpha1.Consensus, - } - if err := util.InitClusterComponentStatusIfNeed(cluster, componentName, *component); err != nil { + if err := util.InitClusterComponentStatusIfNeed(cluster, componentName, appsv1alpha1.Consensus); err != nil { t.Errorf("caught error %v", err) } if len(cluster.Status.Components) == 0 { - t.Errorf("cluster.Status.ComponentDefs[*] not intialized properly") + t.Errorf("cluster.Status.ComponentDefs[*] not initialized properly") } if _, ok := cluster.Status.Components[componentName]; !ok { t.Errorf("cluster.Status.ComponentDefs[componentName] not initialized properly") @@ -78,7 +79,7 @@ func TestInitClusterComponentStatusIfNeed(t *testing.T) { t.Errorf("cluster.Status.ComponentDefs[componentName].ConsensusSetStatus not initialized properly") } else if consensusSetStatus.Leader.Name != "" || consensusSetStatus.Leader.AccessMode != appsv1alpha1.None || - consensusSetStatus.Leader.Pod != util.ComponentStatusDefaultPodName { + consensusSetStatus.Leader.Pod != constant.ComponentStatusDefaultPodName { t.Errorf("cluster.Status.ComponentDefs[componentName].ConsensusSetStatus.Leader not initialized properly") } } @@ -156,10 +157,53 @@ func TestSortPods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.args.pods = randSort(tt.want) - SortPods(tt.args.pods, tt.args.rolePriorityMap) + util.SortPods(tt.args.pods, tt.args.rolePriorityMap, constant.RoleLabelKey) if !tt.wantErr { assert.Equal(t, tt.args.pods, tt.want) } }) } } + +func TestComposeRoleEnv(t *testing.T) { + componentDef := &appsv1alpha1.ClusterComponentDefinition{ + WorkloadType: appsv1alpha1.Consensus, + ConsensusSpec: &appsv1alpha1.ConsensusSetSpec{ + Leader: appsv1alpha1.ConsensusMember{ + Name: "leader", + AccessMode: appsv1alpha1.ReadWrite, + }, + Followers: []appsv1alpha1.ConsensusMember{ + { + Name: "follower", + AccessMode: appsv1alpha1.Readonly, + }, + }, + }, + } + + set := testk8s.NewFakeStatefulSet("foo", 3) + pods := make([]v1.Pod, 0) + for i := 0; i < 5; i++ { + pod := testk8s.NewFakeStatefulSetPod(set, i) + pod.Status.Conditions = []v1.PodCondition{ + { + Type: v1.PodReady, + Status: v1.ConditionTrue, + }, + } + pod.Labels = map[string]string{constant.RoleLabelKey: "follower"} + pods = append(pods, *pod) + } + pods[0].Labels = map[string]string{constant.RoleLabelKey: "leader"} + leader, followers := composeRoleEnv(componentDef.ConsensusSpec, pods) + assert.Equal(t, "foo-0", leader) + assert.Equal(t, "foo-1,foo-2,foo-3,foo-4", followers) + + dt := time.Now() + pods[3].DeletionTimestamp = &metav1.Time{Time: dt} + pods[4].DeletionTimestamp = &metav1.Time{Time: dt} + leader, followers = composeRoleEnv(componentDef.ConsensusSpec, pods) + assert.Equal(t, "foo-0", leader) + assert.Equal(t, "foo-1,foo-2", followers) +} diff --git a/controllers/apps/components/consensusset/suite_test.go b/controllers/apps/components/consensus/suite_test.go similarity index 73% rename from controllers/apps/components/consensusset/suite_test.go rename to controllers/apps/components/consensus/suite_test.go index e48fe879e..78161f968 100644 --- a/controllers/apps/components/consensusset/suite_test.go +++ b/controllers/apps/components/consensus/suite_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package consensusset +package consensus import ( "context" @@ -55,7 +58,7 @@ func init() { func TestAPIs(t *testing.T) { RegisterFailHandler(Fail) - RunSpecs(t, "ConsensusSet Controller Suite") + RunSpecs(t, "Consensus Controller Suite") } var _ = BeforeSuite(func() { diff --git a/controllers/apps/components/consensusset/consensus_set.go b/controllers/apps/components/consensusset/consensus_set.go deleted file mode 100644 index eb1022a82..000000000 --- a/controllers/apps/components/consensusset/consensus_set.go +++ /dev/null @@ -1,278 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 consensusset - -import ( - "context" - - "github.com/google/go-cmp/cmp" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -type ConsensusSet struct { - Cli client.Client - Cluster *appsv1alpha1.Cluster - Component *appsv1alpha1.ClusterComponentSpec - componentDef *appsv1alpha1.ClusterComponentDefinition -} - -var _ types.Component = &ConsensusSet{} - -func (r *ConsensusSet) IsRunning(ctx context.Context, obj client.Object) (bool, error) { - if obj == nil { - return false, nil - } - sts := util.ConvertToStatefulSet(obj) - isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, r.Cli, sts) - if err != nil { - return false, err - } - pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, sts) - if err != nil { - return false, err - } - for _, pod := range pods { - if !intctrlutil.PodIsReadyWithLabel(pod) { - return false, nil - } - } - - return util.StatefulSetOfComponentIsReady(sts, isRevisionConsistent, &r.Component.Replicas), nil -} - -func (r *ConsensusSet) PodsReady(ctx context.Context, obj client.Object) (bool, error) { - if obj == nil { - return false, nil - } - sts := util.ConvertToStatefulSet(obj) - return util.StatefulSetPodsAreReady(sts, r.Component.Replicas), nil -} - -func (r *ConsensusSet) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { - if pod == nil { - return false - } - return intctrlutil.PodIsReadyWithLabel(*pod) -} - -func (r *ConsensusSet) HandleProbeTimeoutWhenPodsReady(ctx context.Context, recorder record.EventRecorder) (bool, error) { - var ( - compStatus appsv1alpha1.ClusterComponentStatus - ok bool - cluster = r.Cluster - componentName = r.Component.Name - ) - if len(cluster.Status.Components) == 0 { - return true, nil - } - if compStatus, ok = cluster.Status.Components[componentName]; !ok { - return true, nil - } - if compStatus.PodsReadyTime == nil { - return true, nil - } - if !util.IsProbeTimeout(r.componentDef, compStatus.PodsReadyTime) { - return true, nil - } - - podList, err := util.GetComponentPodList(ctx, r.Cli, *cluster, componentName) - if err != nil { - return true, err - } - var ( - isAbnormal bool - needPatch bool - isFailed = true - ) - patch := client.MergeFrom(cluster.DeepCopy()) - for _, pod := range podList.Items { - role := pod.Labels[constant.RoleLabelKey] - if role == r.componentDef.ConsensusSpec.Leader.Name { - isFailed = false - } - if role == "" { - isAbnormal = true - compStatus.SetObjectMessage(pod.Kind, pod.Name, "Role probe timeout, check whether the application is available") - needPatch = true - } - // TODO clear up the message of ready pod in component.message. - } - if !needPatch { - return true, nil - } - if isFailed { - compStatus.Phase = appsv1alpha1.FailedClusterCompPhase - } else if isAbnormal { - compStatus.Phase = appsv1alpha1.AbnormalClusterCompPhase - } - cluster.Status.SetComponentStatus(componentName, compStatus) - if err = r.Cli.Status().Patch(ctx, cluster, patch); err != nil { - return false, err - } - if recorder != nil { - recorder.Eventf(cluster, corev1.EventTypeWarning, types.RoleProbeTimeoutReason, "pod role detection timed out in Component: "+r.Component.Name) - } - // when component status changed, mark OpsRequest to reconcile. - return false, opsutil.MarkRunningOpsRequestAnnotation(ctx, r.Cli, cluster) -} - -func (r *ConsensusSet) GetPhaseWhenPodsNotReady(ctx context.Context, - componentName string) (appsv1alpha1.ClusterComponentPhase, error) { - stsList := &appsv1.StatefulSetList{} - podList, err := util.GetCompRelatedObjectList(ctx, r.Cli, *r.Cluster, - componentName, stsList) - if err != nil || len(stsList.Items) == 0 { - return "", err - } - stsObj := stsList.Items[0] - podCount := len(podList.Items) - componentReplicas := r.Component.Replicas - if podCount == 0 || stsObj.Status.AvailableReplicas == 0 { - return util.GetPhaseWithNoAvailableReplicas(componentReplicas), nil - } - // get the statefulSet of component - var ( - existLatestRevisionFailedPod bool - leaderIsReady bool - consensusSpec = r.componentDef.ConsensusSpec - ) - for _, v := range podList.Items { - // if the pod is terminating, ignore it - if v.DeletionTimestamp != nil { - return "", nil - } - labelValue := v.Labels[constant.RoleLabelKey] - if consensusSpec != nil && labelValue == consensusSpec.Leader.Name && intctrlutil.PodIsReady(&v) { - leaderIsReady = true - continue - } - if !intctrlutil.PodIsReady(&v) && intctrlutil.PodIsControlledByLatestRevision(&v, &stsObj) { - existLatestRevisionFailedPod = true - } - } - return util.GetCompPhaseByConditions(existLatestRevisionFailedPod, leaderIsReady, - componentReplicas, int32(podCount), stsObj.Status.AvailableReplicas), nil -} - -func (r *ConsensusSet) HandleUpdate(ctx context.Context, obj client.Object) error { - if r == nil { - return nil - } - - stsObj := util.ConvertToStatefulSet(obj) - // get compDefName from stsObj.name - compDefName := r.Cluster.Spec.GetComponentDefRefName(stsObj.Labels[constant.KBAppComponentLabelKey]) - - // get component from ClusterDefinition by compDefName - component, err := util.GetComponentDefByCluster(ctx, r.Cli, *r.Cluster, compDefName) - if err != nil { - return err - } - - if component == nil || component.WorkloadType != appsv1alpha1.Consensus { - return nil - } - pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, stsObj) - if err != nil { - return err - } - - // update cluster.status.component.consensusSetStatus based on all pods currently exist - componentName := stsObj.Labels[constant.KBAppComponentLabelKey] - - // first, get the old status - var oldConsensusSetStatus *appsv1alpha1.ConsensusSetStatus - if v, ok := r.Cluster.Status.Components[componentName]; ok { - oldConsensusSetStatus = v.ConsensusSetStatus - } - // create the initial status - newConsensusSetStatus := &appsv1alpha1.ConsensusSetStatus{ - Leader: appsv1alpha1.ConsensusMemberStatus{ - Name: "", - Pod: util.ComponentStatusDefaultPodName, - AccessMode: appsv1alpha1.None, - }, - } - // then, calculate the new status - setConsensusSetStatusRoles(newConsensusSetStatus, component, pods) - // if status changed, do update - if !cmp.Equal(newConsensusSetStatus, oldConsensusSetStatus) { - patch := client.MergeFrom((*r.Cluster).DeepCopy()) - if err = util.InitClusterComponentStatusIfNeed(r.Cluster, componentName, *component); err != nil { - return err - } - componentStatus := r.Cluster.Status.Components[componentName] - componentStatus.ConsensusSetStatus = newConsensusSetStatus - r.Cluster.Status.SetComponentStatus(componentName, componentStatus) - if err = r.Cli.Status().Patch(ctx, r.Cluster, patch); err != nil { - return err - } - // add consensus role info to pod env - if err := updateConsensusRoleInfo(ctx, r.Cli, r.Cluster, component, componentName, pods); err != nil { - return err - } - } - - // prepare to do pods Deletion, that's the only thing we should do, - // the statefulset reconciler will do the others. - // to simplify the process, we do pods Deletion after statefulset reconcile done, - // that is stsObj.Generation == stsObj.Status.ObservedGeneration - if stsObj.Generation != stsObj.Status.ObservedGeneration { - return nil - } - - // then we wait all pods' presence, that is len(pods) == stsObj.Spec.Replicas - // only then, we have enough info about the previous pods before delete the current one - if len(pods) != int(*stsObj.Spec.Replicas) { - return nil - } - - // we don't check whether pod role label present: prefer stateful set's Update done than role probing ready - - // generate the pods Deletion plan - plan := generateConsensusUpdatePlan(ctx, r.Cli, stsObj, pods, *component) - // execute plan - if _, err := plan.WalkOneStep(); err != nil { - return err - } - return nil -} -func NewConsensusSet( - cli client.Client, - cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec, - componentDef appsv1alpha1.ClusterComponentDefinition) (*ConsensusSet, error) { - if err := util.ComponentRuntimeReqArgsCheck(cli, cluster, component); err != nil { - return nil, err - } - return &ConsensusSet{ - Cli: cli, - Cluster: cluster, - Component: component, - componentDef: &componentDef, - }, nil -} diff --git a/controllers/apps/components/deployment_controller.go b/controllers/apps/components/deployment_controller.go deleted file mode 100644 index 47fe745a3..000000000 --- a/controllers/apps/components/deployment_controller.go +++ /dev/null @@ -1,93 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "context" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// DeploymentReconciler reconciles a deployment object -type DeploymentReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder -} - -// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps,resources=deployments/status,verbs=get -// +kubebuilder:rbac:groups=apps,resources=deployments/finalizers,verbs=update - -// 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.14.4/pkg/reconcile -func (r *DeploymentReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - var ( - deploy = &appsv1.Deployment{} - err error - ) - - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Req: req, - Log: log.FromContext(ctx).WithValues("deployment", req.NamespacedName), - } - - if err = r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, deploy); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - - return workloadCompClusterReconcile(reqCtx, r.Client, deploy, - func(cluster *appsv1alpha1.Cluster, componentSpec *appsv1alpha1.ClusterComponentSpec, component types.Component) (ctrl.Result, error) { - compCtx := newComponentContext(reqCtx, r.Client, r.Recorder, component, deploy, componentSpec) - // patch the current componentSpec workload's custom labels - if err := patchWorkloadCustomLabel(reqCtx.Ctx, r.Client, cluster, componentSpec); err != nil { - reqCtx.Recorder.Event(cluster, corev1.EventTypeWarning, "Deployment Controller PatchWorkloadCustomLabelFailed", err.Error()) - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - if requeueAfter, err := updateComponentStatusInClusterStatus(compCtx, cluster); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } else if requeueAfter != 0 { - // if the reconcileAction need requeue, do it - return intctrlutil.RequeueAfter(requeueAfter, reqCtx.Log, "") - } - return intctrlutil.Reconciled() - }) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *DeploymentReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appsv1.Deployment{}). - Owns(&appsv1.ReplicaSet{}). - WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.WorkloadFilterPredicate)). - Complete(r) -} diff --git a/controllers/apps/components/deployment_controller_test.go b/controllers/apps/components/deployment_controller_test.go deleted file mode 100644 index 71fe5c967..000000000 --- a/controllers/apps/components/deployment_controller_test.go +++ /dev/null @@ -1,147 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/stateless" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" - testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" -) - -var _ = Describe("Deployment Controller", func() { - var ( - randomStr = testCtx.GetRandomStr() - clusterDefName = "stateless-definition1-" + randomStr - clusterVersionName = "stateless-cluster-version1-" + randomStr - clusterName = "stateless1-" + randomStr - ) - - const ( - namespace = "default" - statelessCompName = "stateless" - statelessCompType = "stateless" - ) - - cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - - // delete cluster(and all dependent sub-resources), clusterversion and clusterdef - testapps.ClearClusterResources(&testCtx) - - // clear rest resources - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.DeploymentSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) - } - - BeforeEach(cleanAll) - - AfterEach(cleanAll) - - Context("test controller", func() { - It("", func() { - testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatelessNginxComponent, statelessCompType). - Create(&testCtx).GetObject() - - cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(statelessCompName, statelessCompType).SetReplicas(2).Create(&testCtx).GetObject() - - By("patch cluster to Running") - Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { - cluster.Status.Phase = appsv1alpha1.RunningClusterPhase - })) - - By("create the deployment of the stateless component") - deploy := testapps.MockStatelessComponentDeploy(testCtx, clusterName, statelessCompName) - newDeploymentKey := client.ObjectKey{Name: deploy.Name, Namespace: namespace} - Eventually(testapps.CheckObj(&testCtx, newDeploymentKey, func(g Gomega, deploy *appsv1.Deployment) { - g.Expect(deploy.Generation == 1).Should(BeTrue()) - })).Should(Succeed()) - - By("check stateless component phase is Failed") - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, statelessCompName)).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) - - By("mock error message and PodCondition about some pod's failure") - podName := fmt.Sprintf("%s-%s-%s", clusterName, statelessCompName, testCtx.GetRandomStr()) - pod := testapps.MockStatelessPod(testCtx, deploy, clusterName, statelessCompName, podName) - // mock pod container is failed - errMessage := "Back-off pulling image nginx:latest" - Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { - pod.Status.ContainerStatuses = []corev1.ContainerStatus{ - { - State: corev1.ContainerState{ - Waiting: &corev1.ContainerStateWaiting{ - Reason: "ImagePullBackOff", - Message: errMessage, - }, - }, - }, - } - })).Should(Succeed()) - // mock failed container timed out - Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { - pod.Status.Conditions = []corev1.PodCondition{ - { - Type: corev1.ContainersReady, - Status: corev1.ConditionFalse, - LastTransitionTime: metav1.NewTime(time.Now().Add(-2 * time.Minute)), - }, - } - })).Should(Succeed()) - // mark deployment to reconcile - Expect(testapps.ChangeObj(&testCtx, deploy, func() { - deploy.Annotations = map[string]string{ - "reconcile": "1", - } - })).Should(Succeed()) - - By("check component.Status.Message contains pod error message") - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { - compStatus := tmpCluster.Status.Components[statelessCompName] - g.Expect(compStatus.GetObjectMessage("Pod", pod.Name)).Should(Equal(errMessage)) - })).Should(Succeed()) - - By("mock deployment is ready") - Expect(testapps.ChangeObjStatus(&testCtx, deploy, func() { - testk8s.MockDeploymentReady(deploy, stateless.NewRSAvailableReason, deploy.Name+"-5847cb795c") - })).Should(Succeed()) - - By("waiting for the component to be running") - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, statelessCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) - }) - }) -}) diff --git a/controllers/apps/components/internal/component_base.go b/controllers/apps/components/internal/component_base.go new file mode 100644 index 000000000..9d17628e3 --- /dev/null +++ b/controllers/apps/components/internal/component_base.go @@ -0,0 +1,652 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package internal + +import ( + "context" + "fmt" + "reflect" + "strconv" + "time" + + "golang.org/x/exp/slices" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + k8sscheme "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" +) + +type ComponentBase struct { + Client client.Client + Recorder record.EventRecorder + Cluster *appsv1alpha1.Cluster + ClusterVersion *appsv1alpha1.ClusterVersion // building config needs the cluster version + Component *component.SynthesizedComponent // built synthesized component, replace it with component workload proto + ComponentSet types.ComponentSet + Dag *graph.DAG + WorkloadVertex *ictrltypes.LifecycleVertex // DAG vertex of main workload object +} + +func (c *ComponentBase) GetName() string { + return c.Component.Name +} + +func (c *ComponentBase) GetNamespace() string { + return c.Cluster.Namespace +} + +func (c *ComponentBase) GetClusterName() string { + return c.Cluster.Name +} + +func (c *ComponentBase) GetDefinitionName() string { + return c.Component.ComponentDef +} + +func (c *ComponentBase) GetCluster() *appsv1alpha1.Cluster { + return c.Cluster +} + +func (c *ComponentBase) GetClusterVersion() *appsv1alpha1.ClusterVersion { + return c.ClusterVersion +} + +func (c *ComponentBase) GetSynthesizedComponent() *component.SynthesizedComponent { + return c.Component +} + +func (c *ComponentBase) GetMatchingLabels() client.MatchingLabels { + return client.MatchingLabels{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: c.GetClusterName(), + constant.KBAppComponentLabelKey: c.GetName(), + } +} + +func (c *ComponentBase) GetReplicas() int32 { + return c.Component.Replicas +} + +func (c *ComponentBase) GetConsensusSpec() *appsv1alpha1.ConsensusSetSpec { + return c.Component.ConsensusSpec +} + +func (c *ComponentBase) GetPrimaryIndex() int32 { + if c.Component.PrimaryIndex == nil { + return 0 + } + return *c.Component.PrimaryIndex +} + +func (c *ComponentBase) GetPhase() appsv1alpha1.ClusterComponentPhase { + if c.Cluster.Status.Components == nil { + return "" + } + if _, ok := c.Cluster.Status.Components[c.GetName()]; !ok { + return "" + } + return c.Cluster.Status.Components[c.GetName()].Phase +} + +func (c *ComponentBase) SetWorkload(obj client.Object, action *ictrltypes.LifecycleAction, parent *ictrltypes.LifecycleVertex) { + c.WorkloadVertex = c.AddResource(obj, action, parent) +} + +func (c *ComponentBase) AddResource(obj client.Object, action *ictrltypes.LifecycleAction, + parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + if obj == nil { + panic("try to add nil object") + } + vertex := &ictrltypes.LifecycleVertex{ + Obj: obj, + Action: action, + } + c.Dag.AddVertex(vertex) + + if parent != nil { + c.Dag.Connect(parent, vertex) + } + return vertex +} + +func (c *ComponentBase) CreateResource(obj client.Object, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + return ictrltypes.LifecycleObjectCreate(c.Dag, obj, parent) +} + +func (c *ComponentBase) DeleteResource(obj client.Object, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + return ictrltypes.LifecycleObjectDelete(c.Dag, obj, parent) +} + +func (c *ComponentBase) UpdateResource(obj client.Object, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + return ictrltypes.LifecycleObjectUpdate(c.Dag, obj, parent) +} + +func (c *ComponentBase) PatchResource(obj client.Object, objCopy client.Object, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + return ictrltypes.LifecycleObjectPatch(c.Dag, obj, objCopy, parent) +} + +func (c *ComponentBase) NoopResource(obj client.Object, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex { + return ictrltypes.LifecycleObjectNoop(c.Dag, obj, parent) +} + +// ValidateObjectsAction validates the action of objects in dag has been determined. +func (c *ComponentBase) ValidateObjectsAction() error { + for _, v := range c.Dag.Vertices() { + node, ok := v.(*ictrltypes.LifecycleVertex) + if !ok { + return fmt.Errorf("unexpected vertex type, cluster: %s, component: %s, vertex: %T", + c.GetClusterName(), c.GetName(), v) + } + if node.Obj == nil { + return fmt.Errorf("unexpected nil vertex object, cluster: %s, component: %s, vertex: %T", + c.GetClusterName(), c.GetName(), v) + } + if node.Action == nil { + return fmt.Errorf("unexpected nil vertex action, cluster: %s, component: %s, vertex: %T", + c.GetClusterName(), c.GetName(), v) + } + } + return nil +} + +// ResolveObjectsAction resolves the action of objects in dag to guarantee that all object actions will be determined. +func (c *ComponentBase) ResolveObjectsAction(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + snapshot, err := readCacheSnapshot(reqCtx, cli, c.GetCluster()) + if err != nil { + return err + } + for _, v := range c.Dag.Vertices() { + node, ok := v.(*ictrltypes.LifecycleVertex) + if !ok { + return fmt.Errorf("unexpected vertex type, cluster: %s, component: %s, vertex: %T", + c.GetClusterName(), c.GetName(), v) + } + if node.Action == nil { + if action, err := resolveObjectAction(snapshot, node); err != nil { + return err + } else { + node.Action = action + } + } + } + if c.GetCluster().IsStatusUpdating() { + for _, vertex := range c.Dag.Vertices() { + v, _ := vertex.(*ictrltypes.LifecycleVertex) + // TODO(refactor): fix me, this is a workaround for h-scaling to update stateful set. + if _, ok := v.Obj.(*appsv1.StatefulSet); !ok { + v.Immutable = true + } + } + } + return c.ValidateObjectsAction() +} + +func (c *ComponentBase) UpdatePDB(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + pdbObjList, err := util.ListObjWithLabelsInNamespace(reqCtx.Ctx, cli, generics.PodDisruptionBudgetSignature, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + for _, v := range ictrltypes.FindAll[*policyv1.PodDisruptionBudget](c.Dag) { + node := v.(*ictrltypes.LifecycleVertex) + pdbProto := node.Obj.(*policyv1.PodDisruptionBudget) + + if pos := slices.IndexFunc(pdbObjList, func(pdbObj *policyv1.PodDisruptionBudget) bool { + return pdbObj.GetName() == pdbProto.GetName() + }); pos < 0 { + node.Action = ictrltypes.ActionCreatePtr() // TODO: Create or Noop? + } else { + pdbObj := pdbObjList[pos] + if !reflect.DeepEqual(pdbObj.Spec, pdbProto.Spec) { + pdbObj.Spec = pdbProto.Spec + node.Obj = pdbObj + node.Action = ictrltypes.ActionUpdatePtr() + } + } + } + return nil +} + +func (c *ComponentBase) UpdateService(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + svcObjList, err := util.ListObjWithLabelsInNamespace(reqCtx.Ctx, cli, generics.ServiceSignature, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return client.IgnoreNotFound(err) + } + + svcProtoList := ictrltypes.FindAll[*corev1.Service](c.Dag) + + // create new services or update existing services + for _, vertex := range svcProtoList { + node, _ := vertex.(*ictrltypes.LifecycleVertex) + svcProto, _ := node.Obj.(*corev1.Service) + + if pos := slices.IndexFunc(svcObjList, func(svc *corev1.Service) bool { + return svc.GetName() == svcProto.GetName() + }); pos < 0 { + node.Action = ictrltypes.ActionCreatePtr() + } else { + svcProto.Annotations = util.MergeServiceAnnotations(svcObjList[pos].Annotations, svcProto.Annotations) + node.Action = ictrltypes.ActionUpdatePtr() + } + } + + // delete useless services + for _, svc := range svcObjList { + if pos := slices.IndexFunc(svcProtoList, func(vertex graph.Vertex) bool { + node, _ := vertex.(*ictrltypes.LifecycleVertex) + svcProto, _ := node.Obj.(*corev1.Service) + return svcProto.GetName() == svc.GetName() + }); pos < 0 { + c.DeleteResource(svc, nil) + } + } + return nil +} + +// SetStatusPhase sets the cluster component phase and messages conditionally. +func (c *ComponentBase) SetStatusPhase(phase appsv1alpha1.ClusterComponentPhase, + statusMessage appsv1alpha1.ComponentMessageMap, phaseTransitionMsg string) { + updatefn := func(status *appsv1alpha1.ClusterComponentStatus) error { + if status.Phase == phase { + return nil + } + status.Phase = phase + if status.Message == nil { + status.Message = statusMessage + } else { + for k, v := range statusMessage { + status.Message[k] = v + } + } + return nil + } + if err := c.updateStatus(phaseTransitionMsg, updatefn); err != nil { + panic(fmt.Sprintf("unexpected error occurred while updating component status: %s", err.Error())) + } +} + +func (c *ComponentBase) StatusWorkload(reqCtx intctrlutil.RequestCtx, cli client.Client, obj client.Object, txn *statusReconciliationTxn) error { + // if reflect.ValueOf(obj).Kind() == reflect.Ptr && reflect.ValueOf(obj).IsNil() { + // return nil + // } + + pods, err := util.ListPodOwnedByComponent(reqCtx.Ctx, cli, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return err + } + + isRunning, err := c.ComponentSet.IsRunning(reqCtx.Ctx, obj) + if err != nil { + return err + + } + + var podsReady *bool + if c.Component.Replicas > 0 { + podsReadyForComponent, err := c.ComponentSet.PodsReady(reqCtx.Ctx, obj) + if err != nil { + return err + } + podsReady = &podsReadyForComponent + } + + hasFailedPodTimedOut := false + timedOutPodStatusMessage := appsv1alpha1.ComponentMessageMap{} + var requeueAfter time.Duration + clusterGenerationFromWorkload := obj.GetAnnotations()[constant.KubeBlocksGenerationKey] + // check if it is the latest obj after cluster does updates. + if !isRunning && !appsv1alpha1.ComponentPodsAreReady(podsReady) && + clusterGenerationFromWorkload == strconv.FormatInt(c.Cluster.Generation, 10) { + hasFailedPodTimedOut, timedOutPodStatusMessage, requeueAfter = hasFailedAndTimedOutPod(pods) + } + + phase, statusMessage, err := c.buildStatus(reqCtx.Ctx, pods, isRunning, podsReady, hasFailedPodTimedOut, timedOutPodStatusMessage) + if err != nil { + return err + } + + phaseTransitionCondMsg := "" + if podsReady == nil { + phaseTransitionCondMsg = fmt.Sprintf("Running: %v, PodsReady: nil, PodsTimedout: %v", isRunning, hasFailedPodTimedOut) + } else { + phaseTransitionCondMsg = fmt.Sprintf("Running: %v, PodsReady: %v, PodsTimedout: %v", isRunning, *podsReady, hasFailedPodTimedOut) + } + + updatefn := func(status *appsv1alpha1.ClusterComponentStatus) error { + if phase != "" { + status.Phase = phase + } + status.SetMessage(statusMessage) + if !appsv1alpha1.ComponentPodsAreReady(podsReady) { + status.PodsReadyTime = nil + } else if !appsv1alpha1.ComponentPodsAreReady(status.PodsReady) { + // set podsReadyTime when pods of component are ready at the moment. + status.PodsReadyTime = &metav1.Time{Time: time.Now()} + } + status.PodsReady = podsReady + return nil + } + + if txn != nil { + txn.propose(phase, func() { + if err = c.updateStatus(phaseTransitionCondMsg, updatefn); err != nil { + panic(fmt.Sprintf("unexpected error occurred while updating component status: %s", err.Error())) + } + }) + if requeueAfter != 0 { + return intctrlutil.NewDelayedRequeueError(requeueAfter, "requeue for workload status to reconcile.") + } + return nil + } + // TODO(refactor): wait = true to requeue. + if err = c.updateStatus(phaseTransitionCondMsg, updatefn); err != nil { + return err + } + if requeueAfter != 0 { + return intctrlutil.NewDelayedRequeueError(requeueAfter, "requeue for workload status to reconcile.") + } + return nil +} + +func (c *ComponentBase) buildStatus(ctx context.Context, pods []*corev1.Pod, isRunning bool, podsReady *bool, + hasFailedPodTimedOut bool, timedOutPodStatusMessage appsv1alpha1.ComponentMessageMap) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) { + var ( + err error + phase appsv1alpha1.ClusterComponentPhase + statusMessage appsv1alpha1.ComponentMessageMap + ) + if isRunning { + if c.Component.Replicas == 0 { + // if replicas number of component is zero, the component has stopped. + // 'Stopped' is a special 'Running' status for workload(StatefulSet/Deployment). + phase = appsv1alpha1.StoppedClusterCompPhase + } else { + // change component phase to Running when workloads of component are running. + phase = appsv1alpha1.RunningClusterCompPhase + } + return phase, statusMessage, nil + } + + if appsv1alpha1.ComponentPodsAreReady(podsReady) { + // check if the role probe timed out when component phase is not Running but all pods of component are ready. + phase, statusMessage = c.ComponentSet.GetPhaseWhenPodsReadyAndProbeTimeout(pods) + // if component is not running and probe is not timed out, requeue. + if phase == "" { + return phase, statusMessage, intctrlutil.NewDelayedRequeueError(time.Second*30, "wait for probe timed out") + } + return phase, statusMessage, nil + } + + // get the phase if failed pods have timed out or the pods are not running when there are no changes to the component. + if hasFailedPodTimedOut || slices.Contains(appsv1alpha1.GetComponentUpRunningPhase(), c.GetPhase()) { + phase, statusMessage, err = c.ComponentSet.GetPhaseWhenPodsNotReady(ctx, c.GetName()) + if err != nil { + return "", nil, err + } + } + if statusMessage == nil { + statusMessage = timedOutPodStatusMessage + } else { + for k, v := range timedOutPodStatusMessage { + statusMessage[k] = v + } + } + return phase, statusMessage, nil +} + +// updateStatus updates the cluster component status by @updatefn, with additional message to explain the transition occurred. +func (c *ComponentBase) updateStatus(phaseTransitionMsg string, updatefn func(status *appsv1alpha1.ClusterComponentStatus) error) error { + if updatefn == nil { + return nil + } + + if c.Cluster.Status.Components == nil { + c.Cluster.Status.Components = make(map[string]appsv1alpha1.ClusterComponentStatus) + } + if _, ok := c.Cluster.Status.Components[c.GetName()]; !ok { + c.Cluster.Status.Components[c.GetName()] = appsv1alpha1.ClusterComponentStatus{} + } + + status := c.Cluster.Status.Components[c.GetName()] + phase := status.Phase + err := updatefn(&status) + if err != nil { + return err + } + c.Cluster.Status.Components[c.GetName()] = status + + if phase != status.Phase { + // TODO: logging the event + if c.Recorder != nil { + c.Recorder.Eventf(c.Cluster, corev1.EventTypeNormal, types.ComponentPhaseTransition, phaseTransitionMsg) + } + } + + return nil +} + +// hasFailedAndTimedOutPod returns whether the pods of components are still failed after a PodFailedTimeout period. +// if return true, component phase will be set to Failed/Abnormal. +func hasFailedAndTimedOutPod(pods []*corev1.Pod) (bool, appsv1alpha1.ComponentMessageMap, time.Duration) { + var ( + hasTimedOutPod bool + messages = appsv1alpha1.ComponentMessageMap{} + hasFailedPod bool + requeueAfter time.Duration + ) + for _, pod := range pods { + isFailed, isTimedOut, messageStr := isPodFailedAndTimedOut(pod) + if !isFailed { + continue + } + if isTimedOut { + hasTimedOutPod = true + messages.SetObjectMessage(pod.Kind, pod.Name, messageStr) + } else { + hasFailedPod = true + } + } + if hasFailedPod && !hasTimedOutPod { + requeueAfter = time.Second * 30 + } + return hasTimedOutPod, messages, requeueAfter +} + +// isPodScheduledFailedAndTimedOut checks whether the unscheduled pod has timed out. +func isPodScheduledFailedAndTimedOut(pod *corev1.Pod) (bool, bool, string) { + for _, cond := range pod.Status.Conditions { + if cond.Type != corev1.PodScheduled { + continue + } + if cond.Status == corev1.ConditionTrue { + return false, false, "" + } + return true, time.Now().After(cond.LastTransitionTime.Add(types.PodScheduledFailedTimeout)), cond.Message + } + return false, false, "" +} + +// isPodFailedAndTimedOut checks if the pod is failed and timed out. +func isPodFailedAndTimedOut(pod *corev1.Pod) (bool, bool, string) { + if isFailed, isTimedOut, message := isPodScheduledFailedAndTimedOut(pod); isFailed { + return isFailed, isTimedOut, message + } + initContainerFailed, message := isAnyContainerFailed(pod.Status.InitContainerStatuses) + if initContainerFailed { + return initContainerFailed, isContainerFailedAndTimedOut(pod, corev1.PodInitialized), message + } + containerFailed, message := isAnyContainerFailed(pod.Status.ContainerStatuses) + if containerFailed { + return containerFailed, isContainerFailedAndTimedOut(pod, corev1.ContainersReady), message + } + return false, false, "" +} + +// isAnyContainerFailed checks whether any container in the list is failed. +func isAnyContainerFailed(containersStatus []corev1.ContainerStatus) (bool, string) { + for _, v := range containersStatus { + waitingState := v.State.Waiting + if waitingState != nil && waitingState.Message != "" { + return true, waitingState.Message + } + terminatedState := v.State.Terminated + if terminatedState != nil && terminatedState.Message != "" { + return true, terminatedState.Message + } + } + return false, "" +} + +// isContainerFailedAndTimedOut checks whether the failed container has timed out. +func isContainerFailedAndTimedOut(pod *corev1.Pod, podConditionType corev1.PodConditionType) bool { + containerReadyCondition := intctrlutil.GetPodCondition(&pod.Status, podConditionType) + if containerReadyCondition == nil || containerReadyCondition.LastTransitionTime.IsZero() { + return false + } + return time.Now().After(containerReadyCondition.LastTransitionTime.Add(types.PodContainerFailedTimeout)) +} + +type gvkName struct { + gvk schema.GroupVersionKind + ns, name string +} + +type clusterSnapshot map[gvkName]client.Object + +func getGVKName(object client.Object, scheme *runtime.Scheme) (*gvkName, error) { + gvk, err := apiutil.GVKForObject(object, scheme) + if err != nil { + return nil, err + } + return &gvkName{ + gvk: gvk, + ns: object.GetNamespace(), + name: object.GetName(), + }, nil +} + +func isOwnerOf(owner, obj client.Object, scheme *runtime.Scheme) bool { + ro, ok := owner.(runtime.Object) + if !ok { + return false + } + gvk, err := apiutil.GVKForObject(ro, scheme) + if err != nil { + return false + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + UID: owner.GetUID(), + Name: owner.GetName(), + } + owners := obj.GetOwnerReferences() + referSameObject := func(a, b metav1.OwnerReference) bool { + aGV, err := schema.ParseGroupVersion(a.APIVersion) + if err != nil { + return false + } + + bGV, err := schema.ParseGroupVersion(b.APIVersion) + if err != nil { + return false + } + + return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name + } + for _, ownerRef := range owners { + if referSameObject(ownerRef, ref) { + return true + } + } + return false +} + +func ownedKinds() []client.ObjectList { + return []client.ObjectList{ + &appsv1.StatefulSetList{}, + &appsv1.DeploymentList{}, + &corev1.ServiceList{}, + &corev1.SecretList{}, + &corev1.ConfigMapList{}, + &corev1.PersistentVolumeClaimList{}, // TODO(merge): remove it? + &policyv1.PodDisruptionBudgetList{}, + &dataprotectionv1alpha1.BackupPolicyList{}, + } +} + +// read all objects owned by component +func readCacheSnapshot(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster *appsv1alpha1.Cluster) (clusterSnapshot, error) { + // list what kinds of object cluster owns + kinds := ownedKinds() + snapshot := make(clusterSnapshot) + ml := client.MatchingLabels{constant.AppInstanceLabelKey: cluster.GetName()} + inNS := client.InNamespace(cluster.Namespace) + for _, list := range kinds { + if err := cli.List(reqCtx.Ctx, list, inNS, ml); err != nil { + return nil, err + } + // reflect get list.Items + items := reflect.ValueOf(list).Elem().FieldByName("Items") + l := items.Len() + for i := 0; i < l; i++ { + // get the underlying object + object := items.Index(i).Addr().Interface().(client.Object) + // put to snapshot if owned by our cluster + if isOwnerOf(cluster, object, k8sscheme.Scheme) { + name, err := getGVKName(object, k8sscheme.Scheme) + if err != nil { + return nil, err + } + snapshot[*name] = object + } + } + } + return snapshot, nil +} + +func resolveObjectAction(snapshot clusterSnapshot, vertex *ictrltypes.LifecycleVertex) (*ictrltypes.LifecycleAction, error) { + gvk, err := getGVKName(vertex.Obj, k8sscheme.Scheme) + if err != nil { + return nil, err + } + if _, ok := snapshot[*gvk]; ok { + return ictrltypes.ActionNoopPtr(), nil + } else { + return ictrltypes.ActionCreatePtr(), nil + } +} diff --git a/controllers/apps/components/internal/component_base_stateful.go b/controllers/apps/components/internal/component_base_stateful.go new file mode 100644 index 000000000..4b35bf07d --- /dev/null +++ b/controllers/apps/components/internal/component_base_stateful.go @@ -0,0 +1,882 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package internal + +import ( + "encoding/json" + "fmt" + "reflect" + "strconv" + "strings" + + "github.com/spf13/viper" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" +) + +// StatefulComponentBase as a base class for single stateful-set based component (stateful & replication & consensus). +type StatefulComponentBase struct { + ComponentBase + // runningWorkload can be nil, and the replicas of workload can be nil (zero) + runningWorkload *appsv1.StatefulSet +} + +func (c *StatefulComponentBase) init(reqCtx intctrlutil.RequestCtx, cli client.Client, builder ComponentWorkloadBuilder, load bool) error { + var err error + if builder != nil { + if err = builder.BuildEnv(). + BuildWorkload(). + BuildPDB(). + BuildHeadlessService(). + BuildConfig(). + BuildTLSVolume(). + BuildVolumeMount(). + BuildService(). + BuildTLSCert(). + Complete(); err != nil { + return err + } + } + if load { + c.runningWorkload, err = c.loadRunningWorkload(reqCtx, cli) + if err != nil { + return err + } + } + return nil +} + +func (c *StatefulComponentBase) loadRunningWorkload(reqCtx intctrlutil.RequestCtx, cli client.Client) (*appsv1.StatefulSet, error) { + stsList, err := util.ListStsOwnedByComponent(reqCtx.Ctx, cli, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return nil, err + } + cnt := len(stsList) + if cnt == 1 { + return stsList[0], nil + } + if cnt == 0 { + return nil, nil + } else { + return nil, fmt.Errorf("more than one workloads found for the component, cluster: %s, component: %s, cnt: %d", + c.GetClusterName(), c.GetName(), cnt) + } +} + +func (c *StatefulComponentBase) GetBuiltObjects(builder ComponentWorkloadBuilder) ([]client.Object, error) { + dag := c.Dag + defer func() { + c.Dag = dag + }() + + c.Dag = graph.NewDAG() + if err := c.init(intctrlutil.RequestCtx{}, nil, builder, false); err != nil { + return nil, err + } + + objs := make([]client.Object, 0) + for _, v := range c.Dag.Vertices() { + if vv, ok := v.(*ictrltypes.LifecycleVertex); ok { + objs = append(objs, vv.Obj) + } + } + return objs, nil +} + +func (c *StatefulComponentBase) Create(reqCtx intctrlutil.RequestCtx, cli client.Client, builder ComponentWorkloadBuilder) error { + if err := c.init(reqCtx, cli, builder, false); err != nil { + return err + } + + if err := c.ValidateObjectsAction(); err != nil { + return err + } + + c.SetStatusPhase(appsv1alpha1.CreatingClusterCompPhase, nil, "Create a new component") + + return nil +} + +func (c *StatefulComponentBase) Delete(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + // TODO(impl): delete component owned resources + return nil +} + +func (c *StatefulComponentBase) Update(reqCtx intctrlutil.RequestCtx, cli client.Client, builder ComponentWorkloadBuilder) error { + if err := c.init(reqCtx, cli, builder, true); err != nil { + return err + } + + if c.runningWorkload != nil { + if err := c.Restart(reqCtx, cli); err != nil { + return err + } + + // cluster.spec.componentSpecs[*].volumeClaimTemplates[*].spec.resources.requests[corev1.ResourceStorage] + if err := c.ExpandVolume(reqCtx, cli); err != nil { + return err + } + + // cluster.spec.componentSpecs[*].replicas + if err := c.HorizontalScale(reqCtx, cli); err != nil { + return err + } + } + + if err := c.updateUnderlyingResources(reqCtx, cli, c.runningWorkload); err != nil { + return err + } + + return c.ResolveObjectsAction(reqCtx, cli) +} + +func (c *StatefulComponentBase) Status(reqCtx intctrlutil.RequestCtx, cli client.Client, builder ComponentWorkloadBuilder) error { + if err := c.init(reqCtx, cli, builder, true); err != nil { + return err + } + if c.runningWorkload == nil { + return nil + } + + statusTxn := &statusReconciliationTxn{} + + if err := c.statusExpandVolume(reqCtx, cli, statusTxn); err != nil { + return err + } + + if err := c.statusHorizontalScale(reqCtx, cli, statusTxn); err != nil { + return err + } + + // TODO(impl): restart pod if needed, move it to @Update and restart pod directly. + if vertexes, err := c.ComponentSet.HandleRestart(reqCtx.Ctx, c.runningWorkload); err != nil { + return err + } else { + for _, v := range vertexes { + c.Dag.AddVertex(v) + } + } + + if vertexes, err := c.ComponentSet.HandleHA(reqCtx.Ctx, c.runningWorkload); err != nil { + return err + } else { + for _, v := range vertexes { + c.Dag.AddVertex(v) + } + } + + if vertexes, err := c.ComponentSet.HandleRoleChange(reqCtx.Ctx, c.runningWorkload); err != nil { + return err + } else { + for _, v := range vertexes { + c.Dag.AddVertex(v) + } + } + + if err := c.StatusWorkload(reqCtx, cli, c.runningWorkload, statusTxn); err != nil { + return err + } + + if err := statusTxn.commit(); err != nil { + return err + } + + if err := c.handleGarbageOfRestoreBeforeRunning(); err != nil { + return err + } + c.updateWorkload(c.runningWorkload) + return nil +} + +func (c *StatefulComponentBase) Restart(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return util.RestartPod(&c.runningWorkload.Spec.Template) +} + +func (c *StatefulComponentBase) ExpandVolume(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + for _, vct := range c.runningWorkload.Spec.VolumeClaimTemplates { + var proto *corev1.PersistentVolumeClaimTemplate + for _, v := range c.Component.VolumeClaimTemplates { + if v.Name == vct.Name { + proto = &v + break + } + } + // REVIEW: seems we can remove a volume claim from templates at runtime, without any changes and warning messages? + if proto == nil { + continue + } + + if err := c.expandVolumes(reqCtx, cli, vct.Name, proto); err != nil { + return err + } + } + return nil +} + +func (c *StatefulComponentBase) expandVolumes(reqCtx intctrlutil.RequestCtx, cli client.Client, + vctName string, proto *corev1.PersistentVolumeClaimTemplate) error { + pvcNotFound := false + for i := *c.runningWorkload.Spec.Replicas - 1; i >= 0; i-- { + pvc := &corev1.PersistentVolumeClaim{} + pvcKey := types.NamespacedName{ + Namespace: c.GetNamespace(), + Name: fmt.Sprintf("%s-%s-%d", vctName, c.runningWorkload.Name, i), + } + if err := cli.Get(reqCtx.Ctx, pvcKey, pvc); err != nil { + if apierrors.IsNotFound(err) { + pvcNotFound = true + } else { + return err + } + } + if err := c.updatePVCSize(reqCtx, cli, pvcKey, pvc, pvcNotFound, proto); err != nil { + return err + } + } + return nil +} + +func (c *StatefulComponentBase) updatePVCSize(reqCtx intctrlutil.RequestCtx, cli client.Client, pvcKey types.NamespacedName, + pvc *corev1.PersistentVolumeClaim, pvcNotFound bool, vctProto *corev1.PersistentVolumeClaimTemplate) error { + // reference: https://kubernetes.io/docs/concepts/storage/persistent-volumes/#recovering-from-failure-when-expanding-volumes + // 1. Mark the PersistentVolume(PV) that is bound to the PersistentVolumeClaim(PVC) with Retain reclaim policy. + // 2. Delete the PVC. Since PV has Retain reclaim policy - we will not lose any data when we recreate the PVC. + // 3. Delete the claimRef entry from PV specs, so as new PVC can bind to it. This should make the PV Available. + // 4. Re-create the PVC with smaller size than PV and set volumeName field of the PVC to the name of the PV. This should bind new PVC to existing PV. + // 5. Don't forget to restore the reclaim policy of the PV. + newPVC := pvc.DeepCopy() + if pvcNotFound { + newPVC.Name = pvcKey.Name + newPVC.Namespace = pvcKey.Namespace + newPVC.SetLabels(vctProto.Labels) + newPVC.Spec = vctProto.Spec + ml := client.MatchingLabels{ + constant.PVCNameLabelKey: pvcKey.Name, + } + pvList := corev1.PersistentVolumeList{} + if err := cli.List(reqCtx.Ctx, &pvList, ml); err != nil { + return err + } + for _, pv := range pvList.Items { + // find pv referenced this pvc + if pv.Spec.ClaimRef == nil { + continue + } + if pv.Spec.ClaimRef.Name == pvcKey.Name { + newPVC.Spec.VolumeName = pv.Name + break + } + } + } else { + newPVC.Spec.Resources.Requests[corev1.ResourceStorage] = vctProto.Spec.Resources.Requests[corev1.ResourceStorage] + // delete annotation to make it re-bind + delete(newPVC.Annotations, "pv.kubernetes.io/bind-completed") + } + + pvNotFound := false + + // step 1: update pv to retain + pv := &corev1.PersistentVolume{} + pvKey := types.NamespacedName{ + Namespace: pvcKey.Namespace, + Name: newPVC.Spec.VolumeName, + } + if err := cli.Get(reqCtx.Ctx, pvKey, pv); err != nil { + if apierrors.IsNotFound(err) { + pvNotFound = true + } else { + return err + } + } + + type pvcRecreateStep int + const ( + pvPolicyRetainStep pvcRecreateStep = iota + deletePVCStep + removePVClaimRefStep + createPVCStep + pvRestorePolicyStep + ) + + addStepMap := map[pvcRecreateStep]func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex{ + pvPolicyRetainStep: func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex { + // step 1: update pv to retain + retainPV := pv.DeepCopy() + if retainPV.Labels == nil { + retainPV.Labels = make(map[string]string) + } + // add label to pv, in case pvc get deleted, and we can't find pv + retainPV.Labels[constant.PVCNameLabelKey] = pvcKey.Name + if retainPV.Annotations == nil { + retainPV.Annotations = make(map[string]string) + } + retainPV.Annotations[constant.PVLastClaimPolicyAnnotationKey] = string(pv.Spec.PersistentVolumeReclaimPolicy) + retainPV.Spec.PersistentVolumeReclaimPolicy = corev1.PersistentVolumeReclaimRetain + return c.PatchResource(retainPV, pv, fromVertex) + }, + deletePVCStep: func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex { + // step 2: delete pvc, this will not delete pv because policy is 'retain' + removeFinalizerPVC := pvc.DeepCopy() + removeFinalizerPVC.SetFinalizers([]string{}) + removeFinalizerPVCVertex := c.PatchResource(removeFinalizerPVC, pvc, fromVertex) + return c.DeleteResource(pvc, removeFinalizerPVCVertex) + }, + removePVClaimRefStep: func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex { + // step 3: remove claimRef in pv + removeClaimRefPV := pv.DeepCopy() + if removeClaimRefPV.Spec.ClaimRef != nil { + removeClaimRefPV.Spec.ClaimRef.UID = "" + removeClaimRefPV.Spec.ClaimRef.ResourceVersion = "" + } + return c.PatchResource(removeClaimRefPV, pv, fromVertex) + }, + createPVCStep: func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex { + // step 4: create new pvc + newPVC.SetResourceVersion("") + return c.CreateResource(newPVC, fromVertex) + }, + pvRestorePolicyStep: func(fromVertex *ictrltypes.LifecycleVertex, step pvcRecreateStep) *ictrltypes.LifecycleVertex { + // step 5: restore to previous pv policy + restorePV := pv.DeepCopy() + policy := corev1.PersistentVolumeReclaimPolicy(restorePV.Annotations[constant.PVLastClaimPolicyAnnotationKey]) + if len(policy) == 0 { + policy = corev1.PersistentVolumeReclaimDelete + } + restorePV.Spec.PersistentVolumeReclaimPolicy = policy + return c.PatchResource(restorePV, pv, fromVertex) + }, + } + + updatePVCByRecreateFromStep := func(fromStep pvcRecreateStep) { + lastVertex := c.WorkloadVertex + for step := pvRestorePolicyStep; step >= fromStep && step >= pvPolicyRetainStep; step-- { + lastVertex = addStepMap[step](lastVertex, step) + } + } + + targetQuantity := vctProto.Spec.Resources.Requests[corev1.ResourceStorage] + if pvcNotFound && !pvNotFound { + // this could happen if create pvc step failed when recreating pvc + updatePVCByRecreateFromStep(removePVClaimRefStep) + return nil + } + if pvcNotFound && pvNotFound { + // if both pvc and pv not found, do nothing + return nil + } + if reflect.DeepEqual(pvc.Spec.Resources, newPVC.Spec.Resources) && pv.Spec.PersistentVolumeReclaimPolicy == corev1.PersistentVolumeReclaimRetain { + // this could happen if create pvc succeeded but last step failed + updatePVCByRecreateFromStep(pvRestorePolicyStep) + return nil + } + if pvcQuantity := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; !viper.GetBool(constant.CfgRecoverVolumeExpansionFailure) && + pvcQuantity.Cmp(targetQuantity) == 1 && // check if it's compressing volume + targetQuantity.Cmp(*pvc.Status.Capacity.Storage()) >= 0 { // check if target size is greater than or equal to actual size + // this branch means we can update pvc size by recreate it + updatePVCByRecreateFromStep(pvPolicyRetainStep) + return nil + } + if pvcQuantity := pvc.Spec.Resources.Requests[corev1.ResourceStorage]; pvcQuantity.Cmp(vctProto.Spec.Resources.Requests[corev1.ResourceStorage]) != 0 { + // use pvc's update without anything extra + c.UpdateResource(newPVC, c.WorkloadVertex) + return nil + } + // all the else means no need to update + + return nil +} + +func (c *StatefulComponentBase) statusExpandVolume(reqCtx intctrlutil.RequestCtx, cli client.Client, txn *statusReconciliationTxn) error { + for _, vct := range c.runningWorkload.Spec.VolumeClaimTemplates { + running, failed, err := c.hasVolumeExpansionRunning(reqCtx, cli, vct.Name) + if err != nil { + return err + } + if failed { + txn.propose(appsv1alpha1.AbnormalClusterCompPhase, func() { + c.SetStatusPhase(appsv1alpha1.AbnormalClusterCompPhase, nil, "Volume Expansion failed") + }) + return nil + } + if running { + txn.propose(appsv1alpha1.SpecReconcilingClusterCompPhase, func() { + c.SetStatusPhase(appsv1alpha1.SpecReconcilingClusterCompPhase, nil, "Volume Expansion failed") + }) + return nil + } + } + return nil +} + +func (c *StatefulComponentBase) hasVolumeExpansionRunning(reqCtx intctrlutil.RequestCtx, cli client.Client, vctName string) (bool, bool, error) { + var ( + running bool + failed bool + ) + volumes, err := c.getRunningVolumes(reqCtx, cli, vctName, c.runningWorkload) + if err != nil { + return false, false, err + } + for _, v := range volumes { + if v.Status.Capacity == nil || v.Status.Capacity.Storage().Cmp(v.Spec.Resources.Requests[corev1.ResourceStorage]) >= 0 { + continue + } + running = true + // TODO: how to check the expansion failed? + } + return running, failed, nil +} + +func (c *StatefulComponentBase) HorizontalScale(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + ret := c.horizontalScaling(c.runningWorkload) + if ret == 0 { + return nil + } + if ret < 0 { + if err := c.scaleIn(reqCtx, cli, c.runningWorkload); err != nil { + return err + } + } else { + if err := c.scaleOut(reqCtx, cli, c.runningWorkload); err != nil { + return err + } + } + + if err := c.updatePodReplicaLabel4Scaling(reqCtx, cli, c.Component.Replicas); err != nil { + return err + } + + // update KB___ env needed by pod to obtain hostname. + c.updatePodEnvConfig() + + reqCtx.Recorder.Eventf(c.Cluster, + corev1.EventTypeNormal, + "HorizontalScale", + "start horizontal scale component %s of cluster %s from %d to %d", + c.GetName(), c.GetClusterName(), int(c.Component.Replicas)-ret, c.Component.Replicas) + + return nil +} + +// < 0 for scale in, > 0 for scale out, and == 0 for nothing +func (c *StatefulComponentBase) horizontalScaling(stsObj *appsv1.StatefulSet) int { + return int(c.Component.Replicas - *stsObj.Spec.Replicas) +} + +func (c *StatefulComponentBase) updatePodEnvConfig() { + for _, v := range ictrltypes.FindAll[*corev1.ConfigMap](c.Dag) { + node := v.(*ictrltypes.LifecycleVertex) + // TODO: need a way to reference the env config. + envConfigName := fmt.Sprintf("%s-%s-env", c.GetClusterName(), c.GetName()) + if node.Obj.GetName() == envConfigName { + node.Action = ictrltypes.ActionUpdatePtr() + } + } +} + +func (c *StatefulComponentBase) updatePodReplicaLabel4Scaling(reqCtx intctrlutil.RequestCtx, cli client.Client, replicas int32) error { + pods, err := util.ListPodOwnedByComponent(reqCtx.Ctx, cli, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return err + } + for _, pod := range pods { + obj := pod.DeepCopy() + if obj.Annotations == nil { + obj.Annotations = make(map[string]string) + } + obj.Annotations[constant.ComponentReplicasAnnotationKey] = strconv.Itoa(int(replicas)) + c.UpdateResource(obj, c.WorkloadVertex) + } + return nil +} + +func (c *StatefulComponentBase) scaleIn(reqCtx intctrlutil.RequestCtx, cli client.Client, stsObj *appsv1.StatefulSet) error { + for i := c.Component.Replicas; i < *stsObj.Spec.Replicas; i++ { + for _, vct := range stsObj.Spec.VolumeClaimTemplates { + pvcKey := types.NamespacedName{ + Namespace: stsObj.Namespace, + Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), + } + // create cronjob to delete pvc after 30 minutes + if obj, err := checkedCreateDeletePVCCronJob(reqCtx, cli, pvcKey, stsObj, c.Cluster); err != nil { + return err + } else if obj != nil { + c.CreateResource(obj, nil) + } + } + } + return nil +} + +func (c *StatefulComponentBase) postScaleIn(reqCtx intctrlutil.RequestCtx, cli client.Client, txn *statusReconciliationTxn) error { + hasJobFailed := func(reqCtx intctrlutil.RequestCtx, cli client.Client) (*batchv1.Job, string, error) { + jobs, err := util.ListObjWithLabelsInNamespace(reqCtx.Ctx, cli, generics.JobSignature, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return nil, "", err + } + for _, job := range jobs { + // TODO: use a better way to check the delete PVC job. + if !strings.HasPrefix(job.Name, "delete-pvc-") { + continue + } + for _, cond := range job.Status.Conditions { + if cond.Type == batchv1.JobFailed && cond.Status == corev1.ConditionTrue { + return job, fmt.Sprintf("%s-%s", cond.Reason, cond.Message), nil + } + } + } + return nil, "", nil + } + if job, msg, err := hasJobFailed(reqCtx, cli); err != nil { + return err + } else if job != nil { + msgKey := fmt.Sprintf("%s/%s", job.GetObjectKind().GroupVersionKind().Kind, job.GetName()) + statusMessage := appsv1alpha1.ComponentMessageMap{msgKey: msg} + txn.propose(appsv1alpha1.AbnormalClusterCompPhase, func() { + c.SetStatusPhase(appsv1alpha1.AbnormalClusterCompPhase, statusMessage, "PVC deletion job failed") + }) + } + return nil +} + +func (c *StatefulComponentBase) scaleOut(reqCtx intctrlutil.RequestCtx, cli client.Client, stsObj *appsv1.StatefulSet) error { + var ( + key = client.ObjectKey{ + Namespace: stsObj.Namespace, + Name: stsObj.Name, + } + snapshotKey = types.NamespacedName{ + Namespace: stsObj.Namespace, + Name: stsObj.Name + "-scaling", + } + horizontalScalePolicy = c.Component.HorizontalScalePolicy + + cleanCronJobs = func() error { + for i := *stsObj.Spec.Replicas; i < c.Component.Replicas; i++ { + for _, vct := range stsObj.Spec.VolumeClaimTemplates { + pvcKey := types.NamespacedName{ + Namespace: key.Namespace, + Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), + } + // delete deletion cronjob if exists + cronJobKey := pvcKey + cronJobKey.Name = "delete-pvc-" + pvcKey.Name + cronJob := &batchv1.CronJob{} + if err := cli.Get(reqCtx.Ctx, cronJobKey, cronJob); err != nil { + return client.IgnoreNotFound(err) + } + c.DeleteResource(cronJob, c.WorkloadVertex) + } + } + return nil + } + + checkAllPVCsExist = func() (bool, error) { + for i := *stsObj.Spec.Replicas; i < c.Component.Replicas; i++ { + for _, vct := range stsObj.Spec.VolumeClaimTemplates { + pvcKey := types.NamespacedName{ + Namespace: key.Namespace, + Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), + } + // check pvc existence + pvcExists, err := isPVCExists(cli, reqCtx.Ctx, pvcKey) + if err != nil { + return true, err + } + if !pvcExists { + return false, nil + } + } + } + return true, nil + } + ) + + if err := cleanCronJobs(); err != nil { + return err + } + + allPVCsExist, err := checkAllPVCsExist() + if err != nil { + return err + } + if !allPVCsExist { + if horizontalScalePolicy == nil { + c.WorkloadVertex.Immutable = false + return nil + } + // do backup according to component's horizontal scale policy + c.WorkloadVertex.Immutable = true + stsProto := c.WorkloadVertex.Obj.(*appsv1.StatefulSet) + objs, err := doBackup(reqCtx, cli, c.Cluster, c.Component, snapshotKey, stsProto, stsObj) + if err != nil { + return err + } + for _, obj := range objs { + c.CreateResource(obj, nil) + } + return nil + } + // pvcs are ready, stateful_set.replicas should be updated + c.WorkloadVertex.Immutable = false + + return c.postScaleOut(reqCtx, cli, stsObj) +} + +func (c *StatefulComponentBase) postScaleOut(reqCtx intctrlutil.RequestCtx, cli client.Client, stsObj *appsv1.StatefulSet) error { + var ( + snapshotKey = types.NamespacedName{ + Namespace: stsObj.Namespace, + Name: stsObj.Name + "-scaling", + } + horizontalScalePolicy = c.Component.HorizontalScalePolicy + + checkAllPVCBoundIfNeeded = func() (bool, error) { + if horizontalScalePolicy == nil || + horizontalScalePolicy.Type != appsv1alpha1.HScaleDataClonePolicyFromSnapshot || + !isSnapshotAvailable(cli, reqCtx.Ctx) { + return true, nil + } + return isAllPVCBound(cli, reqCtx.Ctx, stsObj, int(c.Component.Replicas)) + } + + cleanBackupResourcesIfNeeded = func() error { + if horizontalScalePolicy == nil || + horizontalScalePolicy.Type != appsv1alpha1.HScaleDataClonePolicyFromSnapshot || + !isSnapshotAvailable(cli, reqCtx.Ctx) { + return nil + } + // if all pvc bounded, clean backup resources + objs, err := deleteSnapshot(cli, reqCtx, snapshotKey, c.GetCluster(), c.GetName()) + if err != nil { + return err + } + for _, obj := range objs { + c.DeleteResource(obj, nil) + } + return nil + } + ) + + // check all pvc bound, wait next reconciliation if not all ready + allPVCBounded, err := checkAllPVCBoundIfNeeded() + if err != nil { + return err + } + if !allPVCBounded { + return nil + } + // clean backup resources. + // there will not be any backup resources other than scale out. + if err := cleanBackupResourcesIfNeeded(); err != nil { + return err + } + + return nil +} + +func (c *StatefulComponentBase) statusHorizontalScale(reqCtx intctrlutil.RequestCtx, cli client.Client, txn *statusReconciliationTxn) error { + ret := c.horizontalScaling(c.runningWorkload) + if ret < 0 { + return nil + } + if ret > 0 { + // forward the h-scaling progress. + return c.scaleOut(reqCtx, cli, c.runningWorkload) + } + if ret == 0 { // sts has been updated + if err := c.postScaleIn(reqCtx, cli, txn); err != nil { + return err + } + if err := c.postScaleOut(reqCtx, cli, c.runningWorkload); err != nil { + return err + } + } + return nil +} + +func (c *StatefulComponentBase) updateUnderlyingResources(reqCtx intctrlutil.RequestCtx, cli client.Client, stsObj *appsv1.StatefulSet) error { + if stsObj == nil { + c.createWorkload() + c.SetStatusPhase(appsv1alpha1.SpecReconcilingClusterCompPhase, nil, "Component workload created") + } else { + if c.updateWorkload(stsObj) { + c.SetStatusPhase(appsv1alpha1.SpecReconcilingClusterCompPhase, nil, "Component workload updated") + } + // to work around that the scaled PVC will be deleted at object action. + if err := c.updateVolumes(reqCtx, cli, stsObj); err != nil { + return err + } + } + if err := c.UpdatePDB(reqCtx, cli); err != nil { + return err + } + if err := c.UpdateService(reqCtx, cli); err != nil { + return err + } + return nil +} + +func (c *StatefulComponentBase) createWorkload() { + stsProto := c.WorkloadVertex.Obj.(*appsv1.StatefulSet) + c.WorkloadVertex.Obj = stsProto + c.WorkloadVertex.Action = ictrltypes.ActionCreatePtr() +} + +func (c *StatefulComponentBase) updateWorkload(stsObj *appsv1.StatefulSet) bool { + stsObjCopy := stsObj.DeepCopy() + stsProto := c.WorkloadVertex.Obj.(*appsv1.StatefulSet) + + // keep the original template annotations. + // if annotations exist and are replaced, the statefulSet will be updated. + util.MergeAnnotations(stsObjCopy.Spec.Template.Annotations, &stsProto.Spec.Template.Annotations) + util.BuildWorkLoadAnnotations(stsObjCopy, c.Cluster) + stsObjCopy.Spec.Template = stsProto.Spec.Template + stsObjCopy.Spec.Replicas = stsProto.Spec.Replicas + stsObjCopy.Spec.UpdateStrategy = stsProto.Spec.UpdateStrategy + if !reflect.DeepEqual(&stsObj.Spec, &stsObjCopy.Spec) { + // TODO(REVIEW): always return true and update component phase to Updating. stsObj.Spec contains default values which set by Kubernetes + c.WorkloadVertex.Obj = stsObjCopy + c.WorkloadVertex.Action = ictrltypes.ActionPtr(ictrltypes.UPDATE) + return true + } + return false +} + +func (c *StatefulComponentBase) updateVolumes(reqCtx intctrlutil.RequestCtx, cli client.Client, stsObj *appsv1.StatefulSet) error { + // PVCs which have been added to the dag because of volume expansion. + pvcNameSet := sets.New[string]() + for _, v := range ictrltypes.FindAll[*corev1.PersistentVolumeClaim](c.Dag) { + pvcNameSet.Insert(v.(*ictrltypes.LifecycleVertex).Obj.GetName()) + } + + for _, vct := range c.Component.VolumeClaimTemplates { + pvcs, err := c.getRunningVolumes(reqCtx, cli, vct.Name, stsObj) + if err != nil { + return err + } + for _, pvc := range pvcs { + if pvcNameSet.Has(pvc.Name) { + continue + } + c.NoopResource(pvc, c.WorkloadVertex) + } + } + return nil +} + +func (c *StatefulComponentBase) getRunningVolumes(reqCtx intctrlutil.RequestCtx, cli client.Client, vctName string, + stsObj *appsv1.StatefulSet) ([]*corev1.PersistentVolumeClaim, error) { + pvcs, err := util.ListObjWithLabelsInNamespace(reqCtx.Ctx, cli, generics.PersistentVolumeClaimSignature, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + matchedPVCs := make([]*corev1.PersistentVolumeClaim, 0) + prefix := fmt.Sprintf("%s-%s", vctName, stsObj.Name) + for _, pvc := range pvcs { + if strings.HasPrefix(pvc.Name, prefix) { + matchedPVCs = append(matchedPVCs, pvc) + } + } + return matchedPVCs, nil +} + +// handleGarbageOfRestoreBeforeRunning handles the garbage for restore before cluster phase changes to Running. +// @return ErrNoOps if no operation +// REVIEW: this handling is rather hackish, call for refactor. +// Deprecated: to be removed by PITR feature. +func (c *StatefulComponentBase) handleGarbageOfRestoreBeforeRunning() error { + clusterBackupResourceMap, err := c.getClusterBackupSourceMap(c.GetCluster()) + if err != nil { + return err + } + if clusterBackupResourceMap == nil { + return nil + } + if c.GetPhase() != appsv1alpha1.RunningClusterCompPhase { + return nil + } + + // remove the garbage for restore if the component restores from backup. + for _, v := range clusterBackupResourceMap { + // remove the init container for restore + if err = c.removeStsInitContainerForRestore(v); err != nil { + return err + } + } + // TODO: remove from the cluster annotation RestoreFromBackUpAnnotationKey? + return nil +} + +// getClusterBackupSourceMap gets the backup source map from cluster.annotations +func (c *StatefulComponentBase) getClusterBackupSourceMap(cluster *appsv1alpha1.Cluster) (map[string]string, error) { + compBackupMapString := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] + if len(compBackupMapString) == 0 { + return nil, nil + } + compBackupMap := map[string]string{} + err := json.Unmarshal([]byte(compBackupMapString), &compBackupMap) + for k := range compBackupMap { + if cluster.Spec.GetComponentByName(k) == nil { + return nil, intctrlutil.NewErrorf(intctrlutil.ErrorTypeNotFound, "restore: not found componentSpecs[*].name %s", k) + } + } + return compBackupMap, err +} + +// removeStsInitContainerForRestore removes the statefulSet's init container which restores data from backup. +func (c *StatefulComponentBase) removeStsInitContainerForRestore(backupName string) error { + sts := c.WorkloadVertex.Obj.(*appsv1.StatefulSet) + initContainers := sts.Spec.Template.Spec.InitContainers + restoreInitContainerName := component.GetRestoredInitContainerName(backupName) + restoreInitContainerIndex, _ := intctrlutil.GetContainerByName(initContainers, restoreInitContainerName) + if restoreInitContainerIndex == -1 { + return nil + } + + initContainers = append(initContainers[:restoreInitContainerIndex], initContainers[restoreInitContainerIndex+1:]...) + sts.Spec.Template.Spec.InitContainers = initContainers + if *c.WorkloadVertex.Action != ictrltypes.UPDATE { + if *c.WorkloadVertex.Action != ictrltypes.CREATE && *c.WorkloadVertex.Action != ictrltypes.DELETE { + c.WorkloadVertex.Action = ictrltypes.ActionUpdatePtr() + } + } + // TODO: it seems not reasonable to reset component phase back to Creating. + //// if need to remove init container, reset component to Creating. + // c.SetStatusPhase(appsv1alpha1.CreatingClusterCompPhase, "Remove init container for restore") + return nil +} diff --git a/controllers/apps/components/internal/component_base_stateful_hscale.go b/controllers/apps/components/internal/component_base_stateful_hscale.go new file mode 100644 index 000000000..d917fdac6 --- /dev/null +++ b/controllers/apps/components/internal/component_base_stateful_hscale.go @@ -0,0 +1,471 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package internal + +import ( + "context" + "fmt" + "strings" + "time" + + snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + "github.com/pkg/errors" + "github.com/spf13/viper" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" + types2 "github.com/apecloud/kubeblocks/internal/controller/client" + "github.com/apecloud/kubeblocks/internal/controller/component" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// TODO: handle unfinished jobs from previous scale in +func checkedCreateDeletePVCCronJob(reqCtx intctrlutil.RequestCtx, cli types2.ReadonlyClient, + pvcKey types.NamespacedName, stsObj *appsv1.StatefulSet, cluster *appsv1alpha1.Cluster) (client.Object, error) { + // hack: delete after 30 minutes + utc := time.Now().Add(30 * time.Minute).UTC() + schedule := fmt.Sprintf("%d %d %d %d *", utc.Minute(), utc.Hour(), utc.Day(), utc.Month()) + cronJob, err := builder.BuildCronJob(pvcKey, schedule, stsObj) + if err != nil { + return nil, err + } + + job := &batchv1.CronJob{} + if err := cli.Get(reqCtx.Ctx, client.ObjectKeyFromObject(cronJob), job); err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + reqCtx.Recorder.Eventf(cluster, + corev1.EventTypeNormal, + "CronJobCreate", + "create cronjob to delete pvc/%s", + pvcKey.Name) + return cronJob, nil + } + return nil, nil +} + +func isPVCExists(cli types2.ReadonlyClient, ctx context.Context, + pvcKey types.NamespacedName) (bool, error) { + pvc := corev1.PersistentVolumeClaim{} + if err := cli.Get(ctx, pvcKey, &pvc); err != nil { + return false, client.IgnoreNotFound(err) + } + return true, nil +} + +func isAllPVCBound(cli types2.ReadonlyClient, + ctx context.Context, + stsObj *appsv1.StatefulSet, + targetReplicas int) (bool, error) { + if len(stsObj.Spec.VolumeClaimTemplates) == 0 { + return true, nil + } + for i := 0; i < targetReplicas; i++ { + pvcKey := types.NamespacedName{ + Namespace: stsObj.Namespace, + Name: fmt.Sprintf("%s-%s-%d", stsObj.Spec.VolumeClaimTemplates[0].Name, stsObj.Name, i), + } + pvc := corev1.PersistentVolumeClaim{} + // check pvc existence + if err := cli.Get(ctx, pvcKey, &pvc); err != nil { + return false, client.IgnoreNotFound(err) + } + if pvc.Status.Phase != corev1.ClaimBound { + return false, nil + } + } + return true, nil +} + +// check volume snapshot available +func isSnapshotAvailable(cli types2.ReadonlyClient, ctx context.Context) bool { + if !viper.GetBool("VOLUMESNAPSHOT") { + return false + } + vsList := snapshotv1.VolumeSnapshotList{} + compatClient := intctrlutil.VolumeSnapshotCompatClient{ReadonlyClient: cli, Ctx: ctx} + getVSErr := compatClient.List(&vsList) + return getVSErr == nil +} + +func deleteSnapshot(cli types2.ReadonlyClient, + reqCtx intctrlutil.RequestCtx, + snapshotKey types.NamespacedName, + cluster *appsv1alpha1.Cluster, + componentName string) ([]client.Object, error) { + objs, err := deleteBackup(reqCtx.Ctx, cli, cluster.Name, componentName) + if err != nil { + return nil, err + } + if len(objs) > 0 { + reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "BackupJobDelete", "Delete backupJob/%s", snapshotKey.Name) + } + + compatClient := intctrlutil.VolumeSnapshotCompatClient{ReadonlyClient: cli, Ctx: reqCtx.Ctx} + vs := &snapshotv1.VolumeSnapshot{} + err = compatClient.Get(snapshotKey, vs) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + if err == nil { + objs = append(objs, vs) + reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "VolumeSnapshotDelete", "Delete volumeSnapshot/%s", snapshotKey.Name) + } + + return objs, nil +} + +// deleteBackup will delete all backup related resources created during horizontal scaling +func deleteBackup(ctx context.Context, cli types2.ReadonlyClient, clusterName string, componentName string) ([]client.Object, error) { + ml := getBackupMatchingLabels(clusterName, componentName) + backupList := dataprotectionv1alpha1.BackupList{} + if err := cli.List(ctx, &backupList, ml); err != nil { + return nil, err + } + objs := make([]client.Object, 0) + for i := range backupList.Items { + objs = append(objs, &backupList.Items[i]) + } + return objs, nil +} + +func getBackupMatchingLabels(clusterName string, componentName string) client.MatchingLabels { + return client.MatchingLabels{ + constant.AppInstanceLabelKey: clusterName, + constant.KBAppComponentLabelKey: componentName, + constant.KBManagedByKey: "cluster", // the resources are managed by which controller + } +} + +func doBackup(reqCtx intctrlutil.RequestCtx, + cli types2.ReadonlyClient, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, + snapshotKey types.NamespacedName, + stsProto *appsv1.StatefulSet, + stsObj *appsv1.StatefulSet) ([]client.Object, error) { + if component.HorizontalScalePolicy == nil { + return nil, nil + } + + objs := make([]client.Object, 0) + + // do backup according to component's horizontal scale policy + switch component.HorizontalScalePolicy.Type { + // use backup tool such as xtrabackup + case appsv1alpha1.HScaleDataClonePolicyFromBackup: + // TODO: db core not support yet, leave it empty + reqCtx.Recorder.Eventf(cluster, + corev1.EventTypeWarning, + "HorizontalScaleFailed", + "scale with backup tool not supported yet") + // use volume snapshot + case appsv1alpha1.HScaleDataClonePolicyFromSnapshot: + if !isSnapshotAvailable(cli, reqCtx.Ctx) { + // TODO: add ut + return nil, fmt.Errorf("HorizontalScaleFailed: volume snapshot not supported") + } + vcts := component.VolumeClaimTemplates + if len(vcts) == 0 { + reqCtx.Recorder.Eventf(cluster, + corev1.EventTypeNormal, + "HorizontalScale", + "no VolumeClaimTemplates, no need to do data clone.") + break + } + vsExists, err := isVolumeSnapshotExists(cli, reqCtx.Ctx, cluster, component) + if err != nil { + return nil, err + } + // if volumesnapshot not exist, do snapshot to create it. + if !vsExists { + if snapshots, err := doSnapshot(cli, + reqCtx, + cluster, + snapshotKey, + stsObj, + vcts, + component.ComponentDef, + component.HorizontalScalePolicy.BackupPolicyTemplateName); err != nil { + return nil, err + } else { + objs = append(objs, snapshots...) + } + break + } + // volumesnapshot exists, check if it is ready for use. + ready, err := isVolumeSnapshotReadyToUse(cli, reqCtx.Ctx, cluster, component) + if err != nil { + return nil, err + } + // volumesnapshot not ready, wait till it is ready after reconciling. + if !ready { + break + } + // if volumesnapshot ready, + // create pvc from snapshot for every new pod + for i := *stsObj.Spec.Replicas; i < *stsProto.Spec.Replicas; i++ { + vct := vcts[0] + for _, tmpVct := range vcts { + if tmpVct.Name == component.HorizontalScalePolicy.VolumeMountsName { + vct = tmpVct + break + } + } + // sync vct.spec.resources from component + for _, tmpVct := range component.VolumeClaimTemplates { + if vct.Name == tmpVct.Name { + vct.Spec.Resources = tmpVct.Spec.Resources + break + } + } + pvcKey := types.NamespacedName{ + Namespace: stsObj.Namespace, + Name: fmt.Sprintf("%s-%s-%d", + vct.Name, + stsObj.Name, + i), + } + if pvc, err := checkedCreatePVCFromSnapshot(cli, + reqCtx.Ctx, + pvcKey, + cluster, + component, + vct, + stsObj); err != nil { + reqCtx.Log.Error(err, "checkedCreatePVCFromSnapshot failed") + return nil, err + } else if pvc != nil { + objs = append(objs, pvc) + } + } + // do nothing + case appsv1alpha1.HScaleDataClonePolicyNone: + break + } + return objs, nil +} + +// check snapshot existence +func isVolumeSnapshotExists(cli types2.ReadonlyClient, + ctx context.Context, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) (bool, error) { + ml := getBackupMatchingLabels(cluster.Name, component.Name) + vsList := snapshotv1.VolumeSnapshotList{} + compatClient := intctrlutil.VolumeSnapshotCompatClient{ReadonlyClient: cli, Ctx: ctx} + if err := compatClient.List(&vsList, ml); err != nil { + return false, client.IgnoreNotFound(err) + } + for _, vs := range vsList.Items { + // when do h-scale very shortly after last h-scale, + // the last volume snapshot could not be deleted completely + if vs.DeletionTimestamp.IsZero() { + return true, nil + } + } + return false, nil +} + +func doSnapshot(cli types2.ReadonlyClient, + reqCtx intctrlutil.RequestCtx, + cluster *appsv1alpha1.Cluster, + snapshotKey types.NamespacedName, + stsObj *appsv1.StatefulSet, + vcts []corev1.PersistentVolumeClaimTemplate, + componentDef, + backupPolicyTemplateName string) ([]client.Object, error) { + backupPolicyTemplate := &appsv1alpha1.BackupPolicyTemplate{} + err := cli.Get(reqCtx.Ctx, client.ObjectKey{Name: backupPolicyTemplateName}, backupPolicyTemplate) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + + if err != nil { + // no backuppolicytemplate, then try native volumesnapshot + pvcName := strings.Join([]string{vcts[0].Name, stsObj.Name, "0"}, "-") + snapshot, err := builder.BuildVolumeSnapshot(snapshotKey, pvcName, stsObj) + if err != nil { + return nil, err + } + reqCtx.Eventf(cluster, corev1.EventTypeNormal, "VolumeSnapshotCreate", "Create volumesnapshot/%s", snapshotKey.Name) + return []client.Object{snapshot}, nil + } + + // if there is backuppolicytemplate created by provider + // create backupjob CR, will ignore error if already exists + return createBackup(reqCtx, cli, stsObj, componentDef, backupPolicyTemplateName, snapshotKey, cluster) +} + +// check snapshot ready to use +func isVolumeSnapshotReadyToUse(cli types2.ReadonlyClient, + ctx context.Context, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) (bool, error) { + ml := getBackupMatchingLabels(cluster.Name, component.Name) + vsList := snapshotv1.VolumeSnapshotList{} + compatClient := intctrlutil.VolumeSnapshotCompatClient{ReadonlyClient: cli, Ctx: ctx} + if err := compatClient.List(&vsList, ml); err != nil { + return false, client.IgnoreNotFound(err) + } + if len(vsList.Items) == 0 || vsList.Items[0].Status == nil { + return false, nil + } + status := vsList.Items[0].Status + if status.Error != nil { + return false, errors.New("VolumeSnapshot/" + vsList.Items[0].Name + ": " + *status.Error.Message) + } + if status.ReadyToUse == nil { + return false, nil + } + return *status.ReadyToUse, nil +} + +func checkedCreatePVCFromSnapshot(cli types2.ReadonlyClient, + ctx context.Context, + pvcKey types.NamespacedName, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, + vct corev1.PersistentVolumeClaimTemplate, + stsObj *appsv1.StatefulSet) (client.Object, error) { + pvc := corev1.PersistentVolumeClaim{} + // check pvc existence + if err := cli.Get(ctx, pvcKey, &pvc); err != nil { + if !apierrors.IsNotFound(err) { + return nil, err + } + ml := getBackupMatchingLabels(cluster.Name, component.Name) + vsList := snapshotv1.VolumeSnapshotList{} + compatClient := intctrlutil.VolumeSnapshotCompatClient{ReadonlyClient: cli, Ctx: ctx} + if err := compatClient.List(&vsList, ml); err != nil { + return nil, err + } + if len(vsList.Items) == 0 { + return nil, fmt.Errorf("volumesnapshot not found in cluster %s component %s", cluster.Name, component.Name) + } + // exclude volumes that are deleting + vsName := "" + for _, vs := range vsList.Items { + if vs.DeletionTimestamp != nil { + continue + } + vsName = vs.Name + break + } + return createPVCFromSnapshot(vct, stsObj, pvcKey, vsName, component) + } + return nil, nil +} + +// createBackup creates backup resources required to do backup, +func createBackup(reqCtx intctrlutil.RequestCtx, + cli types2.ReadonlyClient, + sts *appsv1.StatefulSet, + componentDef, + backupPolicyTemplateName string, + backupKey types.NamespacedName, + cluster *appsv1alpha1.Cluster) ([]client.Object, error) { + objs := make([]client.Object, 0) + createBackup := func(backupPolicyName string) error { + backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} + if err := cli.Get(reqCtx.Ctx, client.ObjectKey{Namespace: backupKey.Namespace, Name: backupPolicyName}, backupPolicy); err != nil && !apierrors.IsNotFound(err) { + return err + } + // wait for backupPolicy created + if len(backupPolicy.Name) == 0 { + return nil + } + backupList := dataprotectionv1alpha1.BackupList{} + ml := getBackupMatchingLabels(cluster.Name, sts.Labels[constant.KBAppComponentLabelKey]) + if err := cli.List(reqCtx.Ctx, &backupList, ml); err != nil { + return err + } + if len(backupList.Items) > 0 { + // check backup status, if failed return error + if backupList.Items[0].Status.Phase == dataprotectionv1alpha1.BackupFailed { + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeBackupFailed, "backup for horizontalScaling failed: %s", + backupList.Items[0].Status.FailureReason) + } + return nil + } + backup, err := builder.BuildBackup(sts, backupPolicyName, backupKey) + if err != nil { + return err + } + objs = append(objs, backup) + return nil + } + backupPolicy, err := getBackupPolicyFromTemplate(reqCtx, cli, cluster, componentDef, backupPolicyTemplateName) + if err != nil { + return nil, err + } + if backupPolicy == nil { + return nil, intctrlutil.NewNotFound("cannot find any backup policy created by %s", backupPolicyTemplateName) + } + if err = createBackup(backupPolicy.Name); err != nil { + return nil, err + } + + reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "BackupJobCreate", "Create backupJob/%s", backupKey.Name) + return objs, nil +} + +// getBackupPolicyFromTemplate gets backup policy from template policy template. +func getBackupPolicyFromTemplate(reqCtx intctrlutil.RequestCtx, + cli types2.ReadonlyClient, + cluster *appsv1alpha1.Cluster, + componentDef, backupPolicyTemplateName string) (*dataprotectionv1alpha1.BackupPolicy, error) { + backupPolicyList := &dataprotectionv1alpha1.BackupPolicyList{} + if err := cli.List(reqCtx.Ctx, backupPolicyList, + client.InNamespace(cluster.Namespace), + client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.Name, + constant.KBAppComponentDefRefLabelKey: componentDef, + }); err != nil { + return nil, err + } + for _, backupPolicy := range backupPolicyList.Items { + if backupPolicy.Annotations[constant.BackupPolicyTemplateAnnotationKey] == backupPolicyTemplateName { + return &backupPolicy, nil + } + } + return nil, nil +} + +func createPVCFromSnapshot(vct corev1.PersistentVolumeClaimTemplate, + sts *appsv1.StatefulSet, + pvcKey types.NamespacedName, + snapshotName string, + component *component.SynthesizedComponent) (client.Object, error) { + pvc, err := builder.BuildPVCFromSnapshot(sts, vct, pvcKey, snapshotName, component) + if err != nil { + return nil, err + } + return pvc, nil +} diff --git a/controllers/apps/components/internal/component_status.go b/controllers/apps/components/internal/component_status.go new file mode 100644 index 000000000..02cb0d4b0 --- /dev/null +++ b/controllers/apps/components/internal/component_status.go @@ -0,0 +1,63 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package internal + +import ( + "sort" + + "golang.org/x/exp/maps" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +var componentPhasePriority = map[appsv1alpha1.ClusterComponentPhase]int{ + appsv1alpha1.FailedClusterCompPhase: 1, + appsv1alpha1.AbnormalClusterCompPhase: 2, + appsv1alpha1.SpecReconcilingClusterCompPhase: 3, + appsv1alpha1.StoppedClusterCompPhase: 4, + appsv1alpha1.RunningClusterCompPhase: 5, + appsv1alpha1.CreatingClusterCompPhase: 6, +} + +type statusReconciliationTxn struct { + proposals map[appsv1alpha1.ClusterComponentPhase]func() +} + +func (t *statusReconciliationTxn) propose(phase appsv1alpha1.ClusterComponentPhase, mutator func()) { + if t.proposals == nil { + t.proposals = make(map[appsv1alpha1.ClusterComponentPhase]func()) + } + if _, ok := t.proposals[phase]; ok { + return // keep first + } + t.proposals[phase] = mutator +} + +func (t *statusReconciliationTxn) commit() error { + if len(t.proposals) == 0 { + return nil + } + phases := maps.Keys(t.proposals) + sort.Slice(phases, func(i, j int) bool { + return componentPhasePriority[phases[i]] < componentPhasePriority[phases[j]] + }) + t.proposals[phases[0]]() + return nil +} diff --git a/controllers/apps/components/internal/component_workload.go b/controllers/apps/components/internal/component_workload.go new file mode 100644 index 000000000..9eeff20fb --- /dev/null +++ b/controllers/apps/components/internal/component_workload.go @@ -0,0 +1,343 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package internal + +import ( + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/internal/controller/builder" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/plan" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// TODO(impl): define a custom workload to encapsulate all the resources. + +type ComponentWorkloadBuilder interface { + // runtime, config, script, env, volume, service, monitor, probe + BuildEnv() ComponentWorkloadBuilder + BuildConfig() ComponentWorkloadBuilder + BuildWorkload() ComponentWorkloadBuilder + BuildPDB() ComponentWorkloadBuilder + BuildVolumeMount() ComponentWorkloadBuilder + BuildService() ComponentWorkloadBuilder + BuildHeadlessService() ComponentWorkloadBuilder + BuildTLSCert() ComponentWorkloadBuilder + BuildTLSVolume() ComponentWorkloadBuilder + + Complete() error +} + +type ComponentWorkloadBuilderBase struct { + ReqCtx intctrlutil.RequestCtx + Client client.Client + Comp types.Component + DefaultAction *ictrltypes.LifecycleAction + ConcreteBuilder ComponentWorkloadBuilder + Error error + EnvConfig *corev1.ConfigMap + Workload client.Object + LocalObjs []client.Object // cache the objects needed for configuration, should remove this after refactoring the configuration +} + +func (b *ComponentWorkloadBuilderBase) BuildEnv() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + envCfg, err := builder.BuildEnvConfigLow(b.ReqCtx, b.Client, b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + b.EnvConfig = envCfg + b.LocalObjs = append(b.LocalObjs, envCfg) + return []client.Object{envCfg}, err + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildConfig() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + if b.Workload == nil { + return nil, fmt.Errorf("build config but workload is nil, cluster: %s, component: %s", + b.Comp.GetClusterName(), b.Comp.GetName()) + } + + objs, err := plan.RenderConfigNScriptFiles(b.Comp.GetClusterVersion(), b.Comp.GetCluster(), + b.Comp.GetSynthesizedComponent(), b.Workload, b.getRuntime(), b.LocalObjs, b.ReqCtx.Ctx, b.Client) + if err != nil { + return nil, err + } + for _, obj := range objs { + if cm, ok := obj.(*corev1.ConfigMap); ok { + b.LocalObjs = append(b.LocalObjs, cm) + } + } + return objs, nil + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildWorkload4StatefulSet(workloadType string) ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + if b.EnvConfig == nil { + return nil, fmt.Errorf("build %s workload but env config is nil, cluster: %s, component: %s", + workloadType, b.Comp.GetClusterName(), b.Comp.GetName()) + } + + component := b.Comp.GetSynthesizedComponent() + sts, err := builder.BuildStsLow(b.ReqCtx, b.Comp.GetCluster(), component, b.EnvConfig.Name) + if err != nil { + return nil, err + } + + b.Workload = sts + + return nil, nil // don't return sts here + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildPDB() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + // if without this handler, the cluster controller will occur error during reconciling. + // conditionally build PodDisruptionBudget + synthesizedComponent := b.Comp.GetSynthesizedComponent() + if synthesizedComponent.MinAvailable != nil { + pdb, err := builder.BuildPDBLow(b.Comp.GetCluster(), synthesizedComponent) + if err != nil { + return nil, err + } + return []client.Object{pdb}, nil + } else { + panic("this shouldn't happen") + } + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildVolumeMount() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + if b.Workload == nil { + return nil, fmt.Errorf("build volume mount but workload is nil, cluster: %s, component: %s", + b.Comp.GetClusterName(), b.Comp.GetName()) + } + + podSpec := b.getRuntime() + for _, cc := range []*[]corev1.Container{&podSpec.Containers, &podSpec.InitContainers} { + volumes := podSpec.Volumes + for _, c := range *cc { + for _, v := range c.VolumeMounts { + // if persistence is not found, add emptyDir pod.spec.volumes[] + createfn := func(_ string) corev1.Volume { + return corev1.Volume{ + Name: v.Name, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + } + volumes, _ = intctrlutil.CreateOrUpdateVolume(volumes, v.Name, createfn, nil) + } + } + podSpec.Volumes = volumes + } + return nil, nil + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildService() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + svcList, err := builder.BuildSvcListLow(b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + if err != nil { + return nil, err + } + objs := make([]client.Object, 0) + for _, svc := range svcList { + objs = append(objs, svc) + } + return objs, err + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildHeadlessService() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + svc, err := builder.BuildHeadlessSvcLow(b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + return []client.Object{svc}, err + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildTLSCert() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + cluster := b.Comp.GetCluster() + component := b.Comp.GetSynthesizedComponent() + if !component.TLS { + return nil, nil + } + if component.Issuer == nil { + return nil, fmt.Errorf("issuer shouldn't be nil when tls enabled") + } + + objs := make([]client.Object, 0) + switch component.Issuer.Name { + case appsv1alpha1.IssuerUserProvided: + if err := plan.CheckTLSSecretRef(b.ReqCtx.Ctx, b.Client, cluster.Namespace, component.Issuer.SecretRef); err != nil { + return nil, err + } + case appsv1alpha1.IssuerKubeBlocks: + secret, err := plan.ComposeTLSSecret(cluster.Namespace, cluster.Name, component.Name) + if err != nil { + return nil, err + } + objs = append(objs, secret) + b.LocalObjs = append(b.LocalObjs, secret) + } + return objs, nil + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) BuildTLSVolume() ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + if b.Workload == nil { + return nil, fmt.Errorf("build TLS volumes but workload is nil, cluster: %s, component: %s", + b.Comp.GetClusterName(), b.Comp.GetName()) + } + // build secret volume and volume mount + return nil, updateTLSVolumeAndVolumeMount(b.getRuntime(), b.Comp.GetClusterName(), *b.Comp.GetSynthesizedComponent()) + } + return b.BuildWrapper(buildfn) +} + +func (b *ComponentWorkloadBuilderBase) Complete() error { + if b.Error != nil { + return b.Error + } + if b.Workload == nil { + return fmt.Errorf("fail to create component workloads, cluster: %s, component: %s", + b.Comp.GetClusterName(), b.Comp.GetName()) + } + b.Comp.SetWorkload(b.Workload, b.DefaultAction, nil) + return nil +} + +func (b *ComponentWorkloadBuilderBase) BuildWrapper(buildfn func() ([]client.Object, error)) ComponentWorkloadBuilder { + if b.Error != nil || buildfn == nil { + return b.ConcreteBuilder + } + objs, err := buildfn() + if err != nil { + b.Error = err + } else { + for _, obj := range objs { + b.Comp.AddResource(obj, b.DefaultAction, nil) + } + } + return b.ConcreteBuilder +} + +func (b *ComponentWorkloadBuilderBase) getRuntime() *corev1.PodSpec { + if sts, ok := b.Workload.(*appsv1.StatefulSet); ok { + return &sts.Spec.Template.Spec + } + if deploy, ok := b.Workload.(*appsv1.Deployment); ok { + return &deploy.Spec.Template.Spec + } + return nil +} + +func updateTLSVolumeAndVolumeMount(podSpec *corev1.PodSpec, clusterName string, component component.SynthesizedComponent) error { + if !component.TLS { + return nil + } + + // update volume + volumes := podSpec.Volumes + volume, err := composeTLSVolume(clusterName, component) + if err != nil { + return err + } + volumes = append(volumes, *volume) + podSpec.Volumes = volumes + + // update volumeMount + for index, container := range podSpec.Containers { + volumeMounts := container.VolumeMounts + volumeMount := composeTLSVolumeMount() + volumeMounts = append(volumeMounts, volumeMount) + podSpec.Containers[index].VolumeMounts = volumeMounts + } + + return nil +} + +func composeTLSVolume(clusterName string, component component.SynthesizedComponent) (*corev1.Volume, error) { + if !component.TLS { + return nil, fmt.Errorf("can't compose TLS volume when TLS not enabled") + } + if component.Issuer == nil { + return nil, fmt.Errorf("issuer shouldn't be nil when TLS enabled") + } + if component.Issuer.Name == appsv1alpha1.IssuerUserProvided && component.Issuer.SecretRef == nil { + return nil, fmt.Errorf("secret ref shouldn't be nil when issuer is UserProvided") + } + + var secretName, ca, cert, key string + switch component.Issuer.Name { + case appsv1alpha1.IssuerKubeBlocks: + secretName = plan.GenerateTLSSecretName(clusterName, component.Name) + ca = builder.CAName + cert = builder.CertName + key = builder.KeyName + case appsv1alpha1.IssuerUserProvided: + secretName = component.Issuer.SecretRef.Name + ca = component.Issuer.SecretRef.CA + cert = component.Issuer.SecretRef.Cert + key = component.Issuer.SecretRef.Key + } + volume := corev1.Volume{ + Name: builder.VolumeName, + VolumeSource: corev1.VolumeSource{ + Secret: &corev1.SecretVolumeSource{ + SecretName: secretName, + Items: []corev1.KeyToPath{ + {Key: ca, Path: builder.CAName}, + {Key: cert, Path: builder.CertName}, + {Key: key, Path: builder.KeyName}, + }, + Optional: func() *bool { o := false; return &o }(), + }, + }, + } + + return &volume, nil +} + +func composeTLSVolumeMount() corev1.VolumeMount { + return corev1.VolumeMount{ + Name: builder.VolumeName, + MountPath: builder.MountPath, + ReadOnly: true, + } +} diff --git a/controllers/apps/components/pod_controller.go b/controllers/apps/components/pod_controller.go deleted file mode 100644 index ea74f949f..000000000 --- a/controllers/apps/components/pod_controller.go +++ /dev/null @@ -1,116 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "context" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// PodReconciler reconciles a Pod object -type PodReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder -} - -// +kubebuilder:rbac:groups=apps,resources=pods,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps,resources=pods/status,verbs=get -// +kubebuilder:rbac:groups=apps,resources=pods/finalizers,verbs=update - -// 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.12.2/pkg/reconcile -func (r *PodReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - var ( - pod = &corev1.Pod{} - err error - cluster *appsv1alpha1.Cluster - ok bool - componentName string - componentStatus appsv1alpha1.ClusterComponentStatus - ) - - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Req: req, - Log: log.FromContext(ctx).WithValues("pod", req.NamespacedName), - } - - if err = r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, pod); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - - if cluster, err = util.GetClusterByObject(reqCtx.Ctx, r.Client, pod); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - if cluster == nil { - return intctrlutil.Reconciled() - } - - if componentName, ok = pod.Labels[constant.KBAppComponentLabelKey]; !ok { - return intctrlutil.Reconciled() - } - - if cluster.Status.Components == nil { - return intctrlutil.Reconciled() - } - if componentStatus, ok = cluster.Status.Components[componentName]; !ok { - return intctrlutil.Reconciled() - } - if componentStatus.ConsensusSetStatus == nil { - return intctrlutil.Reconciled() - } - if componentStatus.ConsensusSetStatus.Leader.Pod == util.ComponentStatusDefaultPodName { - return intctrlutil.Reconciled() - } - - // sync leader status from cluster.status - patch := client.MergeFrom(pod.DeepCopy()) - if pod.Annotations == nil { - pod.Annotations = make(map[string]string) - } - pod.Annotations[constant.LeaderAnnotationKey] = componentStatus.ConsensusSetStatus.Leader.Pod - if err = r.Client.Patch(reqCtx.Ctx, pod, patch); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - r.Recorder.Eventf(pod, corev1.EventTypeNormal, "AddAnnotation", "add annotation %s=%s", constant.LeaderAnnotationKey, componentStatus.ConsensusSetStatus.Leader.Pod) - - return intctrlutil.Reconciled() -} - -// SetupWithManager sets up the controller with the Manager. -func (r *PodReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&corev1.Pod{}). - WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.WorkloadFilterPredicate)). - Complete(r) -} diff --git a/controllers/apps/components/pod_controller_test.go b/controllers/apps/components/pod_controller_test.go deleted file mode 100644 index 02e406972..000000000 --- a/controllers/apps/components/pod_controller_test.go +++ /dev/null @@ -1,107 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" -) - -var _ = Describe("Pod Controller", func() { - - var ( - randomStr = testCtx.GetRandomStr() - clusterName = "mysql-" + randomStr - clusterDefName = "cluster-definition-consensus-" + randomStr - clusterVersionName = "cluster-version-operations-" + randomStr - ) - - const ( - revisionID = "6fdd48d9cd" - consensusCompName = "consensus" - consensusCompType = "consensus" - ) - - cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - - // delete cluster(and all dependent sub-resources), clusterversion and clusterdef - testapps.ClearClusterResources(&testCtx) - - // clear rest resources - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) - } - - BeforeEach(cleanAll) - - AfterEach(cleanAll) - - Context("test controller", func() { - It("test pod controller", func() { - - leaderName := "test-leader-name" - podName := "test-pod-name" - - By("mock cluster object") - _, _, cluster := testapps.InitConsensusMysql(testCtx, clusterDefName, - clusterVersionName, clusterName, consensusCompType, consensusCompName) - - By("mock cluster's consensus status") - Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { - cluster.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{} - cluster.Status.Components[consensusCompName] = appsv1alpha1.ClusterComponentStatus{ - ConsensusSetStatus: &appsv1alpha1.ConsensusSetStatus{ - Leader: appsv1alpha1.ConsensusMemberStatus{ - Pod: leaderName, - AccessMode: "ReadWrite", - }, - }, - } - })).Should(Succeed()) - - By("triggering pod reconcile") - pod := testapps.NewPodFactory(cluster.Namespace, podName). - AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). - AddLabels(constant.AppInstanceLabelKey, cluster.Name). - AddLabels(constant.KBAppComponentLabelKey, consensusCompName). - Create(&testCtx).GetObject() - podKey := client.ObjectKeyFromObject(pod) - - By("checking pod has leader annotation") - testapps.CheckObj(&testCtx, podKey, func(g Gomega, pod *corev1.Pod) { - g.Expect(pod.Annotations).ShouldNot(BeNil()) - g.Expect(pod.Annotations[constant.LeaderAnnotationKey]).Should(Equal(leaderName)) - }) - }) - }) -}) diff --git a/controllers/apps/components/replication/component_replication.go b/controllers/apps/components/replication/component_replication.go new file mode 100644 index 000000000..15a68aa77 --- /dev/null +++ b/controllers/apps/components/replication/component_replication.go @@ -0,0 +1,111 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/controllers/apps/components/stateful" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +func NewReplicationComponent(cli client.Client, + recorder record.EventRecorder, + cluster *appsv1alpha1.Cluster, + clusterVersion *appsv1alpha1.ClusterVersion, + synthesizedComponent *component.SynthesizedComponent, + dag *graph.DAG) *replicationComponent { + comp := &replicationComponent{ + StatefulComponentBase: internal.StatefulComponentBase{ + ComponentBase: internal.ComponentBase{ + Client: cli, + Recorder: recorder, + Cluster: cluster, + ClusterVersion: clusterVersion, + Component: synthesizedComponent, + ComponentSet: &ReplicationSet{ + Stateful: stateful.Stateful{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: nil, + ComponentDef: nil, + Component: nil, + }, + }, + }, + Dag: dag, + WorkloadVertex: nil, + }, + }, + } + comp.ComponentSet.SetComponent(comp) + return comp +} + +type replicationComponent struct { + internal.StatefulComponentBase +} + +var _ types.Component = &replicationComponent{} + +func (c *replicationComponent) newBuilder(reqCtx intctrlutil.RequestCtx, cli client.Client, + action *ictrltypes.LifecycleAction) internal.ComponentWorkloadBuilder { + builder := &replicationComponentWorkloadBuilder{ + ComponentWorkloadBuilderBase: internal.ComponentWorkloadBuilderBase{ + ReqCtx: reqCtx, + Client: cli, + Comp: c, + DefaultAction: action, + Error: nil, + EnvConfig: nil, + Workload: nil, + }, + } + builder.ConcreteBuilder = builder + return builder +} + +func (c *replicationComponent) GetWorkloadType() appsv1alpha1.WorkloadType { + return appsv1alpha1.Replication +} + +func (c *replicationComponent) GetBuiltObjects(reqCtx intctrlutil.RequestCtx, cli client.Client) ([]client.Object, error) { + return c.StatefulComponentBase.GetBuiltObjects(c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *replicationComponent) Create(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Create(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *replicationComponent) Update(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Update(reqCtx, cli, c.newBuilder(reqCtx, cli, nil)) +} + +func (c *replicationComponent) Status(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Status(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionNoopPtr())) +} diff --git a/controllers/apps/components/replication/component_replication_workload.go b/controllers/apps/components/replication/component_replication_workload.go new file mode 100644 index 000000000..5b4720311 --- /dev/null +++ b/controllers/apps/components/replication/component_replication_workload.go @@ -0,0 +1,54 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" +) + +type replicationComponentWorkloadBuilder struct { + internal.ComponentWorkloadBuilderBase +} + +var _ internal.ComponentWorkloadBuilder = &replicationComponentWorkloadBuilder{} + +func (b *replicationComponentWorkloadBuilder) BuildWorkload() internal.ComponentWorkloadBuilder { + return b.BuildWorkload4StatefulSet("replication") +} + +func (b *replicationComponentWorkloadBuilder) BuildService() internal.ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + svcList, err := builder.BuildSvcListLow(b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + if err != nil { + return nil, err + } + objs := make([]client.Object, 0, len(svcList)) + for _, svc := range svcList { + svc.Spec.Selector[constant.RoleLabelKey] = string(Primary) + objs = append(objs, svc) + } + return objs, err + } + return b.BuildWrapper(buildfn) +} diff --git a/controllers/apps/components/replication/replication.go b/controllers/apps/components/replication/replication.go new file mode 100644 index 000000000..6c81cb2fe --- /dev/null +++ b/controllers/apps/components/replication/replication.go @@ -0,0 +1,288 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/stateful" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// ReplicationSet is a component object used by Cluster, ClusterComponentDefinition and ClusterComponentSpec +type ReplicationSet struct { + stateful.Stateful +} + +var _ types.ComponentSet = &ReplicationSet{} + +func (r *ReplicationSet) getName() string { + if r.Component != nil { + return r.Component.GetName() + } + return r.ComponentSpec.Name +} + +func (r *ReplicationSet) getWorkloadType() appsv1alpha1.WorkloadType { + if r.Component != nil { + return r.Component.GetWorkloadType() + } + return r.ComponentDef.WorkloadType +} + +func (r *ReplicationSet) getReplicas() int32 { + if r.Component != nil { + return r.Component.GetReplicas() + } + return r.ComponentSpec.Replicas +} + +func (r *ReplicationSet) getPrimaryIndex() int32 { + if r.Component != nil { + return r.Component.GetPrimaryIndex() + } + return r.ComponentSpec.GetPrimaryIndex() +} + +func (r *ReplicationSet) SetComponent(comp types.Component) { + r.Component = comp +} + +// IsRunning is the implementation of the type Component interface method, +// which is used to check whether the replicationSet component is running normally. +func (r *ReplicationSet) IsRunning(ctx context.Context, obj client.Object) (bool, error) { + var componentStatusIsRunning = true + sts := util.ConvertToStatefulSet(obj) + isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, r.Cli, sts) + if err != nil { + return false, err + } + stsIsReady := util.StatefulSetOfComponentIsReady(sts, isRevisionConsistent, nil) + if !stsIsReady { + return false, nil + } + if sts.Status.AvailableReplicas < r.getReplicas() { + componentStatusIsRunning = false + } + return componentStatusIsRunning, nil +} + +// PodsReady is the implementation of the type Component interface method, +// which is used to check whether all the pods of replicationSet component are ready. +func (r *ReplicationSet) PodsReady(ctx context.Context, obj client.Object) (bool, error) { + return r.Stateful.PodsReady(ctx, obj) +} + +// PodIsAvailable is the implementation of the type Component interface method, +// Check whether the status of a Pod of the replicationSet is ready, including the role label on the Pod +func (r *ReplicationSet) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { + if pod == nil { + return false + } + return intctrlutil.PodIsReadyWithLabel(*pod) +} + +func (r *ReplicationSet) GetPhaseWhenPodsReadyAndProbeTimeout(pods []*corev1.Pod) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap) { + return "", nil +} + +// GetPhaseWhenPodsNotReady is the implementation of the type Component interface method, +// when the pods of replicationSet are not ready, calculate the component phase is Failed or Abnormal. +// if return an empty phase, means the pods of component are ready and skips it. +func (r *ReplicationSet) GetPhaseWhenPodsNotReady(ctx context.Context, + componentName string) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) { + stsList := &appsv1.StatefulSetList{} + podList, err := util.GetCompRelatedObjectList(ctx, r.Cli, *r.Cluster, + componentName, stsList) + if err != nil || len(stsList.Items) == 0 { + return "", nil, err + } + stsObj := stsList.Items[0] + podCount := len(podList.Items) + componentReplicas := r.getReplicas() + if podCount == 0 || stsObj.Status.AvailableReplicas == 0 { + return util.GetPhaseWithNoAvailableReplicas(componentReplicas), nil, nil + } + // get the statefulSet of component + var ( + existLatestRevisionFailedPod bool + primaryIsReady bool + statusMessages appsv1alpha1.ComponentMessageMap + ) + for _, v := range podList.Items { + // if the pod is terminating, ignore it + if v.DeletionTimestamp != nil { + return "", nil, nil + } + labelValue := v.Labels[constant.RoleLabelKey] + if labelValue == string(Primary) && intctrlutil.PodIsReady(&v) { + primaryIsReady = true + continue + } + if labelValue == "" { + // REVIEW: this isn't a get function, where r.Cluster.Status.Components is being updated. + // patch abnormal reason to cluster.status.ComponentDefs. + if statusMessages == nil { + statusMessages = appsv1alpha1.ComponentMessageMap{} + } + statusMessages.SetObjectMessage(v.Kind, v.Name, "empty label for pod, please check.") + } + if !intctrlutil.PodIsReady(&v) && intctrlutil.PodIsControlledByLatestRevision(&v, &stsObj) { + existLatestRevisionFailedPod = true + } + } + return util.GetCompPhaseByConditions(existLatestRevisionFailedPod, primaryIsReady, + componentReplicas, int32(podCount), stsObj.Status.AvailableReplicas), statusMessages, nil +} + +func (r *ReplicationSet) HandleRestart(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + sts := util.ConvertToStatefulSet(obj) + if sts.Generation != sts.Status.ObservedGeneration { + return nil, nil + } + vertexes := make([]graph.Vertex, 0) + pods, err := util.GetPods4Delete(ctx, r.Cli, sts) + if err != nil { + return nil, err + } + for _, pod := range pods { + vertexes = append(vertexes, &ictrltypes.LifecycleVertex{ + Obj: pod, + Action: ictrltypes.ActionDeletePtr(), + Orphan: true, + }) + } + return vertexes, nil +} + +func (r *ReplicationSet) HandleRoleChange(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + podList, err := r.getRunningPods(ctx, obj) + if err != nil { + return nil, err + } + if len(podList) == 0 { + return nil, nil + } + vertexes := make([]graph.Vertex, 0) + podsToSyncStatus := make([]*corev1.Pod, 0) + for i := range podList { + pod := &podList[i] + // if there is no role label on the Pod, it needs to be updated with statefulSet's role label. + if v, ok := pod.Labels[constant.RoleLabelKey]; !ok || v == "" { + _, o := util.ParseParentNameAndOrdinal(pod.Name) + role := string(Secondary) + if o == r.getPrimaryIndex() { + role = string(Primary) + } + pod.GetLabels()[constant.RoleLabelKey] = role + vertexes = append(vertexes, &ictrltypes.LifecycleVertex{ + Obj: pod, + Action: ictrltypes.ActionUpdatePtr(), // update or patch? + }) + } + // else { + // podsToSyncStatus = append(podsToSyncStatus, pod) + // } + podsToSyncStatus = append(podsToSyncStatus, pod) + } + // // REVIEW/TODO: (Y-Rookie) + // // 1. should employ rolling deletion as default strategy instead of delete them all. + // if err := util.DeleteStsPods(ctx, r.Cli, sts); err != nil { + // return err + // } + // sync cluster.spec.componentSpecs.[x].primaryIndex when failover occurs and switchPolicy is Noop. + // TODO(refactor): syncPrimaryIndex will update cluster spec, resolve it. + if err := syncPrimaryIndex(ctx, r.Cli, r.Cluster, r.getName()); err != nil { + return nil, err + } + // sync cluster.status.components.replicationSet.status + if err := syncReplicationSetClusterStatus(r.Cluster, r.getWorkloadType(), r.getName(), podsToSyncStatus); err != nil { + return nil, err + } + return vertexes, nil +} + +// TODO(refactor): imple HandleHA asynchronously + +func (r *ReplicationSet) HandleHA(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + pods, err := r.getRunningPods(ctx, obj) + if err != nil { + return nil, err + } + if len(pods) == 0 { + return nil, nil + } + // If the Pods already exists, check whether there is a HA switching and the HA process is prioritized to handle. + // TODO(xingran) After refactoring, HA switching will be handled in the replicationSet controller. + primaryIndexChanged, _, err := CheckPrimaryIndexChanged(ctx, r.Cli, r.Cluster, r.getName(), r.getPrimaryIndex()) + if err != nil { + return nil, err + } + if primaryIndexChanged { + compSpec := util.GetClusterComponentSpecByName(*r.Cluster, r.getName()) + if err := HandleReplicationSetHASwitch(ctx, r.Cli, r.Cluster, compSpec); err != nil { + return nil, err + } + } + return nil, nil +} + +func (r *ReplicationSet) getRunningPods(ctx context.Context, obj client.Object) ([]corev1.Pod, error) { + sts := util.ConvertToStatefulSet(obj) + if sts.Generation != sts.Status.ObservedGeneration { + return nil, nil + } + return util.GetPodListByStatefulSet(ctx, r.Cli, sts) +} + +func newReplicationSet(cli client.Client, + cluster *appsv1alpha1.Cluster, + spec *appsv1alpha1.ClusterComponentSpec, + def appsv1alpha1.ClusterComponentDefinition) *ReplicationSet { + return &ReplicationSet{ + Stateful: stateful.Stateful{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: spec, + ComponentDef: &def, + Component: nil, + }, + }, + } +} + +func DefaultRole(i int32) string { + role := string(Secondary) + if i == 0 { + role = string(Primary) + } + return role +} diff --git a/controllers/apps/components/replicationset/replication_set_switch.go b/controllers/apps/components/replication/replication_switch.go similarity index 89% rename from controllers/apps/components/replicationset/replication_set_switch.go rename to controllers/apps/components/replication/replication_switch.go index 469262879..ac22d424c 100644 --- a/controllers/apps/components/replicationset/replication_set_switch.go +++ b/controllers/apps/components/replication/replication_switch.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package replicationset +package replication import ( "context" @@ -59,7 +62,7 @@ type SwitchInstance struct { SecondariesRole []*SwitchRoleInfo } -// SwitchRoleInfo is used to record the role information including health detection, role detection, data delay detection info, etc. +// SwitchRoleInfo is used to record the role information including health detection, role detection, replication lag detection info, etc. type SwitchRoleInfo struct { // k8s pod obj Pod *corev1.Pod @@ -89,13 +92,13 @@ type SwitchPhaseStatus string // SwitchPhase defines the phase of switching. type SwitchPhase string -// SwitchDetectManager is an interface to implement various detections that high-availability depends on, including health detection, role detection, data delay detection, etc. +// SwitchDetectManager is an interface to implement various detections that high-availability depends on, including health detection, role detection, replication lag detection, etc. type SwitchDetectManager interface { // healthDetect is used to implement Pod health detection healthDetect(pod *corev1.Pod) (*HealthDetectResult, error) // roleDetect is used to detect the role of the Pod in the database kernel roleDetect(pod *corev1.Pod) (*RoleDetectResult, error) - // lagDetect is used to detect the data delay between the secondary and the primary + // lagDetect is used to detect the replication lag between the secondary and the primary lagDetect(pod *corev1.Pod) (*LagDetectResult, error) } @@ -145,7 +148,7 @@ const ( // detection implements the detection logic and saves the detection results to the SwitchRoleInfo of the corresponding role pod of the SwitchInstance, // if skipSecondary is true, the detection logic of the secondaries will be skipped, which is used in some scenarios where there is no need to detect the secondary, -// currently supported detection types are health detection, role detection, and delay detection. +// currently supported detection types are health detection, role detection, and lag detection. func (s *Switch) detection(skipSecondary bool) { s.SwitchStatus.SwitchPhase = SwitchPhaseDetect s.SwitchStatus.SwitchPhaseStatus = SwitchPhaseStatusExecuting @@ -235,7 +238,7 @@ func (s *Switch) election() *SwitchRoleInfo { } // do election priority - // TODO(xingran): the secondary with the smallest data delay is selected as the candidate primary currently, and more rules can be added in the future + // TODO(xingran): the secondary with the smallest replication lag is selected as the candidate primary currently, and more rules can be added in the future sort.Sort(SwitchRoleInfoList(filterRoles)) s.SwitchStatus.SwitchPhaseStatus = SwitchPhaseStatusSucceed return filterRoles[0] @@ -281,7 +284,7 @@ func (s *Switch) decision() bool { } makeMaxAvailabilityDecision := func() bool { - // old primary is alive, check the data delay of candidate primary + // old primary is alive, check the replication lag of candidate primary if *s.SwitchInstance.OldPrimaryRole.HealthDetectInfo { // The LagDetectInfo is 0, which means that the primary and the secondary data are consistent and can be switched if *s.SwitchInstance.CandidatePrimaryRole.LagDetectInfo == 0 { @@ -371,9 +374,9 @@ func (s *Switch) updateRoleLabel() error { // initSwitchInstance initializes the switchInstance object without detection info according to the pod list under the component, // and the detection information will be filled in the detection phase. func (s *Switch) initSwitchInstance(oldPrimaryIndex, newPrimaryIndex int32) error { - var stsList = &appsv1.StatefulSetList{} + var podList = &corev1.PodList{} if err := utils.GetObjectListByComponentName(s.SwitchResource.Ctx, s.SwitchResource.Cli, - *s.SwitchResource.Cluster, stsList, s.SwitchResource.CompSpec.Name); err != nil { + *s.SwitchResource.Cluster, podList, s.SwitchResource.CompSpec.Name); err != nil { return err } if s.SwitchInstance == nil { @@ -383,35 +386,15 @@ func (s *Switch) initSwitchInstance(oldPrimaryIndex, newPrimaryIndex int32) erro SecondariesRole: make([]*SwitchRoleInfo, 0), } } - for _, sts := range stsList.Items { - pod, err := getAndCheckReplicationPodByStatefulSet(s.SwitchResource.Ctx, s.SwitchResource.Cli, &sts) - if err != nil { - return err - } + for _, pod := range podList.Items { sri := &SwitchRoleInfo{ - Pod: pod, + Pod: &pod, HealthDetectInfo: nil, RoleDetectInfo: nil, LagDetectInfo: nil, } - - // because the first sts is named differently than the other sts, special handling is required here. - // TODO: The following code is not very elegant, and it is recommended to be optimized in the future. - clusterCompName := fmt.Sprintf("%s-%s", s.SwitchResource.Cluster.GetName(), s.SwitchResource.CompSpec.Name) - if sts.GetName() == clusterCompName { - if oldPrimaryIndex == 0 { - s.SwitchInstance.OldPrimaryRole = sri - continue - } - if newPrimaryIndex == 0 { - s.SwitchInstance.CandidatePrimaryRole = sri - continue - } - s.SwitchInstance.SecondariesRole = append(s.SwitchInstance.SecondariesRole, sri) - continue - } - - switch int32(utils.GetOrdinalSts(&sts)) { + _, o := utils.ParseParentNameAndOrdinal(pod.Name) + switch o { case oldPrimaryIndex: s.SwitchInstance.OldPrimaryRole = sri case newPrimaryIndex: diff --git a/controllers/apps/components/replicationset/replication_set_switch_test.go b/controllers/apps/components/replication/replication_switch_test.go similarity index 82% rename from controllers/apps/components/replicationset/replication_set_switch_test.go rename to controllers/apps/components/replication/replication_switch_test.go index 2008dcc19..70cb4bca8 100644 --- a/controllers/apps/components/replicationset/replication_set_switch_test.go +++ b/controllers/apps/components/replication/replication_switch_test.go @@ -1,26 +1,30 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package replicationset +package replication import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -58,7 +62,7 @@ var _ = Describe("ReplicationSet Switch", func() { ) cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -98,7 +102,7 @@ var _ = Describe("ReplicationSet Switch", func() { By("Creating a cluster with replication workloadType.") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). SetReplicas(testapps.DefaultReplicationReplicas). SetPrimaryIndex(DefaultPrimaryIndexDiffWithStsOrdinal). SetSwitchPolicy(clusterSwitchPolicy). @@ -110,38 +114,28 @@ var _ = Describe("ReplicationSet Switch", func() { Image: testapps.DefaultRedisImageName, ImagePullPolicy: corev1.PullIfNotPresent, } - stsList := make([]*appsv1.StatefulSet, 0) - for k, v := range map[string]string{ - clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-0": string(Primary), - clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-1": string(Secondary), - clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-2": string(Secondary), - clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-3": string(Secondary), - } { - sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, k, clusterObj.Name, testapps.DefaultRedisCompName). - AddContainer(container). - AddAppInstanceLabel(clusterObj.Name). - AddAppComponentLabel(testapps.DefaultRedisCompName). - AddAppManangedByLabel(). - AddRoleLabel(v). - SetReplicas(1). - Create(&testCtx).GetObject() - isStsPrimary, err := checkObjRoleLabelIsPrimary(sts) - if v == string(Primary) { - Expect(err).To(Succeed()) - Expect(isStsPrimary).Should(BeTrue()) - } else { - Expect(err).To(Succeed()) - Expect(isStsPrimary).ShouldNot(BeTrue()) - } - stsList = append(stsList, sts) - } + + replicationSetSts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, + clusterObj.Name+"-"+testapps.DefaultRedisCompName, clusterObj.Name, testapps.DefaultRedisCompName). + AddContainer(container). + AddAppInstanceLabel(clusterObj.Name). + AddAppComponentLabel(testapps.DefaultRedisCompName). + AddAppManangedByLabel(). + SetReplicas(4). + Create(&testCtx).GetObject() By("Creating Pods of replication workloadType.") - for _, sts := range stsList { - _ = testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). + for i := int32(0); i < *replicationSetSts.Spec.Replicas; i++ { + podBuilder := testapps.NewPodFactory(testCtx.DefaultNamespace, + fmt.Sprintf("%s-%d", replicationSetSts.Name, i)). AddContainer(container). - AddLabelsInMap(sts.Labels). - Create(&testCtx).GetObject() + AddLabelsInMap(replicationSetSts.Labels) + if i == 0 { + podBuilder.AddRoleLabel(string(Primary)) + } else { + podBuilder.AddRoleLabel(string(Secondary)) + } + _ = podBuilder.Create(&testCtx).GetObject() } clusterComponentSpec := &clusterObj.Spec.ComponentSpecs[0] @@ -162,7 +156,7 @@ var _ = Describe("ReplicationSet Switch", func() { Expect(s.SwitchStatus.SwitchPhaseStatus).Should(Equal(SwitchPhaseStatusSucceed)) By("Test switch election with multi secondaries should be successful, and the candidate primary should be the priorityPod.") - priorityPod := clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-2-0" + priorityPod := clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-3" for _, sri := range s.SwitchInstance.SecondariesRole { if sri.Pod.Name != priorityPod { sri.LagDetectInfo = &lagNotZero @@ -239,7 +233,7 @@ var _ = Describe("ReplicationSet Switch", func() { BeforeEach(func() { By("Mock a replicationSpec with SwitchPolicy and SwitchCmdExecutorConfig.") - mockReplicationSpec := &appsv1alpha1.ReplicationSpec{ + mockReplicationSpec := &appsv1alpha1.ReplicationSetSpec{ SwitchPolicies: []appsv1alpha1.SwitchPolicy{ { Type: appsv1alpha1.MaximumAvailability, @@ -289,13 +283,13 @@ var _ = Describe("ReplicationSet Switch", func() { } By("Create a clusterDefinition obj with replication workloadType.") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). + AddComponentDef(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompDefName). AddReplicationSpec(mockReplicationSpec). Create(&testCtx).GetObject() By("Create a clusterVersion obj with replication workloadType.") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(testapps.DefaultRedisCompType).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). + AddComponentVersion(testapps.DefaultRedisCompDefName).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). Create(&testCtx).GetObject() }) diff --git a/controllers/apps/components/replicationset/replication_set_switch_utils.go b/controllers/apps/components/replication/replication_switch_utils.go similarity index 88% rename from controllers/apps/components/replicationset/replication_set_switch_utils.go rename to controllers/apps/components/replication/replication_switch_utils.go index 00946ab42..7277cc2cd 100644 --- a/controllers/apps/components/replicationset/replication_set_switch_utils.go +++ b/controllers/apps/components/replication/replication_switch_utils.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package replicationset +package replication import ( "context" @@ -296,7 +299,7 @@ func (pdm *ProbeDetectManager) roleDetect(pod *corev1.Pod) (*RoleDetectResult, e return &res, nil } -// lagDetect is the implementation of the SwitchDetectManager interface, which gets data delay detection information by actively calling the API provided by the probe +// lagDetect is the implementation of the SwitchDetectManager interface, which gets replication lag detection information by actively calling the API provided by the probe // TODO(xingran) Wait for the probe interface to be ready before implementation func (pdm *ProbeDetectManager) lagDetect(pod *corev1.Pod) (*LagDetectResult, error) { var res LagDetectResult = 0 @@ -305,7 +308,7 @@ func (pdm *ProbeDetectManager) lagDetect(pod *corev1.Pod) (*LagDetectResult, err // getSwitchStatementsBySwitchPolicyType gets the SwitchStatements corresponding to switchPolicyType func getSwitchStatementsBySwitchPolicyType(switchPolicyType appsv1alpha1.SwitchPolicyType, - replicationSpec *appsv1alpha1.ReplicationSpec) (*appsv1alpha1.SwitchStatements, error) { + replicationSpec *appsv1alpha1.ReplicationSetSpec) (*appsv1alpha1.SwitchStatements, error) { if replicationSpec == nil || len(replicationSpec.SwitchPolicies) == 0 { return nil, fmt.Errorf("replicationSpec and replicationSpec.SwitchPolicies can not be nil") } @@ -463,7 +466,7 @@ func cleanSwitchCmdJobs(s *Switch) error { return nil } -// getSwitchCmdJobLabel gets the labels for job that execute the switch commands. +// getSwitchCmdJobLabel gets the labels for job that executes the switch commands. func getSwitchCmdJobLabel(clusterName, componentName string) map[string]string { return map[string]string{ constant.AppInstanceLabelKey: clusterName, @@ -481,18 +484,44 @@ func CheckPrimaryIndexChanged(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, compName string, - specPrimaryIndex int32) (bool, int32, error) { + currentPrimaryIndex int32) (bool, int32, error) { // get the statefulSet object whose current role label is primary - primarySts, err := getReplicationSetPrimaryObj(ctx, cli, cluster, generics.StatefulSetSignature, compName) + pod, err := getReplicationSetPrimaryObj(ctx, cli, cluster, generics.PodSignature, compName) if err != nil { return false, -1, err } - - clusterCompName := fmt.Sprintf("%s-%s", cluster.GetName(), compName) - if primarySts.GetName() == clusterCompName { - return specPrimaryIndex != 0, 0, nil + if pod == nil { + return false, -1, nil } + _, o := util.ParseParentNameAndOrdinal(pod.Name) + return currentPrimaryIndex != o, o, nil +} - currentPrimaryIndex := int32(util.GetOrdinalSts(primarySts)) - return specPrimaryIndex != currentPrimaryIndex, currentPrimaryIndex, nil +// syncPrimaryIndex syncs cluster.spec.componentSpecs.[x].primaryIndex when failover occurs and switchPolicy is Noop. +func syncPrimaryIndex(ctx context.Context, + cli client.Client, + cluster *appsv1alpha1.Cluster, + compName string) error { + clusterCompSpec := util.GetClusterComponentSpecByName(*cluster, compName) + if clusterCompSpec == nil || clusterCompSpec.SwitchPolicy == nil || clusterCompSpec.SwitchPolicy.Type != appsv1alpha1.Noop { + return nil + } + isChanged, currentPrimaryIndex, err := CheckPrimaryIndexChanged(ctx, cli, cluster, compName, clusterCompSpec.GetPrimaryIndex()) + if err != nil { + return err + } + // if primaryIndex is changed, sync cluster.spec.componentSpecs.[x].primaryIndex + if isChanged { + clusterDeepCopy := cluster.DeepCopy() + for index := range cluster.Spec.ComponentSpecs { + if cluster.Spec.ComponentSpecs[index].Name == compName { + cluster.Spec.ComponentSpecs[index].PrimaryIndex = ¤tPrimaryIndex + break + } + } + if err := cli.Patch(ctx, cluster, client.MergeFrom(clusterDeepCopy)); err != nil { + return err + } + } + return nil } diff --git a/controllers/apps/components/replicationset/replication_set_switch_utils_test.go b/controllers/apps/components/replication/replication_switch_utils_test.go similarity index 75% rename from controllers/apps/components/replicationset/replication_set_switch_utils_test.go rename to controllers/apps/components/replication/replication_switch_utils_test.go index f284cdbc7..3ec129a54 100644 --- a/controllers/apps/components/replicationset/replication_set_switch_utils_test.go +++ b/controllers/apps/components/replication/replication_switch_utils_test.go @@ -1,26 +1,30 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package replicationset +package replication import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,7 +48,7 @@ var _ = Describe("ReplicationSet Switch Util", func() { ) cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -75,7 +79,7 @@ var _ = Describe("ReplicationSet Switch Util", func() { By("Creating a cluster with replication workloadType.") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). SetReplicas(testapps.DefaultReplicationReplicas). SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). SetSwitchPolicy(clusterSwitchPolicy). @@ -87,35 +91,24 @@ var _ = Describe("ReplicationSet Switch Util", func() { Image: testapps.DefaultRedisImageName, ImagePullPolicy: corev1.PullIfNotPresent, } - stsList := make([]*appsv1.StatefulSet, 0) - for k, v := range map[string]string{ - string(Primary): clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-0", - string(Secondary): clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-1", - } { - sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, v, clusterObj.Name, testapps.DefaultRedisCompName). - AddContainer(container). - AddAppInstanceLabel(clusterObj.Name). - AddAppComponentLabel(testapps.DefaultRedisCompName). - AddAppManangedByLabel(). - AddRoleLabel(k). - SetReplicas(1). - Create(&testCtx).GetObject() - isStsPrimary, err := checkObjRoleLabelIsPrimary(sts) - if k == string(Primary) { - Expect(err).To(Succeed()) - Expect(isStsPrimary).Should(BeTrue()) - } else { - Expect(err).To(Succeed()) - Expect(isStsPrimary).ShouldNot(BeTrue()) - } - stsList = append(stsList, sts) - } + sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, + clusterObj.Name+"-"+testapps.DefaultRedisCompName, + clusterObj.Name, + testapps.DefaultRedisCompName). + AddContainer(container). + AddAppInstanceLabel(clusterObj.Name). + AddAppComponentLabel(testapps.DefaultRedisCompName). + AddAppManangedByLabel(). + SetReplicas(2). + Create(&testCtx).GetObject() By("Creating Pods of replication workloadType.") - for _, sts := range stsList { - _ = testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). + + for i := int32(0); i < *sts.Spec.Replicas; i++ { + _ = testapps.NewPodFactory(testCtx.DefaultNamespace, fmt.Sprintf("%s-%d", sts.Name, i)). AddContainer(container). AddLabelsInMap(sts.Labels). + AddRoleLabel(DefaultRole(i)). Create(&testCtx).GetObject() } clusterComponentSpec := &clusterObj.Spec.ComponentSpecs[0] @@ -131,12 +124,14 @@ var _ = Describe("ReplicationSet Switch Util", func() { Expect(err).Should(Succeed()) By("Test update cluster component primaryIndex should be successful.") - testapps.UpdateClusterCompSpecPrimaryIndex(&testCtx, clusterObj, clusterComponentSpec.Name, &DefaultPrimaryIndexDiffWithStsOrdinal) + testapps.UpdateClusterCompSpecPrimaryIndex(&testCtx, clusterObj, clusterComponentSpec.Name, + &DefaultPrimaryIndexDiffWithStsOrdinal) By("Test new Switch obj and init SwitchInstance should be successful.") clusterObj.Spec.ComponentSpecs[0].PrimaryIndex = &DefaultPrimaryIndexDiffWithStsOrdinal clusterComponentSpec.PrimaryIndex = &DefaultPrimaryIndexDiffWithStsOrdinal - s := newSwitch(testCtx.Ctx, k8sClient, clusterObj, &clusterDefObj.Spec.ComponentDefs[0], clusterComponentSpec, nil, nil, nil, nil, nil) + s := newSwitch(testCtx.Ctx, k8sClient, clusterObj, &clusterDefObj.Spec.ComponentDefs[0], clusterComponentSpec, + nil, nil, nil, nil, nil) err = s.initSwitchInstance(DefaultReplicationPrimaryIndex, DefaultPrimaryIndexDiffWithStsOrdinal) Expect(err).Should(Succeed()) @@ -156,7 +151,7 @@ var _ = Describe("ReplicationSet Switch Util", func() { BeforeEach(func() { By("Mock a replicationSpec with SwitchPolicy and SwitchCmdExecutorConfig.") - replicationSpec := &appsv1alpha1.ReplicationSpec{ + replicationSpec := &appsv1alpha1.ReplicationSetSpec{ SwitchPolicies: []appsv1alpha1.SwitchPolicy{ { Type: appsv1alpha1.MaximumAvailability, @@ -206,13 +201,13 @@ var _ = Describe("ReplicationSet Switch Util", func() { } By("Create a clusterDefinition obj with replication workloadType.") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). + AddComponentDef(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompDefName). AddReplicationSpec(replicationSpec). Create(&testCtx).GetObject() By("Create a clusterVersion obj with replication workloadType.") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(testapps.DefaultRedisCompType).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). + AddComponentVersion(testapps.DefaultRedisCompDefName).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). Create(&testCtx).GetObject() }) diff --git a/controllers/apps/components/replication/replication_test.go b/controllers/apps/components/replication/replication_test.go new file mode 100644 index 000000000..825336c27 --- /dev/null +++ b/controllers/apps/components/replication/replication_test.go @@ -0,0 +1,211 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" +) + +var _ = Describe("Replication Component", func() { + var ( + clusterName = "test-cluster-repl" + clusterDefName = "test-cluster-def-repl" + clusterVersionName = "test-cluster-version-repl" + controllerRivision = "mock-revision" + ) + + var ( + clusterDefObj *appsv1alpha1.ClusterDefinition + clusterVersionObj *appsv1alpha1.ClusterVersion + clusterObj *appsv1alpha1.Cluster + ) + + cleanAll := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + // delete cluster(and all dependent sub-resources), clusterversion and clusterdef + testapps.ClearClusterResources(&testCtx) + + // clear rest resources + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + // namespaced resources + testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) + testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) + } + + BeforeEach(cleanAll) + + AfterEach(cleanAll) + + Context("Replication Component test", func() { + It("Replication Component test", func() { + + By("Create a clusterDefinition obj with replication workloadType.") + clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompDefName). + Create(&testCtx).GetObject() + + By("Create a clusterVersion obj with replication workloadType.") + clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). + AddComponentVersion(testapps.DefaultRedisCompDefName).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). + Create(&testCtx).GetObject() + + By("Creating a cluster with replication workloadType.") + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). + SetReplicas(testapps.DefaultReplicationReplicas). + Create(&testCtx).GetObject() + + // mock cluster is Running + Expect(testapps.ChangeObjStatus(&testCtx, clusterObj, func() { + clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ + testapps.DefaultRedisCompName: { + Phase: appsv1alpha1.RunningClusterCompPhase, + }, + } + })).Should(Succeed()) + + By("Creating statefulSet of replication workloadType.") + replicas := int32(2) + status := appsv1.StatefulSetStatus{ + AvailableReplicas: replicas, + ObservedGeneration: 1, + Replicas: replicas, + ReadyReplicas: replicas, + UpdatedReplicas: replicas, + CurrentRevision: controllerRivision, + UpdateRevision: controllerRivision, + } + + replicationSetSts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, + clusterObj.Name+"-"+testapps.DefaultRedisCompName, clusterObj.Name, testapps.DefaultRedisCompName). + AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). + AddAppInstanceLabel(clusterObj.Name). + AddAppComponentLabel(testapps.DefaultRedisCompName). + AddAppManangedByLabel(). + SetReplicas(replicas). + Create(&testCtx).GetObject() + stsObjectKey := client.ObjectKey{Name: replicationSetSts.Name, Namespace: testCtx.DefaultNamespace} + + Expect(replicationSetSts.Spec.VolumeClaimTemplates).Should(BeEmpty()) + + compDefName := clusterObj.Spec.GetComponentDefRefName(testapps.DefaultRedisCompName) + componentDef := clusterDefObj.GetComponentDefByName(compDefName) + component := clusterObj.Spec.GetComponentByName(testapps.DefaultRedisCompName) + replicationComponent := newReplicationSet(k8sClient, clusterObj, component, *componentDef) + var podList []*corev1.Pod + + for _, availableReplica := range []int32{0, replicas} { + status.AvailableReplicas = availableReplica + replicationSetSts.Status = status + testk8s.PatchStatefulSetStatus(&testCtx, replicationSetSts.Name, status) + + if availableReplica > 0 { + // Create pods of the statefulset + stsPods := testapps.MockReplicationComponentPods(nil, testCtx, replicationSetSts, clusterObj.Name, + testapps.DefaultRedisCompName, map[int32]string{ + 0: string(Primary), + 1: string(Secondary), + }) + podList = append(podList, stsPods...) + By("Testing pods are ready") + podsReady, _ := replicationComponent.PodsReady(ctx, replicationSetSts) + Expect(podsReady).Should(BeTrue()) + By("Testing component is running") + isRunning, _ := replicationComponent.IsRunning(ctx, replicationSetSts) + Expect(isRunning).Should(BeTrue()) + } else { + podsReady, _ := replicationComponent.PodsReady(ctx, replicationSetSts) + By("Testing pods are not ready") + Expect(podsReady).Should(BeFalse()) + By("Testing component is not running") + isRunning, _ := replicationComponent.IsRunning(ctx, replicationSetSts) + Expect(isRunning).Should(BeFalse()) + } + } + + // TODO(refactor): probe timed-out pod + // By("Testing handle probe timed out") + // requeue, _ := replicationComponent.HandleProbeTimeoutWhenPodsReady(ctx, nil) + // Expect(requeue == false).Should(BeTrue()) + + By("Testing pod is available") + primaryPod := podList[0] + Expect(replicationComponent.PodIsAvailable(primaryPod, 10)).Should(BeTrue()) + + By("Testing component phase when pods not ready") + // mock secondary pod is not ready. + testk8s.UpdatePodStatusScheduleFailed(ctx, testCtx, podList[1].Name, podList[1].Namespace) + status.AvailableReplicas -= 1 + testk8s.PatchStatefulSetStatus(&testCtx, replicationSetSts.Name, status) + phase, _, _ := replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) + Expect(phase).Should(Equal(appsv1alpha1.AbnormalClusterCompPhase)) + + // mock primary pod label is empty + Expect(testapps.ChangeObj(&testCtx, primaryPod, func(lpod *corev1.Pod) { + lpod.Labels[constant.RoleLabelKey] = "" + })).Should(Succeed()) + phase, _, _ = replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) + Expect(phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) + _, statusMessages, _ := replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) + Expect(statusMessages[fmt.Sprintf("%s/%s", primaryPod.Kind, primaryPod.Name)]). + Should(ContainSubstring("empty label for pod, please check")) + + By("Checking if the pod is not updated when statefulset is not updated") + Expect(testCtx.Cli.Get(testCtx.Ctx, stsObjectKey, replicationSetSts)).Should(Succeed()) + vertexes, err := replicationComponent.HandleRestart(ctx, replicationSetSts) + Expect(err).To(Succeed()) + Expect(len(vertexes)).To(Equal(0)) + pods, err := util.GetPodListByStatefulSet(ctx, k8sClient, replicationSetSts) + Expect(err).To(Succeed()) + Expect(len(pods)).To(Equal(int(replicas))) + Expect(util.IsStsAndPodsRevisionConsistent(ctx, k8sClient, replicationSetSts)).Should(BeTrue()) + + By("Checking if the pod is deleted when statefulset is updated") + status.UpdateRevision = "new-mock-revision" + testk8s.PatchStatefulSetStatus(&testCtx, replicationSetSts.Name, status) + Expect(testCtx.Cli.Get(testCtx.Ctx, stsObjectKey, replicationSetSts)).Should(Succeed()) + vertexes, err = replicationComponent.HandleRestart(ctx, replicationSetSts) + Expect(err).To(Succeed()) + Expect(len(vertexes)).To(Equal(int(replicas))) + Expect(*vertexes[0].(*ictrltypes.LifecycleVertex).Action == ictrltypes.DELETE).To(BeTrue()) + }) + }) +}) diff --git a/controllers/apps/components/replication/replication_utils.go b/controllers/apps/components/replication/replication_utils.go new file mode 100644 index 000000000..c40ce3fec --- /dev/null +++ b/controllers/apps/components/replication/replication_utils.go @@ -0,0 +1,253 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "context" + "fmt" + "reflect" + + "golang.org/x/exp/slices" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" +) + +type ReplicationRole string + +const ( + Primary ReplicationRole = "primary" + Secondary ReplicationRole = "secondary" +) + +// syncReplicationSetClusterStatus syncs replicationSet pod status to cluster.status.component[componentName].ReplicationStatus. +func syncReplicationSetClusterStatus(cluster *appsv1alpha1.Cluster, + workloadType appsv1alpha1.WorkloadType, compName string, podList []*corev1.Pod) error { + if len(podList) == 0 { + return nil + } + + replicationStatus := cluster.Status.Components[compName].ReplicationSetStatus + if replicationStatus == nil { + if err := util.InitClusterComponentStatusIfNeed(cluster, compName, workloadType); err != nil { + return err + } + replicationStatus = cluster.Status.Components[compName].ReplicationSetStatus + } + return syncReplicationSetStatus(replicationStatus, podList) +} + +// syncReplicationSetStatus syncs the target pod info in cluster.status.components. +func syncReplicationSetStatus(replicationStatus *appsv1alpha1.ReplicationSetStatus, podList []*corev1.Pod) error { + for _, pod := range podList { + role := pod.Labels[constant.RoleLabelKey] + if role == "" { + return fmt.Errorf("pod %s has no role label", pod.Name) + } + if role == string(Primary) { + if replicationStatus.Primary.Pod == pod.Name { + continue + } + replicationStatus.Primary.Pod = pod.Name + // if current primary pod in secondary list, it means the primary pod has been switched, remove it. + for index, secondary := range replicationStatus.Secondaries { + if secondary.Pod == pod.Name { + replicationStatus.Secondaries = append(replicationStatus.Secondaries[:index], replicationStatus.Secondaries[index+1:]...) + break + } + } + } else { + var exist = false + for _, secondary := range replicationStatus.Secondaries { + if secondary.Pod == pod.Name { + exist = true + break + } + } + if !exist { + replicationStatus.Secondaries = append(replicationStatus.Secondaries, appsv1alpha1.ReplicationMemberStatus{ + Pod: pod.Name, + }) + } + } + } + return nil +} + +// removeTargetPodsInfoInStatus removes the target pod info from cluster.status.components. +func removeTargetPodsInfoInStatus(replicationStatus *appsv1alpha1.ReplicationSetStatus, + targetPodList []*corev1.Pod, + componentReplicas int32) error { + if replicationStatus == nil { + return nil + } + targetPodNameMap := make(map[string]struct{}) + for _, pod := range targetPodList { + targetPodNameMap[pod.Name] = struct{}{} + } + if _, ok := targetPodNameMap[replicationStatus.Primary.Pod]; ok { + if componentReplicas != 0 { + return fmt.Errorf("primary pod cannot be removed") + } + replicationStatus.Primary = appsv1alpha1.ReplicationMemberStatus{ + Pod: constant.ComponentStatusDefaultPodName, + } + } + newSecondaries := make([]appsv1alpha1.ReplicationMemberStatus, 0) + for _, secondary := range replicationStatus.Secondaries { + if _, ok := targetPodNameMap[secondary.Pod]; ok { + continue + } + // add pod that do not need to be removed to newSecondaries slice. + newSecondaries = append(newSecondaries, secondary) + } + replicationStatus.Secondaries = newSecondaries + return nil +} + +// checkObjRoleLabelIsPrimary checks whether it is the primary obj(statefulSet or pod) by the label tag on obj. +func checkObjRoleLabelIsPrimary[T generics.Object, PT generics.PObject[T]](obj PT) (bool, error) { + if obj == nil || obj.GetLabels() == nil { + // REVIEW/TODO: need avoid using dynamic error string, this is bad for + // error type checking (errors.Is) + return false, fmt.Errorf("obj %s or obj's labels is nil, pls check", obj.GetName()) + } + if _, ok := obj.GetLabels()[constant.RoleLabelKey]; !ok { + // REVIEW/TODO: need avoid using dynamic error string, this is bad for + // error type checking (errors.Is) + return false, fmt.Errorf("obj %s or obj labels key is nil, pls check", obj.GetName()) + } + return obj.GetLabels()[constant.RoleLabelKey] == string(Primary), nil +} + +// getReplicationSetPrimaryObj gets the primary obj(statefulSet or pod) of the replication workload. +func getReplicationSetPrimaryObj[T generics.Object, PT generics.PObject[T], L generics.ObjList[T], PL generics.PObjList[T, L]]( + ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, _ func(T, L), compSpecName string) (PT, error) { + var ( + objList L + ) + matchLabels := client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.Name, + constant.KBAppComponentLabelKey: compSpecName, + constant.AppManagedByLabelKey: constant.AppName, + constant.RoleLabelKey: string(Primary), + } + if err := cli.List(ctx, PL(&objList), client.InNamespace(cluster.Namespace), matchLabels); err != nil { + return nil, err + } + objListItems := reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T) + if len(objListItems) != 1 { + // TODO:(xingran) Temporary modification to fix the issue where the cluster state cannot reach the final state + // due to the update order of the role label. Subsequent PR will immediately reconstruct this part. + return nil, nil + // return nil, fmt.Errorf("the number of current replicationSet primary obj is not 1, pls check") + } + return &objListItems[0], nil +} + +// updateObjRoleLabel updates the value of the role label of the object. +func updateObjRoleLabel[T generics.Object, PT generics.PObject[T]]( + ctx context.Context, cli client.Client, obj T, role string) error { + pObj := PT(&obj) + patch := client.MergeFrom(PT(pObj.DeepCopy())) + pObj.GetLabels()[constant.RoleLabelKey] = role + if err := cli.Patch(ctx, pObj, patch); err != nil { + return err + } + return nil +} + +// filterReplicationWorkload filters workload which workloadType is not Replication. +func filterReplicationWorkload(ctx context.Context, + cli client.Client, + cluster *appsv1alpha1.Cluster, + compSpecName string) (*appsv1alpha1.ClusterComponentDefinition, error) { + if compSpecName == "" { + return nil, fmt.Errorf("cluster's compSpecName is nil, pls check") + } + compDefName := cluster.Spec.GetComponentDefRefName(compSpecName) + compDef, err := util.GetComponentDefByCluster(ctx, cli, *cluster, compDefName) + if err != nil { + return compDef, err + } + if compDef == nil || compDef.WorkloadType != appsv1alpha1.Replication { + return nil, nil + } + return compDef, nil +} + +// HandleReplicationSetRoleChangeEvent handles the role change event of the replication workload when switchPolicy is Noop. +func HandleReplicationSetRoleChangeEvent(cli client.Client, + reqCtx intctrlutil.RequestCtx, + cluster *appsv1alpha1.Cluster, + compName string, + pod *corev1.Pod, + newRole string) error { + // if newRole is not Primary or Secondary, ignore it. + if !slices.Contains([]string{string(Primary), string(Secondary)}, newRole) { + reqCtx.Log.Info("replicationSet new role is invalid, please check", "new role", newRole) + return nil + } + // if pod current role label equals to newRole, return + if pod.Labels[constant.RoleLabelKey] == newRole { + reqCtx.Log.Info("pod current role label equals to new role, ignore it", "new role", newRole) + return nil + } + // if switchPolicy is not Noop, return + clusterCompSpec := util.GetClusterComponentSpecByName(*cluster, compName) + if clusterCompSpec == nil || clusterCompSpec.SwitchPolicy == nil || clusterCompSpec.SwitchPolicy.Type != appsv1alpha1.Noop { + reqCtx.Log.Info("cluster switchPolicy is not Noop, does not support handling role change event", "cluster", cluster.Name) + return nil + } + + oldPrimaryPod, err := getReplicationSetPrimaryObj(reqCtx.Ctx, cli, cluster, generics.PodSignature, compName) + if err != nil { + reqCtx.Log.Info("handleReplicationSetRoleChangeEvent gets old primary pod failed", "error", err) + return err + } + if oldPrimaryPod == nil { + return nil + } + // pod is old primary and newRole is secondary, it means that the old primary needs to be changed to secondary, + // we do not deal with this situation here, the demote labeling process of old primary to secondary is handled + // in another reconciliation triggered by role change event from secondary -> new primary, + // this is to avoid simultaneous occurrence of two primary or no primary at the same time + if oldPrimaryPod.Name == pod.Name { + reqCtx.Log.Info("pod is old primary and new role is secondary, do not deal with this situation", + "podName", pod.Name, "newRole", newRole) + return nil + } + + // update old primary pod to secondary + if err := updateObjRoleLabel(reqCtx.Ctx, cli, *oldPrimaryPod, string(Secondary)); err != nil { + return err + } + + // update secondary pod to primary + if err := updateObjRoleLabel(reqCtx.Ctx, cli, *pod, string(Primary)); err != nil { + return err + } + return nil +} diff --git a/controllers/apps/components/replication/replication_utils_test.go b/controllers/apps/components/replication/replication_utils_test.go new file mode 100644 index 000000000..91465400f --- /dev/null +++ b/controllers/apps/components/replication/replication_utils_test.go @@ -0,0 +1,256 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package replication + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/log" + + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" +) + +var _ = Describe("ReplicationSet Util", func() { + + var ( + clusterName = "test-cluster-repl" + clusterDefName = "test-cluster-def-repl" + clusterVersionName = "test-cluster-version-repl" + ) + + var ( + clusterDefObj *appsv1alpha1.ClusterDefinition + clusterVersionObj *appsv1alpha1.ClusterVersion + clusterObj *appsv1alpha1.Cluster + ) + + cleanAll := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + // delete cluster(and all dependent sub-resources), clusterversion and clusterdef + testapps.ClearClusterResources(&testCtx) + + // clear rest resources + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + // namespaced resources + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.StatefulSetSignature, true, inNS, ml) + testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) + } + + BeforeEach(cleanAll) + + AfterEach(cleanAll) + + testHandleReplicationSet := func() { + + By("Creating a cluster with replication workloadType.") + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). + SetReplicas(testapps.DefaultReplicationReplicas). + SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). + Create(&testCtx).GetObject() + + By("Creating a statefulSet of replication workloadType.") + container := corev1.Container{ + Name: "mock-redis-container", + Image: testapps.DefaultRedisImageName, + ImagePullPolicy: corev1.PullIfNotPresent, + } + sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, + clusterObj.Name+"-"+testapps.DefaultRedisCompName, clusterObj.Name, testapps.DefaultRedisCompName). + AddFinalizers([]string{constant.DBClusterFinalizerName}). + AddContainer(container). + AddAppInstanceLabel(clusterObj.Name). + AddAppComponentLabel(testapps.DefaultRedisCompName). + AddAppManangedByLabel(). + SetReplicas(2). + Create(&testCtx).GetObject() + + By("Creating Pods of replication workloadType.") + for i := int32(0); i < *sts.Spec.Replicas; i++ { + _ = testapps.NewPodFactory(testCtx.DefaultNamespace, fmt.Sprintf("%s-%d", sts.Name, i)). + AddContainer(container). + AddLabelsInMap(sts.Labels). + AddRoleLabel(DefaultRole(i)). + Create(&testCtx).GetObject() + } + } + + testNeedUpdateReplicationSetStatus := func() { + By("Creating a cluster with replication workloadType.") + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName).Create(&testCtx).GetObject() + + By("init replicationSet cluster status") + patch := client.MergeFrom(clusterObj.DeepCopy()) + clusterObj.Status.Phase = appsv1alpha1.RunningClusterPhase + clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ + testapps.DefaultRedisCompName: { + Phase: appsv1alpha1.RunningClusterCompPhase, + ReplicationSetStatus: &appsv1alpha1.ReplicationSetStatus{ + Primary: appsv1alpha1.ReplicationMemberStatus{ + Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-0", + }, + Secondaries: []appsv1alpha1.ReplicationMemberStatus{ + { + Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-1", + }, + { + Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-2", + }, + }, + }, + }, + } + Expect(k8sClient.Status().Patch(context.Background(), clusterObj, patch)).Should(Succeed()) + + By("testing sync cluster status with add pod") + + var podList []*corev1.Pod + sts := testk8s.NewFakeStatefulSet(clusterObj.Name+testapps.DefaultRedisCompName, 4) + + for i := int32(0); i < *sts.Spec.Replicas; i++ { + pod := testapps.NewPodFactory(testCtx.DefaultNamespace, fmt.Sprintf("%s-%d", sts.Name, i)). + AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). + AddRoleLabel(DefaultRole(i)). + Create(&testCtx).GetObject() + podList = append(podList, pod) + } + err := syncReplicationSetStatus(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus, podList) + Expect(err).Should(Succeed()) + Expect(len(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus.Secondaries)).Should(Equal(3)) + + By("testing sync cluster status with remove pod") + var podRemoveList []*corev1.Pod + *sts.Spec.Replicas -= 1 + podRemoveList = append(podRemoveList, podList[len(podList)-1]) + Expect(removeTargetPodsInfoInStatus(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus, + podRemoveList, clusterObj.Spec.ComponentSpecs[0].Replicas)).Should(Succeed()) + Expect(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus.Secondaries).Should(HaveLen(2)) + } + + testHandleReplicationSetRoleChangeEvent := func() { + By("Creating a cluster with replication workloadType.") + clusterSwitchPolicy := &appsv1alpha1.ClusterSwitchPolicy{ + Type: appsv1alpha1.Noop, + } + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). + SetReplicas(testapps.DefaultReplicationReplicas). + SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). + SetSwitchPolicy(clusterSwitchPolicy). + Create(&testCtx).GetObject() + + By("Creating a statefulSet of replication workloadType.") + container := corev1.Container{ + Name: "mock-redis-container", + Image: testapps.DefaultRedisImageName, + ImagePullPolicy: corev1.PullIfNotPresent, + } + sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, + clusterObj.Name+"-"+testapps.DefaultRedisCompName, clusterObj.Name, testapps.DefaultRedisCompName). + AddContainer(container). + AddAppInstanceLabel(clusterObj.Name). + AddAppComponentLabel(testapps.DefaultRedisCompName). + AddAppManangedByLabel(). + SetReplicas(2). + Create(&testCtx).GetObject() + + By("Creating Pods of replication workloadType.") + var ( + primaryPod *corev1.Pod + secondaryPods []*corev1.Pod + ) + for i := int32(0); i < *sts.Spec.Replicas; i++ { + pod := testapps.NewPodFactory(testCtx.DefaultNamespace, fmt.Sprintf("%s-%d", sts.Name, i)). + AddContainer(container). + AddLabelsInMap(sts.Labels). + AddRoleLabel(DefaultRole(i)). + Create(&testCtx).GetObject() + if pod.Labels[constant.RoleLabelKey] == string(Primary) { + primaryPod = pod + } else { + secondaryPods = append(secondaryPods, pod) + } + } + Expect(primaryPod).ShouldNot(BeNil()) + Expect(secondaryPods).ShouldNot(BeEmpty()) + + By("Test update replicationSet pod role label with event driver, secondary change to primary.") + reqCtx := intctrlutil.RequestCtx{ + Ctx: testCtx.Ctx, + Log: log.FromContext(ctx).WithValues("event", testCtx.DefaultNamespace), + } + Expect(HandleReplicationSetRoleChangeEvent(k8sClient, reqCtx, clusterObj, testapps.DefaultRedisCompName, + secondaryPods[0], string(Primary))).ShouldNot(HaveOccurred()) + + By("Test when secondary change to primary, the old primary label has been updated at the same time, so return nil directly.") + Expect(HandleReplicationSetRoleChangeEvent(k8sClient, reqCtx, clusterObj, testapps.DefaultRedisCompName, + primaryPod, string(Secondary))).ShouldNot(HaveOccurred()) + } + + // Scenarios + + Context("test replicationSet util", func() { + BeforeEach(func() { + By("Create a clusterDefinition obj with replication workloadType.") + clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompDefName). + Create(&testCtx).GetObject() + + By("Create a clusterVersion obj with replication workloadType.") + clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). + AddComponentVersion(testapps.DefaultRedisCompDefName).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). + Create(&testCtx).GetObject() + + }) + + It("Test handReplicationSet with different conditions", func() { + testHandleReplicationSet() + }) + + It("Test need update replicationSet status when horizontal scaling adds pod or removes pod", func() { + testNeedUpdateReplicationSetStatus() + }) + + It("Test update pod role label by roleChangedEvent when ha switch", func() { + testHandleReplicationSetRoleChangeEvent() + }) + }) +}) diff --git a/controllers/apps/components/replicationset/suite_test.go b/controllers/apps/components/replication/suite_test.go similarity index 75% rename from controllers/apps/components/replicationset/suite_test.go rename to controllers/apps/components/replication/suite_test.go index 8e8d7e579..fda81689e 100644 --- a/controllers/apps/components/replicationset/suite_test.go +++ b/controllers/apps/components/replication/suite_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package replicationset +package replication import ( "context" diff --git a/controllers/apps/components/replicationset/replication_set.go b/controllers/apps/components/replicationset/replication_set.go deleted file mode 100644 index 82bd0036b..000000000 --- a/controllers/apps/components/replicationset/replication_set.go +++ /dev/null @@ -1,223 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 replicationset - -import ( - "context" - "reflect" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// ReplicationSet is a component object used by Cluster, ClusterComponentDefinition and ClusterComponentSpec -type ReplicationSet struct { - Cli client.Client - Cluster *appsv1alpha1.Cluster - Component *appsv1alpha1.ClusterComponentSpec - componentDef *appsv1alpha1.ClusterComponentDefinition -} - -var _ types.Component = &ReplicationSet{} - -// IsRunning is the implementation of the type Component interface method, -// which is used to check whether the replicationSet component is running normally. -func (r *ReplicationSet) IsRunning(ctx context.Context, obj client.Object) (bool, error) { - var componentStsList = &appsv1.StatefulSetList{} - var componentStatusIsRunning = true - sts := util.ConvertToStatefulSet(obj) - if err := util.GetObjectListByComponentName(ctx, r.Cli, *r.Cluster, - componentStsList, sts.Labels[constant.KBAppComponentLabelKey]); err != nil { - return false, err - } - var availableReplicas int32 - for _, stsObj := range componentStsList.Items { - isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, r.Cli, sts) - if err != nil { - return false, err - } - stsIsReady := util.StatefulSetOfComponentIsReady(&stsObj, isRevisionConsistent, nil) - availableReplicas += stsObj.Status.AvailableReplicas - if !stsIsReady { - return false, nil - } - } - if availableReplicas < r.Component.Replicas { - componentStatusIsRunning = false - } - return componentStatusIsRunning, nil -} - -// PodsReady is the implementation of the type Component interface method, -// which is used to check whether all the pods of replicationSet component is ready. -func (r *ReplicationSet) PodsReady(ctx context.Context, obj client.Object) (bool, error) { - var podsReady = true - var componentStsList = &appsv1.StatefulSetList{} - sts := util.ConvertToStatefulSet(obj) - if err := util.GetObjectListByComponentName(ctx, r.Cli, *r.Cluster, componentStsList, - sts.Labels[constant.KBAppComponentLabelKey]); err != nil { - return false, err - } - var availableReplicas int32 - for _, stsObj := range componentStsList.Items { - availableReplicas += stsObj.Status.AvailableReplicas - } - if availableReplicas < r.Component.Replicas { - podsReady = false - } - return podsReady, nil -} - -// PodIsAvailable is the implementation of the type Component interface method, -// Check whether the status of a Pod of the replicationSet is ready, including the role label on the Pod -func (r *ReplicationSet) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { - if pod == nil { - return false - } - return intctrlutil.PodIsReadyWithLabel(*pod) -} - -// HandleProbeTimeoutWhenPodsReady is the implementation of the type Component interface method, -// and replicationSet does not need to do role probe detection, so it returns false directly. -func (r *ReplicationSet) HandleProbeTimeoutWhenPodsReady(ctx context.Context, recorder record.EventRecorder) (bool, error) { - return false, nil -} - -// GetPhaseWhenPodsNotReady is the implementation of the type Component interface method, -// when the pods of replicationSet are not ready, calculate the component phase is Failed or Abnormal. -// if return an empty phase, means the pods of component are ready and skips it. -func (r *ReplicationSet) GetPhaseWhenPodsNotReady(ctx context.Context, componentName string) (appsv1alpha1.ClusterComponentPhase, error) { - componentStsList := &appsv1.StatefulSetList{} - podList, err := util.GetCompRelatedObjectList(ctx, r.Cli, *r.Cluster, componentName, componentStsList) - if err != nil || len(componentStsList.Items) == 0 { - return "", err - } - podCount, componentReplicas := len(podList.Items), r.Component.Replicas - if podCount == 0 { - return util.GetPhaseWithNoAvailableReplicas(componentReplicas), nil - } - var ( - stsMap = make(map[string]appsv1.StatefulSet) - availableReplicas int32 - primaryIsReady bool - existLatestRevisionFailedPod bool - needPatch bool - compStatus = r.Cluster.Status.Components[componentName] - ) - for _, v := range componentStsList.Items { - stsMap[v.Name] = v - availableReplicas += v.Status.AvailableReplicas - } - for _, v := range podList.Items { - // if the pod is terminating, ignore the warning event. - if v.DeletionTimestamp != nil { - return "", nil - } - labelValue := v.Labels[constant.RoleLabelKey] - if labelValue == string(Primary) && intctrlutil.PodIsReady(&v) { - primaryIsReady = true - continue - } - if labelValue == "" { - compStatus.SetObjectMessage(v.Kind, v.Name, "empty label for pod, please check.") - needPatch = true - } - controllerRef := metav1.GetControllerOf(&v) - stsObj := stsMap[controllerRef.Name] - if !intctrlutil.PodIsReady(&v) && intctrlutil.PodIsControlledByLatestRevision(&v, &stsObj) { - existLatestRevisionFailedPod = true - } - } - - // REVIEW: this isn't a get function, where r.Cluster.Status.Components is being updated. - // patch abnormal reason to cluster.status.ComponentDefs. - if needPatch { - patch := client.MergeFrom(r.Cluster.DeepCopy()) - r.Cluster.Status.SetComponentStatus(componentName, compStatus) - if err = r.Cli.Status().Patch(ctx, r.Cluster, patch); err != nil { - return "", err - } - } - return util.GetCompPhaseByConditions(existLatestRevisionFailedPod, primaryIsReady, - componentReplicas, int32(podCount), availableReplicas), nil -} - -// HandleUpdate is the implementation of the type Component interface method, handles replicationSet workload Pod updates. -func (r *ReplicationSet) HandleUpdate(ctx context.Context, obj client.Object) error { - var componentStsList = &appsv1.StatefulSetList{} - var podList []*corev1.Pod - sts := util.ConvertToStatefulSet(obj) - if err := util.GetObjectListByComponentName(ctx, r.Cli, *r.Cluster, componentStsList, - sts.Labels[constant.KBAppComponentLabelKey]); err != nil { - return err - } - for _, sts := range componentStsList.Items { - if sts.Generation != sts.Status.ObservedGeneration { - continue - } - pod, err := getAndCheckReplicationPodByStatefulSet(ctx, r.Cli, &sts) - if err != nil { - return err - } - // if there is no role label on the Pod, it needs to be updated with statefulSet's role label. - if v, ok := pod.Labels[constant.RoleLabelKey]; !ok || v == "" { - if err := updateObjRoleLabel(ctx, r.Cli, *pod, sts.Labels[constant.RoleLabelKey]); err != nil { - return err - } - } else { - podList = append(podList, pod) - } - if err := util.DeleteStsPods(ctx, r.Cli, &sts); err != nil { - return err - } - } - // sync cluster.status.components.replicationSet.status - clusterDeepCopy := r.Cluster.DeepCopy() - if err := syncReplicationSetClusterStatus(r.Cli, ctx, r.Cluster, podList); err != nil { - return err - } - if reflect.DeepEqual(clusterDeepCopy.Status.Components, r.Cluster.Status.Components) { - return nil - } - return r.Cli.Status().Patch(ctx, r.Cluster, client.MergeFrom(clusterDeepCopy)) -} - -// NewReplicationSet creates a new ReplicationSet object. -func NewReplicationSet( - cli client.Client, - cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec, - componentDef appsv1alpha1.ClusterComponentDefinition) (*ReplicationSet, error) { - if err := util.ComponentRuntimeReqArgsCheck(cli, cluster, component); err != nil { - return nil, err - } - return &ReplicationSet{ - Cli: cli, - Cluster: cluster, - Component: component, - componentDef: &componentDef, - }, nil -} diff --git a/controllers/apps/components/replicationset/replication_set_test.go b/controllers/apps/components/replicationset/replication_set_test.go deleted file mode 100644 index a758cfd16..000000000 --- a/controllers/apps/components/replicationset/replication_set_test.go +++ /dev/null @@ -1,226 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 replicationset - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" - testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" -) - -var _ = Describe("Replication Component", func() { - var ( - clusterName = "test-cluster-repl" - clusterDefName = "test-cluster-def-repl" - clusterVersionName = "test-cluster-version-repl" - controllerRivision = "mock-revision" - ) - - var ( - clusterDefObj *appsv1alpha1.ClusterDefinition - clusterVersionObj *appsv1alpha1.ClusterVersion - clusterObj *appsv1alpha1.Cluster - ) - - cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - // delete cluster(and all dependent sub-resources), clusterversion and clusterdef - testapps.ClearClusterResources(&testCtx) - - // clear rest resources - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) - } - - BeforeEach(cleanAll) - - AfterEach(cleanAll) - - Context("Replication Component test", func() { - It("Replication Component test", func() { - - By("Create a clusterDefinition obj with replication workloadType.") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). - Create(&testCtx).GetObject() - - By("Create a clusterVersion obj with replication workloadType.") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(testapps.DefaultRedisCompType).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). - Create(&testCtx).GetObject() - - By("Creating a cluster with replication workloadType.") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - SetReplicas(testapps.DefaultReplicationReplicas). - Create(&testCtx).GetObject() - - // mock cluster is Running - Expect(testapps.ChangeObjStatus(&testCtx, clusterObj, func() { - clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ - testapps.DefaultRedisCompName: { - Phase: appsv1alpha1.RunningClusterCompPhase, - }, - } - })).Should(Succeed()) - - By("Creating two statefulSets of replication workloadType.") - status := appsv1.StatefulSetStatus{ - AvailableReplicas: 1, - ObservedGeneration: 1, - Replicas: 1, - ReadyReplicas: 1, - UpdatedReplicas: 1, - CurrentRevision: controllerRivision, - UpdateRevision: controllerRivision, - } - - var ( - primarySts *appsv1.StatefulSet - secondarySts *appsv1.StatefulSet - ) - for k, v := range map[string]string{ - string(Primary): clusterObj.Name + "-" + testapps.DefaultRedisCompName, - string(Secondary): clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-1", - } { - sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, v, clusterObj.Name, testapps.DefaultRedisCompName). - AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). - AddAppInstanceLabel(clusterObj.Name). - AddAppComponentLabel(testapps.DefaultRedisCompName). - AddAppManangedByLabel(). - AddRoleLabel(k). - SetReplicas(1). - Create(&testCtx).GetObject() - isStsPrimary, err := checkObjRoleLabelIsPrimary(sts) - if k == string(Primary) { - Expect(err).To(Succeed()) - Expect(isStsPrimary).Should(BeTrue()) - primarySts = sts - } else { - Expect(err).To(Succeed()) - Expect(isStsPrimary).ShouldNot(BeTrue()) - secondarySts = sts - } - Expect(sts.Spec.VolumeClaimTemplates).Should(BeEmpty()) - } - - compDefName := clusterObj.Spec.GetComponentDefRefName(testapps.DefaultRedisCompName) - componentDef := clusterDefObj.GetComponentDefByName(compDefName) - component := clusterObj.Spec.GetComponentByName(testapps.DefaultRedisCompName) - replicationComponent, err := NewReplicationSet(k8sClient, clusterObj, component, *componentDef) - Expect(err).Should(Succeed()) - var podList []*corev1.Pod - for _, availableReplica := range []int32{0, 1} { - status.AvailableReplicas = availableReplica - primarySts.Status = status - testk8s.PatchStatefulSetStatus(&testCtx, primarySts.Name, status) - secondarySts.Status = status - testk8s.PatchStatefulSetStatus(&testCtx, secondarySts.Name, status) - // Create pod of the statefulset - if availableReplica == 1 { - sts1Pod := testapps.MockReplicationComponentPods(testCtx, primarySts, clusterObj.Name, testapps.DefaultRedisCompName, string(Primary)) - podList = append(podList, sts1Pod...) - sts2Pod := testapps.MockReplicationComponentPods(testCtx, secondarySts, clusterObj.Name, testapps.DefaultRedisCompName, string(Secondary)) - podList = append(podList, sts2Pod...) - } - - podsReady, _ := replicationComponent.PodsReady(ctx, primarySts) - isRunning, _ := replicationComponent.IsRunning(ctx, primarySts) - if availableReplica == 1 { - By("Testing pods are ready") - Expect(podsReady == true).Should(BeTrue()) - - By("Testing component is running") - Expect(isRunning == true).Should(BeTrue()) - } else { - By("Testing pods are not ready") - Expect(podsReady == false).Should(BeTrue()) - - By("Testing component is not running") - Expect(isRunning == false).Should(BeTrue()) - } - } - - By("Testing handle probe timed out") - requeue, _ := replicationComponent.HandleProbeTimeoutWhenPodsReady(ctx, nil) - Expect(requeue == false).Should(BeTrue()) - - By("Testing pod is available") - primaryPod := podList[0] - Expect(replicationComponent.PodIsAvailable(primaryPod, 10)).Should(BeTrue()) - - By("Testing component phase when pods not ready") - // mock secondary pod is not ready. - Expect(testapps.ChangeObjStatus(&testCtx, secondarySts, func() { - secondarySts.Status.AvailableReplicas = 0 - })).Should(Succeed()) - testk8s.UpdatePodStatusNotReady(ctx, testCtx, podList[1].Name) - phase, _ := replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) - Expect(phase).Should(Equal(appsv1alpha1.AbnormalClusterCompPhase)) - - // mock primary pod is not ready - testk8s.UpdatePodStatusNotReady(ctx, testCtx, primaryPod.Name) - phase, _ = replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) - Expect(phase).Should(Equal(appsv1alpha1.FailedClusterCompPhase)) - - // mock pod label is empty - Expect(testapps.ChangeObj(&testCtx, primaryPod, func() { - primaryPod.Labels[constant.RoleLabelKey] = "" - })).Should(Succeed()) - _, _ = replicationComponent.GetPhaseWhenPodsNotReady(ctx, testapps.DefaultRedisCompName) - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(clusterObj), - func(g Gomega, cluster *appsv1alpha1.Cluster) { - compStatus := cluster.Status.Components[testapps.DefaultRedisCompName] - g.Expect(compStatus.GetObjectMessage(primaryPod.Kind, primaryPod.Name)). - Should(ContainSubstring("empty label for pod, please check")) - })).Should(Succeed()) - - By("Checking if the pod is not updated when statefulset is not updated") - Expect(replicationComponent.HandleUpdate(ctx, primarySts)).To(Succeed()) - primaryStsPodList, err := util.GetPodListByStatefulSet(ctx, k8sClient, primarySts) - Expect(err).To(Succeed()) - Expect(len(primaryStsPodList)).To(Equal(1)) - Expect(util.IsStsAndPodsRevisionConsistent(ctx, k8sClient, primarySts)).Should(BeTrue()) - - By("Checking if the pod is deleted when statefulset is updated") - status.UpdateRevision = "new-mock-revision" - testk8s.PatchStatefulSetStatus(&testCtx, primarySts.Name, status) - Expect(replicationComponent.HandleUpdate(ctx, primarySts)).To(Succeed()) - primaryStsPodList, err = util.GetPodListByStatefulSet(ctx, k8sClient, primarySts) - Expect(err).To(Succeed()) - Expect(len(primaryStsPodList)).To(Equal(0)) - }) - }) -}) diff --git a/controllers/apps/components/replicationset/replication_set_utils.go b/controllers/apps/components/replicationset/replication_set_utils.go deleted file mode 100644 index 6a61d09a6..000000000 --- a/controllers/apps/components/replicationset/replication_set_utils.go +++ /dev/null @@ -1,450 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 replicationset - -import ( - "context" - "fmt" - "reflect" - "sort" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" - "github.com/apecloud/kubeblocks/internal/generics" -) - -type ReplicationRole string - -const ( - Primary ReplicationRole = "primary" - Secondary ReplicationRole = "secondary" - DBClusterFinalizerName = "cluster.kubeblocks.io/finalizer" -) - -// HandleReplicationSet handles the replication workload life cycle process, including horizontal scaling, etc. -func HandleReplicationSet(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster, - stsList []*appsv1.StatefulSet) error { - if cluster == nil { - return util.ErrReqClusterObj - } - // handle replication workload horizontal scaling - if err := handleReplicationSetHorizontalScale(ctx, cli, cluster, stsList); err != nil { - return err - } - return nil -} - -// handleReplicationSetHorizontalScale handles changes of replication workload replicas and synchronizes cluster status. -func handleReplicationSetHorizontalScale(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster, - stsList []*appsv1.StatefulSet) error { - - // handle StatefulSets including delete sts when pod number larger than cluster.component[i].replicas - // delete the StatefulSets with the largest sequence number which is not the primary role - clusterCompReplicasMap := make(map[string]int32, len(cluster.Spec.ComponentSpecs)) - for _, clusterComp := range cluster.Spec.ComponentSpecs { - clusterCompReplicasMap[clusterComp.Name] = clusterComp.Replicas - } - - // compOwnsStsMap is used to divide stsList into sts list under each replicationSet component according to componentLabelKey - compOwnsStsMap := make(map[string][]*appsv1.StatefulSet) - for _, stsObj := range stsList { - compName := stsObj.Labels[constant.KBAppComponentLabelKey] - compDef, err := filterReplicationWorkload(ctx, cli, cluster, compName) - if err != nil { - return err - } - if compDef == nil { - continue - } - compOwnsStsMap[compName] = append(compOwnsStsMap[compName], stsObj) - } - - // stsToDeleteMap is used to record the count of statefulsets to be deleted when horizontal scale-in - stsToDeleteMap := make(map[string]int32) - for compName := range compOwnsStsMap { - if int32(len(compOwnsStsMap[compName])) > clusterCompReplicasMap[compName] { - stsToDeleteMap[compName] = int32(len(compOwnsStsMap[compName])) - clusterCompReplicasMap[compName] - } - } - if len(stsToDeleteMap) > 0 { - if err := doHorizontalScaleDown(ctx, cli, cluster, compOwnsStsMap, clusterCompReplicasMap, stsToDeleteMap); err != nil { - return err - } - } - return nil -} - -// handleComponentIsStopped checks the component status is stopped and updates it. -func handleComponentIsStopped(cluster *appsv1alpha1.Cluster) { - for _, clusterComp := range cluster.Spec.ComponentSpecs { - if clusterComp.Replicas == int32(0) { - replicationStatus := cluster.Status.Components[clusterComp.Name] - replicationStatus.Phase = appsv1alpha1.StoppedClusterCompPhase - cluster.Status.SetComponentStatus(clusterComp.Name, replicationStatus) - } - } -} - -func doHorizontalScaleDown(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster, - compOwnsStsMap map[string][]*appsv1.StatefulSet, - clusterCompReplicasMap map[string]int32, - stsToDeleteMap map[string]int32) error { - // remove cluster status and delete sts when horizontal scale-in - for compName, stsToDelCount := range stsToDeleteMap { - // list all statefulSets by cluster and componentKey label - var componentStsList = &appsv1.StatefulSetList{} - err := util.GetObjectListByComponentName(ctx, cli, *cluster, componentStsList, compName) - if err != nil { - return err - } - if int32(len(compOwnsStsMap[compName])) != int32(len(componentStsList.Items)) { - return fmt.Errorf("statefulset total number has changed") - } - dos := make([]*appsv1.StatefulSet, 0) - partition := int32(len(componentStsList.Items)) - stsToDelCount - componentReplicas := clusterCompReplicasMap[compName] - var primarySts *appsv1.StatefulSet - for _, sts := range componentStsList.Items { - // if current primary statefulSet ordinal is larger than target number replica, return err - stsIsPrimary, err := checkObjRoleLabelIsPrimary(&sts) - primarySts = &sts - if err != nil { - return err - } - // check if the current primary statefulSet ordinal is larger than target replicas number of component when the target number is not 0. - if int32(util.GetOrdinalSts(&sts)) >= partition && stsIsPrimary && componentReplicas != 0 { - return fmt.Errorf("current primary statefulset ordinal is larger than target number replicas, can not be reduce, please switchover first") - } - dos = append(dos, sts.DeepCopy()) - } - - // sort the statefulSets by their ordinals desc - sort.Sort(util.DescendingOrdinalSts(dos)) - - if err = RemoveReplicationSetClusterStatus(cli, ctx, cluster, dos[:stsToDelCount], componentReplicas); err != nil { - return err - } - for i := int32(0); i < stsToDelCount; i++ { - err = cli.Delete(ctx, dos[i]) - if err == nil { - patch := client.MergeFrom(dos[i].DeepCopy()) - controllerutil.RemoveFinalizer(dos[i], DBClusterFinalizerName) - if err = cli.Patch(ctx, dos[i], patch); err != nil { - return err - } - continue - } - if apierrors.IsNotFound(err) { - continue - } - return err - } - // reconcile the primary statefulSet after deleting other sts to make component phase is correct. - if componentReplicas != 0 { - if err = util.MarkPrimaryStsToReconcile(ctx, cli, primarySts); err != nil { - return client.IgnoreNotFound(err) - } - } - } - - // if component replicas is 0, handle replication component status after scaling down the replicas. - handleComponentIsStopped(cluster) - return nil -} - -// syncReplicationSetClusterStatus syncs replicationSet pod status to cluster.status.component[componentName].ReplicationStatus. -func syncReplicationSetClusterStatus( - cli client.Client, - ctx context.Context, - cluster *appsv1alpha1.Cluster, - podList []*corev1.Pod) error { - if len(podList) == 0 { - return nil - } - - // update cluster status - componentName, componentDef, err := util.GetComponentInfoByPod(ctx, cli, *cluster, podList[0]) - if err != nil { - return err - } - if componentDef == nil { - return nil - } - oldReplicationSetStatus := cluster.Status.Components[componentName].ReplicationSetStatus - if oldReplicationSetStatus == nil { - if err = util.InitClusterComponentStatusIfNeed(cluster, componentName, *componentDef); err != nil { - return err - } - oldReplicationSetStatus = cluster.Status.Components[componentName].ReplicationSetStatus - } - if err := syncReplicationSetStatus(oldReplicationSetStatus, podList); err != nil { - return err - } - return nil -} - -// syncReplicationSetStatus syncs the target pod info in cluster.status.components. -func syncReplicationSetStatus(replicationStatus *appsv1alpha1.ReplicationSetStatus, podList []*corev1.Pod) error { - for _, pod := range podList { - role := pod.Labels[constant.RoleLabelKey] - if role == "" { - return fmt.Errorf("pod %s has no role label", pod.Name) - } - if role == string(Primary) { - if replicationStatus.Primary.Pod == pod.Name { - continue - } - replicationStatus.Primary.Pod = pod.Name - // if current primary pod in secondary list, it means the primary pod has been switched, remove it. - for index, secondary := range replicationStatus.Secondaries { - if secondary.Pod == pod.Name { - replicationStatus.Secondaries = append(replicationStatus.Secondaries[:index], replicationStatus.Secondaries[index+1:]...) - break - } - } - } else { - var exist = false - for _, secondary := range replicationStatus.Secondaries { - if secondary.Pod == pod.Name { - exist = true - break - } - } - if !exist { - replicationStatus.Secondaries = append(replicationStatus.Secondaries, appsv1alpha1.ReplicationMemberStatus{ - Pod: pod.Name, - }) - } - } - } - return nil -} - -// RemoveReplicationSetClusterStatus removes replicationSet pod status from cluster.status.component[componentName].ReplicationStatus. -func RemoveReplicationSetClusterStatus(cli client.Client, - ctx context.Context, - cluster *appsv1alpha1.Cluster, - stsList []*appsv1.StatefulSet, - componentReplicas int32) error { - if len(stsList) == 0 { - return nil - } - var allPodList []corev1.Pod - for _, stsObj := range stsList { - podList, err := util.GetPodListByStatefulSet(ctx, cli, stsObj) - if err != nil { - return err - } - allPodList = append(allPodList, podList...) - } - componentName := stsList[0].Labels[constant.KBAppComponentLabelKey] - replicationSetStatus := cluster.Status.Components[componentName].ReplicationSetStatus - return removeTargetPodsInfoInStatus(replicationSetStatus, allPodList, componentReplicas) -} - -// removeTargetPodsInfoInStatus remove the target pod info from cluster.status.components. -func removeTargetPodsInfoInStatus(replicationStatus *appsv1alpha1.ReplicationSetStatus, - targetPodList []corev1.Pod, - componentReplicas int32) error { - if replicationStatus == nil { - return nil - } - targetPodNameMap := make(map[string]struct{}) - for _, pod := range targetPodList { - targetPodNameMap[pod.Name] = struct{}{} - } - if _, ok := targetPodNameMap[replicationStatus.Primary.Pod]; ok { - if componentReplicas != 0 { - return fmt.Errorf("primary pod cannot be removed") - } - replicationStatus.Primary = appsv1alpha1.ReplicationMemberStatus{ - Pod: util.ComponentStatusDefaultPodName, - } - } - newSecondaries := make([]appsv1alpha1.ReplicationMemberStatus, 0) - for _, secondary := range replicationStatus.Secondaries { - if _, ok := targetPodNameMap[secondary.Pod]; ok { - continue - } - // add pod that do not need to be removed to newSecondaries slice. - newSecondaries = append(newSecondaries, secondary) - } - replicationStatus.Secondaries = newSecondaries - return nil -} - -// checkObjRoleLabelIsPrimary checks whether it is the primary obj(statefulSet or pod) through the label tag on obj. -func checkObjRoleLabelIsPrimary[T generics.Object, PT generics.PObject[T]](obj PT) (bool, error) { - if obj == nil || obj.GetLabels() == nil { - // REVIEW/TODO: need avoid using dynamic error string, this is bad for - // error type checking (errors.Is) - return false, fmt.Errorf("obj %s or obj's labels is nil, pls check", obj.GetName()) - } - if _, ok := obj.GetLabels()[constant.RoleLabelKey]; !ok { - // REVIEW/TODO: need avoid using dynamic error string, this is bad for - // error type checking (errors.Is) - return false, fmt.Errorf("obj %s or obj labels key is nil, pls check", obj.GetName()) - } - return obj.GetLabels()[constant.RoleLabelKey] == string(Primary), nil -} - -// getReplicationSetPrimaryObj gets the primary obj(statefulSet or pod) of the replication workload. -func getReplicationSetPrimaryObj[T generics.Object, PT generics.PObject[T], L generics.ObjList[T], PL generics.PObjList[T, L]]( - ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, _ func(T, L), compSpecName string) (PT, error) { - var ( - objList L - ) - matchLabels := client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.Name, - constant.KBAppComponentLabelKey: compSpecName, - constant.AppManagedByLabelKey: constant.AppName, - constant.RoleLabelKey: string(Primary), - } - if err := cli.List(ctx, PL(&objList), client.InNamespace(cluster.Namespace), matchLabels); err != nil { - return nil, err - } - objListItems := reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T) - if len(objListItems) != 1 { - return nil, fmt.Errorf("the number of current replicationSet primary obj is not 1, pls check") - } - return &objListItems[0], nil -} - -// updateObjRoleLabel updates the value of the role label of the object. -func updateObjRoleLabel[T generics.Object, PT generics.PObject[T]]( - ctx context.Context, cli client.Client, obj T, role string) error { - pObj := PT(&obj) - patch := client.MergeFrom(PT(pObj.DeepCopy())) - pObj.GetLabels()[constant.RoleLabelKey] = role - if err := cli.Patch(ctx, pObj, patch); err != nil { - return err - } - return nil -} - -// GeneratePVCFromVolumeClaimTemplates generates the required pvc object according to the name of statefulSet and volumeClaimTemplates. -func GeneratePVCFromVolumeClaimTemplates(sts *appsv1.StatefulSet, vctList []corev1.PersistentVolumeClaimTemplate) map[string]*corev1.PersistentVolumeClaim { - claims := make(map[string]*corev1.PersistentVolumeClaim, len(vctList)) - for index := range vctList { - claim := &corev1.PersistentVolumeClaim{ - TypeMeta: metav1.TypeMeta{ - Kind: "PersistentVolumeClaim", - APIVersion: "v1", - }, - Spec: vctList[index].Spec, - } - // The replica of replicationSet statefulSet defaults to 1, so the ordinal here is 0 - claim.Name = GetPersistentVolumeClaimName(sts, &vctList[index], 0) - claim.Namespace = sts.Namespace - claims[vctList[index].Name] = claim - } - return claims -} - -// GetPersistentVolumeClaimName gets the name of PersistentVolumeClaim for a replicationSet pod with an ordinal. -// claimTpl must be a PersistentVolumeClaimTemplate from the VolumeClaimsTemplate in the Cluster API. -func GetPersistentVolumeClaimName(sts *appsv1.StatefulSet, claimTpl *corev1.PersistentVolumeClaimTemplate, ordinal int) string { - return fmt.Sprintf("%s-%s-%d", claimTpl.Name, sts.Name, ordinal) -} - -// filterReplicationWorkload filters workload which workloadType is not Replication. -func filterReplicationWorkload(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster, - compSpecName string) (*appsv1alpha1.ClusterComponentDefinition, error) { - if compSpecName == "" { - return nil, fmt.Errorf("cluster's compSpecName is nil, pls check") - } - compDefName := cluster.Spec.GetComponentDefRefName(compSpecName) - compDef, err := util.GetComponentDefByCluster(ctx, cli, *cluster, compDefName) - if err != nil { - return compDef, err - } - if compDef == nil || compDef.WorkloadType != appsv1alpha1.Replication { - return nil, nil - } - return compDef, nil -} - -// getAndCheckReplicationPodByStatefulSet checks the number of replication statefulSet equal 1 and returns it. -func getAndCheckReplicationPodByStatefulSet(ctx context.Context, cli client.Client, stsObj *appsv1.StatefulSet) (*corev1.Pod, error) { - podList, err := util.GetPodListByStatefulSet(ctx, cli, stsObj) - if err != nil { - return nil, err - } - if len(podList) != 1 { - return nil, fmt.Errorf("pod number in statefulset %s is not 1", stsObj.Name) - } - return &podList[0], nil -} - -// HandleReplicationSetRoleChangeEvent handles the role change event of the replication workload when switchPolicy is Noop. -func HandleReplicationSetRoleChangeEvent(cli client.Client, - reqCtx intctrlutil.RequestCtx, - cluster *appsv1alpha1.Cluster, - compName string, - pod *corev1.Pod, - newRole string) error { - // if newRole is empty or pod current role label equals to newRole, return - if newRole == "" || pod.Labels[constant.RoleLabelKey] == newRole { - reqCtx.Log.Info("new role label is empty or pod current role label equals to new role, ignore it", "new role", newRole) - return nil - } - // if switchPolicy is not Noop, return - clusterCompSpec := util.GetClusterComponentSpecByName(*cluster, compName) - if clusterCompSpec == nil || clusterCompSpec.SwitchPolicy == nil || clusterCompSpec.SwitchPolicy.Type != appsv1alpha1.Noop { - reqCtx.Log.Info("cluster switchPolicy is not Noop, does not support handle role change event", "cluster", cluster.Name) - return nil - } - - oldPrimaryPod, err := getReplicationSetPrimaryObj(reqCtx.Ctx, cli, cluster, generics.PodSignature, compName) - if err != nil { - reqCtx.Log.Info("handleReplicationSetRoleChangeEvent get old primary pod failed", "error", err) - return err - } - // pod is old primary and newRole is secondary, it means that the old primary needs to be changed to secondary, - // we do not deal with this situation because We will only change the old primary to secondary when the new primary changes from secondary to primary, - // this is to avoid simultaneous occurrence of two primary or no primary at the same time - if oldPrimaryPod.Name == pod.Name { - reqCtx.Log.Info("pod is old primary and new role is secondary, do not deal with this situation", "podName", pod.Name, "newRole", newRole) - return nil - } - - // pod is old secondary and newRole is primary - // update old primary to secondary - if err := updateObjRoleLabel(reqCtx.Ctx, cli, *oldPrimaryPod, string(Secondary)); err != nil { - return err - } - - // update secondary pod to primary - return updateObjRoleLabel(reqCtx.Ctx, cli, *pod, newRole) -} diff --git a/controllers/apps/components/replicationset/replication_set_utils_test.go b/controllers/apps/components/replicationset/replication_set_utils_test.go deleted file mode 100644 index 4312e39f8..000000000 --- a/controllers/apps/components/replicationset/replication_set_utils_test.go +++ /dev/null @@ -1,334 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 replicationset - -import ( - "context" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" - "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" - testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" -) - -var _ = Describe("ReplicationSet Util", func() { - - var ( - clusterName = "test-cluster-repl" - clusterDefName = "test-cluster-def-repl" - clusterVersionName = "test-cluster-version-repl" - ) - - var ( - clusterDefObj *appsv1alpha1.ClusterDefinition - clusterVersionObj *appsv1alpha1.ClusterVersion - clusterObj *appsv1alpha1.Cluster - ) - - cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - // delete cluster(and all dependent sub-resources), clusterversion and clusterdef - testapps.ClearClusterResources(&testCtx) - - // clear rest resources - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // namespaced resources - testapps.ClearResources(&testCtx, generics.StatefulSetSignature, inNS, ml) - testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) - } - - BeforeEach(cleanAll) - - AfterEach(cleanAll) - - testHandleReplicationSet := func() { - - By("Creating a cluster with replication workloadType.") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - SetReplicas(testapps.DefaultReplicationReplicas). - SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). - Create(&testCtx).GetObject() - - By("Creating a statefulSet of replication workloadType.") - container := corev1.Container{ - Name: "mock-redis-container", - Image: testapps.DefaultRedisImageName, - ImagePullPolicy: corev1.PullIfNotPresent, - } - stsList := make([]*appsv1.StatefulSet, 0) - secondaryName := clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-1" - for k, v := range map[string]string{ - string(Primary): clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-0", - string(Secondary): secondaryName, - } { - sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, v, clusterObj.Name, testapps.DefaultRedisCompName). - AddFinalizers([]string{DBClusterFinalizerName}). - AddContainer(container). - AddAppInstanceLabel(clusterObj.Name). - AddAppComponentLabel(testapps.DefaultRedisCompName). - AddAppManangedByLabel(). - AddRoleLabel(k). - SetReplicas(1). - Create(&testCtx).GetObject() - isStsPrimary, err := checkObjRoleLabelIsPrimary(sts) - if k == string(Primary) { - Expect(err).To(Succeed()) - Expect(isStsPrimary).Should(BeTrue()) - } else { - Expect(err).To(Succeed()) - Expect(isStsPrimary).ShouldNot(BeTrue()) - } - stsList = append(stsList, sts) - } - - By("Creating Pods of replication workloadType.") - for _, sts := range stsList { - _ = testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). - AddContainer(container). - AddLabelsInMap(sts.Labels). - Create(&testCtx).GetObject() - } - - By("Test ReplicationSet pod number of sts equals 1") - _, err := getAndCheckReplicationPodByStatefulSet(ctx, k8sClient, stsList[0]) - Expect(err).Should(Succeed()) - - By("Test handleReplicationSet success when stsList count equal cluster.replicas.") - err = HandleReplicationSet(ctx, k8sClient, clusterObj, stsList) - Expect(err).Should(Succeed()) - - By("Test handleReplicationSet scale-in return err when remove Finalizer after delete the sts") - clusterObj.Spec.ComponentSpecs[0].Replicas = testapps.DefaultReplicationReplicas - 1 - Expect(HandleReplicationSet(ctx, k8sClient, clusterObj, stsList)).Should(Succeed()) - Eventually(testapps.GetListLen(&testCtx, generics.StatefulSetSignature, - client.InNamespace(testCtx.DefaultNamespace))).Should(Equal(1)) - - By("Test handleReplicationSet scale replicas to 0") - clusterObj.Spec.ComponentSpecs[0].Replicas = 0 - Expect(HandleReplicationSet(ctx, k8sClient, clusterObj, stsList[:1])).Should(Succeed()) - Eventually(testapps.GetListLen(&testCtx, generics.StatefulSetSignature, client.InNamespace(testCtx.DefaultNamespace))).Should(Equal(0)) - Expect(clusterObj.Status.Components[testapps.DefaultRedisCompName].Phase).Should(Equal(appsv1alpha1.StoppedClusterCompPhase)) - } - - testNeedUpdateReplicationSetStatus := func() { - By("Creating a cluster with replication workloadType.") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType).Create(&testCtx).GetObject() - - By("init replicationSet cluster status") - patch := client.MergeFrom(clusterObj.DeepCopy()) - clusterObj.Status.Phase = appsv1alpha1.RunningClusterPhase - clusterObj.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ - testapps.DefaultRedisCompName: { - Phase: appsv1alpha1.RunningClusterCompPhase, - ReplicationSetStatus: &appsv1alpha1.ReplicationSetStatus{ - Primary: appsv1alpha1.ReplicationMemberStatus{ - Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-0-0", - }, - Secondaries: []appsv1alpha1.ReplicationMemberStatus{ - { - Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-1-0", - }, - { - Pod: clusterObj.Name + testapps.DefaultRedisCompName + "-2-0", - }, - }, - }, - }, - } - Expect(k8sClient.Status().Patch(context.Background(), clusterObj, patch)).Should(Succeed()) - - By("testing sync cluster status with add pod") - var podList []*corev1.Pod - sts := testk8s.NewFakeStatefulSet(clusterObj.Name+testapps.DefaultRedisCompName+"-3", 3) - pod := testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). - AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). - AddRoleLabel(string(Secondary)). - Create(&testCtx).GetObject() - podList = append(podList, pod) - err := syncReplicationSetStatus(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus, podList) - Expect(err).Should(Succeed()) - Expect(len(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus.Secondaries)).Should(Equal(3)) - - By("testing sync cluster status with remove pod") - var podRemoveList []corev1.Pod - sts = testk8s.NewFakeStatefulSet(clusterObj.Name+testapps.DefaultRedisCompName+"-2", 3) - pod = testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). - AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). - AddRoleLabel(string(Secondary)). - Create(&testCtx).GetObject() - podRemoveList = append(podRemoveList, *pod) - Expect(removeTargetPodsInfoInStatus(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus, - podRemoveList, clusterObj.Spec.ComponentSpecs[0].Replicas)).Should(Succeed()) - Expect(len(clusterObj.Status.Components[testapps.DefaultRedisCompName].ReplicationSetStatus.Secondaries)).Should(Equal(2)) - } - - testGeneratePVCFromVolumeClaimTemplates := func() { - By("Creating a cluster with replication workloadType.") - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - SetReplicas(testapps.DefaultReplicationReplicas). - SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). - Create(&testCtx).GetObject() - - By("Creating a statefulSet of replication workloadType.") - mockStsName := "mock-stateful-set-0" - mockSts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, mockStsName, clusterObj.Name, testapps.DefaultRedisCompName). - AddContainer(corev1.Container{Name: testapps.DefaultRedisContainerName, Image: testapps.DefaultRedisImageName}). - Create(&testCtx).GetObject() - - mockVCTList := []corev1.PersistentVolumeClaimTemplate{ - { - ObjectMeta: metav1.ObjectMeta{ - Name: "mock-vct", - Namespace: testCtx.DefaultNamespace, - }, - Spec: corev1.PersistentVolumeClaimSpec{ - VolumeName: "data", - }, - }, - } - pvcMap := GeneratePVCFromVolumeClaimTemplates(mockSts, mockVCTList) - for _, pvc := range pvcMap { - Expect(pvc.Name).Should(BeEquivalentTo("mock-vct-mock-stateful-set-0-0")) - } - } - - testHandleReplicationSetRoleChangeEvent := func() { - By("Creating a cluster with replication workloadType.") - clusterSwitchPolicy := &appsv1alpha1.ClusterSwitchPolicy{ - Type: appsv1alpha1.Noop, - } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - SetReplicas(testapps.DefaultReplicationReplicas). - SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). - SetSwitchPolicy(clusterSwitchPolicy). - Create(&testCtx).GetObject() - - By("Creating a statefulSet of replication workloadType.") - container := corev1.Container{ - Name: "mock-redis-container", - Image: testapps.DefaultRedisImageName, - ImagePullPolicy: corev1.PullIfNotPresent, - } - stsList := make([]*appsv1.StatefulSet, 0) - secondaryName := clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-1" - for k, v := range map[string]string{ - string(Primary): clusterObj.Name + "-" + testapps.DefaultRedisCompName + "-0", - string(Secondary): secondaryName, - } { - sts := testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, v, clusterObj.Name, testapps.DefaultRedisCompName). - AddContainer(container). - AddAppInstanceLabel(clusterObj.Name). - AddAppComponentLabel(testapps.DefaultRedisCompName). - AddAppManangedByLabel(). - AddRoleLabel(k). - SetReplicas(1). - Create(&testCtx).GetObject() - isStsPrimary, err := checkObjRoleLabelIsPrimary(sts) - if k == string(Primary) { - Expect(err).To(Succeed()) - Expect(isStsPrimary).Should(BeTrue()) - } else { - Expect(err).To(Succeed()) - Expect(isStsPrimary).ShouldNot(BeTrue()) - } - stsList = append(stsList, sts) - } - - By("Creating Pods of replication workloadType.") - var ( - primaryPod *corev1.Pod - secondaryPod *corev1.Pod - ) - for _, sts := range stsList { - pod := testapps.NewPodFactory(testCtx.DefaultNamespace, sts.Name+"-0"). - AddContainer(container). - AddLabelsInMap(sts.Labels). - Create(&testCtx).GetObject() - if sts.Labels[constant.RoleLabelKey] == string(Primary) { - primaryPod = pod - } else { - secondaryPod = pod - } - } - By("Test update replicationSet pod role label with event driver, secondary change to primary.") - reqCtx := intctrlutil.RequestCtx{ - Ctx: testCtx.Ctx, - Log: log.FromContext(ctx).WithValues("event", testCtx.DefaultNamespace), - } - err := HandleReplicationSetRoleChangeEvent(k8sClient, reqCtx, clusterObj, testapps.DefaultRedisCompName, secondaryPod, string(Primary)) - Expect(err).Should(Succeed()) - By("Test when secondary change to primary, the old primary label has been updated at the same time, so return nil directly.") - err = HandleReplicationSetRoleChangeEvent(k8sClient, reqCtx, clusterObj, testapps.DefaultRedisCompName, primaryPod, string(Secondary)) - Expect(err).Should(BeNil()) - } - - // Scenarios - - Context("test replicationSet util", func() { - BeforeEach(func() { - By("Create a clusterDefinition obj with replication workloadType.") - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). - Create(&testCtx).GetObject() - - By("Create a clusterVersion obj with replication workloadType.") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(testapps.DefaultRedisCompType).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). - Create(&testCtx).GetObject() - - }) - - It("Test handReplicationSet with different conditions", func() { - testHandleReplicationSet() - }) - - It("Test need update replicationSet status when horizontal scaling adds pod or removes pod", func() { - testNeedUpdateReplicationSetStatus() - }) - - It("Test generatePVC from volume claim templates", func() { - testGeneratePVCFromVolumeClaimTemplates() - }) - - It("Test update pod role label by roleChangedEvent when ha switch", func() { - testHandleReplicationSetRoleChangeEvent() - }) - }) -}) diff --git a/controllers/apps/components/stateful/component_stateful.go b/controllers/apps/components/stateful/component_stateful.go new file mode 100644 index 000000000..bbfb25af3 --- /dev/null +++ b/controllers/apps/components/stateful/component_stateful.go @@ -0,0 +1,108 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package stateful + +import ( + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +func NewStatefulComponent(cli client.Client, + recorder record.EventRecorder, + cluster *appsv1alpha1.Cluster, + clusterVersion *appsv1alpha1.ClusterVersion, + synthesizedComponent *component.SynthesizedComponent, + dag *graph.DAG) *statefulComponent { + comp := &statefulComponent{ + StatefulComponentBase: internal.StatefulComponentBase{ + ComponentBase: internal.ComponentBase{ + Client: cli, + Recorder: recorder, + Cluster: cluster, + ClusterVersion: clusterVersion, + Component: synthesizedComponent, + ComponentSet: &Stateful{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: nil, + ComponentDef: nil, + Component: nil, + }, + }, + Dag: dag, + WorkloadVertex: nil, + }, + }, + } + comp.ComponentSet.SetComponent(comp) + return comp +} + +type statefulComponent struct { + internal.StatefulComponentBase +} + +var _ types.Component = &statefulComponent{} + +func (c *statefulComponent) newBuilder(reqCtx intctrlutil.RequestCtx, cli client.Client, + action *ictrltypes.LifecycleAction) internal.ComponentWorkloadBuilder { + builder := &statefulComponentWorkloadBuilder{ + ComponentWorkloadBuilderBase: internal.ComponentWorkloadBuilderBase{ + ReqCtx: reqCtx, + Client: cli, + Comp: c, + DefaultAction: action, + Error: nil, + EnvConfig: nil, + Workload: nil, + }, + } + builder.ConcreteBuilder = builder + return builder +} + +func (c *statefulComponent) GetWorkloadType() appsv1alpha1.WorkloadType { + return appsv1alpha1.Stateful +} + +func (c *statefulComponent) GetBuiltObjects(reqCtx intctrlutil.RequestCtx, cli client.Client) ([]client.Object, error) { + return c.StatefulComponentBase.GetBuiltObjects(c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *statefulComponent) Create(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Create(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr())) +} + +func (c *statefulComponent) Update(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Update(reqCtx, cli, c.newBuilder(reqCtx, cli, nil)) +} + +func (c *statefulComponent) Status(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return c.StatefulComponentBase.Status(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionNoopPtr())) +} diff --git a/controllers/apps/components/stateful/component_stateful_workload.go b/controllers/apps/components/stateful/component_stateful_workload.go new file mode 100644 index 000000000..dcc1dc5af --- /dev/null +++ b/controllers/apps/components/stateful/component_stateful_workload.go @@ -0,0 +1,34 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package stateful + +import ( + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" +) + +type statefulComponentWorkloadBuilder struct { + internal.ComponentWorkloadBuilderBase +} + +var _ internal.ComponentWorkloadBuilder = &statefulComponentWorkloadBuilder{} + +func (b *statefulComponentWorkloadBuilder) BuildWorkload() internal.ComponentWorkloadBuilder { + return b.BuildWorkload4StatefulSet("stateful") +} diff --git a/controllers/apps/components/stateful/stateful.go b/controllers/apps/components/stateful/stateful.go index 9647ab4e6..cf44c5934 100644 --- a/controllers/apps/components/stateful/stateful.go +++ b/controllers/apps/components/stateful/stateful.go @@ -1,85 +1,99 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateful import ( "context" + "errors" "time" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" "k8s.io/kubectl/pkg/util/podutils" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/controllers/apps/components/types" "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) type Stateful struct { - Cli client.Client - Cluster *appsv1alpha1.Cluster - Component *appsv1alpha1.ClusterComponentSpec - componentDef *appsv1alpha1.ClusterComponentDefinition + types.ComponentSetBase +} + +var _ types.ComponentSet = &Stateful{} + +func (r *Stateful) getReplicas() int32 { + if r.Component != nil { + return r.Component.GetReplicas() + } + return r.ComponentSpec.Replicas } -var _ types.Component = &Stateful{} +func (r *Stateful) SetComponent(comp types.Component) { + r.Component = comp +} -func (stateful *Stateful) IsRunning(ctx context.Context, obj client.Object) (bool, error) { +func (r *Stateful) IsRunning(ctx context.Context, obj client.Object) (bool, error) { if obj == nil { return false, nil } sts := util.ConvertToStatefulSet(obj) - isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, stateful.Cli, sts) + isRevisionConsistent, err := util.IsStsAndPodsRevisionConsistent(ctx, r.Cli, sts) if err != nil { return false, err } - return util.StatefulSetOfComponentIsReady(sts, isRevisionConsistent, &stateful.Component.Replicas), nil + targetReplicas := r.getReplicas() + return util.StatefulSetOfComponentIsReady(sts, isRevisionConsistent, &targetReplicas), nil } -func (stateful *Stateful) PodsReady(ctx context.Context, obj client.Object) (bool, error) { +func (r *Stateful) PodsReady(ctx context.Context, obj client.Object) (bool, error) { if obj == nil { return false, nil } sts := util.ConvertToStatefulSet(obj) - return util.StatefulSetPodsAreReady(sts, stateful.Component.Replicas), nil + return util.StatefulSetPodsAreReady(sts, r.getReplicas()), nil } -func (stateful *Stateful) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { +func (r *Stateful) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { if pod == nil { return false } return podutils.IsPodAvailable(pod, minReadySeconds, metav1.Time{Time: time.Now()}) } -// HandleProbeTimeoutWhenPodsReady the Stateful component has no role detection, empty implementation here. -func (stateful *Stateful) HandleProbeTimeoutWhenPodsReady(ctx context.Context, recorder record.EventRecorder) (bool, error) { - return false, nil +func (r *Stateful) GetPhaseWhenPodsReadyAndProbeTimeout(pods []*corev1.Pod) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap) { + return "", nil } // GetPhaseWhenPodsNotReady gets the component phase when the pods of component are not ready. -func (stateful *Stateful) GetPhaseWhenPodsNotReady(ctx context.Context, componentName string) (appsv1alpha1.ClusterComponentPhase, error) { +func (r *Stateful) GetPhaseWhenPodsNotReady(ctx context.Context, componentName string) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) { stsList := &appsv1.StatefulSetList{} - podList, err := util.GetCompRelatedObjectList(ctx, stateful.Cli, *stateful.Cluster, componentName, stsList) + podList, err := util.GetCompRelatedObjectList(ctx, r.Cli, *r.Cluster, componentName, stsList) if err != nil || len(stsList.Items) == 0 { - return "", err + return "", nil, err } // if the failed pod is not controlled by the latest revision checkExistFailedPodOfLatestRevision := func(pod *corev1.Pod, workload metav1.Object) bool { @@ -87,27 +101,166 @@ func (stateful *Stateful) GetPhaseWhenPodsNotReady(ctx context.Context, componen return !intctrlutil.PodIsReady(pod) && intctrlutil.PodIsControlledByLatestRevision(pod, sts) } stsObj := stsList.Items[0] - return util.GetComponentPhaseWhenPodsNotReady(podList, &stsObj, stateful.Component.Replicas, - stsObj.Status.AvailableReplicas, checkExistFailedPodOfLatestRevision), nil + return util.GetComponentPhaseWhenPodsNotReady(podList, &stsObj, r.getReplicas(), + stsObj.Status.AvailableReplicas, checkExistFailedPodOfLatestRevision), nil, nil +} + +func (r *Stateful) HandleRestart(context.Context, client.Object) ([]graph.Vertex, error) { + return nil, nil +} + +func (r *Stateful) HandleRoleChange(context.Context, client.Object) ([]graph.Vertex, error) { + return nil, nil +} + +func (r *Stateful) HandleHA(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + return nil, nil } -func (stateful *Stateful) HandleUpdate(ctx context.Context, obj client.Object) error { +// HandleUpdateWithProcessors extends HandleUpdate() with custom processors +// REVIEW/TODO: (nashtsai) +// 1. too many args +func (r *Stateful) HandleUpdateWithProcessors(ctx context.Context, obj client.Object, + compStatusProcessor func(compDef *appsv1alpha1.ClusterComponentDefinition, pods []corev1.Pod, componentName string) error, + priorityMapper func(component *appsv1alpha1.ClusterComponentDefinition) map[string]int, + serialStrategyHandler, bestEffortParallelStrategyHandler, parallelStrategyHandler func(plan *util.Plan, pods []corev1.Pod, rolePriorityMap map[string]int)) error { + if r == nil { + return nil + } + + stsObj := util.ConvertToStatefulSet(obj) + // get compDefName from stsObj.name + compDefName := r.Cluster.Spec.GetComponentDefRefName(stsObj.Labels[constant.KBAppComponentLabelKey]) + + // get componentDef from ClusterDefinition by compDefName + componentDef, err := util.GetComponentDefByCluster(ctx, r.Cli, *r.Cluster, compDefName) + if err != nil { + return err + } + + if componentDef == nil || componentDef.IsStatelessWorkload() { + return nil + } + pods, err := util.GetPodListByStatefulSet(ctx, r.Cli, stsObj) + if err != nil { + return err + } + + // update cluster.status.component.consensusSetStatus when all pods currently exist + if compStatusProcessor != nil { + componentName := stsObj.Labels[constant.KBAppComponentLabelKey] + if err = compStatusProcessor(componentDef, pods, componentName); err != nil { + return err + } + } + + // prepare to do pods Deletion, that's the only thing we should do, + // the statefulset reconciler will do the rest. + // to simplify the process, we do pods Deletion after statefulset reconciliation done, + // that is stsObj.Generation == stsObj.Status.ObservedGeneration + if stsObj.Generation != stsObj.Status.ObservedGeneration { + return nil + } + + // then we wait for all pods' presence, that is len(pods) == stsObj.Spec.Replicas + // at that point, we have enough info about the previous pods before delete the current one + if len(pods) != int(*stsObj.Spec.Replicas) { + return nil + } + + // generate the pods Deletion plan + plan := generateUpdatePlan(ctx, r.Cli, stsObj, pods, componentDef, priorityMapper, + serialStrategyHandler, bestEffortParallelStrategyHandler, parallelStrategyHandler) + // execute plan + if _, err := plan.WalkOneStep(); err != nil { + return err + } return nil } -func NewStateful( - cli client.Client, - cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec, - componentDef appsv1alpha1.ClusterComponentDefinition, -) (*Stateful, error) { - if err := util.ComponentRuntimeReqArgsCheck(cli, cluster, component); err != nil { - return nil, err +func (r *Stateful) HandleUpdate(ctx context.Context, obj client.Object) error { + if r == nil { + return nil } + return r.HandleUpdateWithProcessors(ctx, obj, nil, nil, nil, nil, nil) +} + +func newStateful(cli client.Client, + cluster *appsv1alpha1.Cluster, + spec *appsv1alpha1.ClusterComponentSpec, + def appsv1alpha1.ClusterComponentDefinition) *Stateful { return &Stateful{ - Cli: cli, - Cluster: cluster, - Component: component, - componentDef: &componentDef, - }, nil + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: spec, + ComponentDef: &def, + Component: nil, + }, + } +} + +// generateConsensusUpdatePlan generates Update plan based on UpdateStrategy +func generateUpdatePlan(ctx context.Context, cli client.Client, stsObj *appsv1.StatefulSet, pods []corev1.Pod, + componentDef *appsv1alpha1.ClusterComponentDefinition, + priorityMapper func(component *appsv1alpha1.ClusterComponentDefinition) map[string]int, + serialStrategyHandler, bestEffortParallelStrategyHandler, parallelStrategyHandler func(plan *util.Plan, pods []corev1.Pod, rolePriorityMap map[string]int)) *util.Plan { + stsWorkload := componentDef.GetStatefulSetWorkload() + _, s := stsWorkload.FinalStsUpdateStrategy() + switch s.Type { + case appsv1.RollingUpdateStatefulSetStrategyType, "": + return nil + } + + plan := &util.Plan{} + plan.Start = &util.Step{} + plan.WalkFunc = func(obj interface{}) (bool, error) { + pod, ok := obj.(corev1.Pod) + if !ok { + return false, errors.New("wrong type: obj not Pod") + } + + // if DeletionTimestamp is not nil, it is terminating. + if pod.DeletionTimestamp != nil { + return true, nil + } + + // if pod is the latest version, we do nothing + if intctrlutil.GetPodRevision(&pod) == stsObj.Status.UpdateRevision { + // wait until ready + return !intctrlutil.PodIsReadyWithLabel(pod), nil + } + + // delete the pod to trigger associate StatefulSet to re-create it + if err := cli.Delete(ctx, &pod); err != nil && !apierrors.IsNotFound(err) { + return false, err + } + + return true, nil + } + + var rolePriorityMap map[string]int + if priorityMapper != nil { + rolePriorityMap = priorityMapper(componentDef) + util.SortPods(pods, rolePriorityMap, constant.RoleLabelKey) + } + + // generate plan by UpdateStrategy + switch stsWorkload.GetUpdateStrategy() { + case appsv1alpha1.ParallelStrategy: + if parallelStrategyHandler != nil { + parallelStrategyHandler(plan, pods, rolePriorityMap) + } + case appsv1alpha1.BestEffortParallelStrategy: + if bestEffortParallelStrategyHandler != nil { + bestEffortParallelStrategyHandler(plan, pods, rolePriorityMap) + } + case appsv1alpha1.SerialStrategy: + fallthrough + default: + if serialStrategyHandler != nil { + serialStrategyHandler(plan, pods, rolePriorityMap) + } + } + return plan } diff --git a/controllers/apps/components/stateful/stateful_test.go b/controllers/apps/components/stateful/stateful_test.go index 5abd6114a..b71bcb416 100644 --- a/controllers/apps/components/stateful/stateful_test.go +++ b/controllers/apps/components/stateful/stateful_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateful @@ -24,6 +27,7 @@ import ( . "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -47,7 +51,7 @@ var _ = Describe("Stateful Component", func() { statefulCompName = "stateful" ) cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -70,9 +74,9 @@ var _ = Describe("Stateful Component", func() { Context("Stateful Component test", func() { It("Stateful Component test", func() { By(" init cluster, statefulSet, pods") - clusterDef, _, cluster := testapps.InitConsensusMysql(testCtx, clusterDefName, + clusterDef, _, cluster := testapps.InitConsensusMysql(&testCtx, clusterDefName, clusterVersionName, clusterName, statefulCompDefRef, statefulCompName) - _ = testapps.MockConsensusComponentStatefulSet(testCtx, clusterName, statefulCompName) + _ = testapps.MockConsensusComponentStatefulSet(&testCtx, clusterName, statefulCompName) stsList := &appsv1.StatefulSetList{} Eventually(func() bool { _ = k8sClient.List(ctx, stsList, client.InNamespace(testCtx.DefaultNamespace), client.MatchingLabels{ @@ -86,49 +90,46 @@ var _ = Describe("Stateful Component", func() { sts := &stsList.Items[0] clusterComponent := cluster.Spec.GetComponentByName(statefulCompName) componentDef := clusterDef.GetComponentDefByName(clusterComponent.ComponentDefRef) - stateful, err := NewStateful(k8sClient, cluster, clusterComponent, *componentDef) - Expect(err).Should(Succeed()) - phase, _ := stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) + stateful := newStateful(k8sClient, cluster, clusterComponent, *componentDef) + phase, _, _ := stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) Expect(phase == appsv1alpha1.FailedClusterCompPhase).Should(BeTrue()) By("test pods are not ready") - updateRevison := fmt.Sprintf("%s-%s-%s", clusterName, statefulCompName, "6fdd48d9cd") + updateRevision := fmt.Sprintf("%s-%s-%s", clusterName, statefulCompName, "6fdd48d9cd") Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { availableReplicas := *sts.Spec.Replicas - 1 sts.Status.AvailableReplicas = availableReplicas sts.Status.ReadyReplicas = availableReplicas sts.Status.Replicas = availableReplicas sts.Status.ObservedGeneration = 1 - sts.Status.UpdateRevision = updateRevison + sts.Status.UpdateRevision = updateRevision })).Should(Succeed()) podsReady, _ := stateful.PodsReady(ctx, sts) - Expect(podsReady == false).Should(BeTrue()) + Expect(podsReady).Should(BeFalse()) By("create pods of sts") - podList := testapps.MockConsensusComponentPods(testCtx, sts, clusterName, statefulCompName) + podList := testapps.MockConsensusComponentPods(&testCtx, sts, clusterName, statefulCompName) By("test stateful component is abnormal") - // mock pod is not ready + // mock pod scheduled failure pod := podList[0] - Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { - pod.Status.Conditions = nil - })).Should(Succeed()) + testk8s.UpdatePodStatusScheduleFailed(ctx, testCtx, pod.Name, pod.Namespace) Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(sts), func(g Gomega, tmpSts *appsv1.StatefulSet) { g.Expect(tmpSts.Status.AvailableReplicas == *sts.Spec.Replicas-1).Should(BeTrue()) })).Should(Succeed()) - phase, _ = stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) - Expect(phase == appsv1alpha1.AbnormalClusterCompPhase).Should(BeTrue()) + phase, _, _ = stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) + Expect(phase).Should(Equal(appsv1alpha1.AbnormalClusterCompPhase)) By("not ready pod is not controlled by latest revision, should return empty string") // mock pod is not controlled by latest revision - Expect(testapps.ChangeObj(&testCtx, pod, func() { - pod.Labels[appsv1.ControllerRevisionHashLabelKey] = fmt.Sprintf("%s-%s-%s", clusterName, statefulCompName, "5wdsd8d9fs") + Expect(testapps.ChangeObj(&testCtx, pod, func(lpod *corev1.Pod) { + lpod.Labels[appsv1.ControllerRevisionHashLabelKey] = fmt.Sprintf("%s-%s-%s", clusterName, statefulCompName, "5wdsd8d9fs") })).Should(Succeed()) - phase, _ = stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) - Expect(len(phase) == 0).Should(BeTrue()) + phase, _, _ = stateful.GetPhaseWhenPodsNotReady(ctx, statefulCompName) + Expect(string(phase)).Should(Equal("")) // reset updateRevision - Expect(testapps.ChangeObj(&testCtx, pod, func() { - pod.Labels[appsv1.ControllerRevisionHashLabelKey] = updateRevison + Expect(testapps.ChangeObj(&testCtx, pod, func(lpod *corev1.Pod) { + lpod.Labels[appsv1.ControllerRevisionHashLabelKey] = updateRevision })).Should(Succeed()) By("test pod is available") @@ -140,24 +141,25 @@ var _ = Describe("Stateful Component", func() { // mock sts is ready testk8s.MockStatefulSetReady(sts) podsReady, _ = stateful.PodsReady(ctx, sts) - Expect(podsReady == true).Should(BeTrue()) + Expect(podsReady).Should(BeTrue()) By("test component.replicas is inconsistent with sts.spec.replicas") oldReplicas := clusterComponent.Replicas replicas := int32(4) clusterComponent.Replicas = replicas isRunning, _ := stateful.IsRunning(ctx, sts) - Expect(isRunning == false).Should(BeTrue()) + Expect(isRunning).Should(BeFalse()) // reset replicas clusterComponent.Replicas = oldReplicas By("test component is running") isRunning, _ = stateful.IsRunning(ctx, sts) - Expect(isRunning == true).Should(BeTrue()) + Expect(isRunning).Should(BeTrue()) - By("test handle probe timed out") - requeue, _ := stateful.HandleProbeTimeoutWhenPodsReady(ctx, nil) - Expect(requeue == false).Should(BeTrue()) + // TODO(refactor): probe timed-out pod + // By("test handle probe timed out") + // requeue, _ := stateful.HandleProbeTimeoutWhenPodsReady(ctx, nil) + // Expect(requeue == false).Should(BeTrue()) }) }) diff --git a/controllers/apps/components/stateful/suite_test.go b/controllers/apps/components/stateful/suite_test.go index c26a77511..fb40c857b 100644 --- a/controllers/apps/components/stateful/suite_test.go +++ b/controllers/apps/components/stateful/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateful diff --git a/controllers/apps/components/stateful_set_controller.go b/controllers/apps/components/stateful_set_controller.go deleted file mode 100644 index 5a8d146f3..000000000 --- a/controllers/apps/components/stateful_set_controller.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "context" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/tools/record" - ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// StatefulSetReconciler reconciles a statefulset object -type StatefulSetReconciler struct { - client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder -} - -// +kubebuilder:rbac:groups=apps,resources=statefulsets,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=apps,resources=statefulsets/status,verbs=get -// +kubebuilder:rbac:groups=apps,resources=statefulsets/finalizers,verbs=update - -// 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.14.4/pkg/reconcile -func (r *StatefulSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - var ( - sts = &appsv1.StatefulSet{} - err error - ) - - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Req: req, - Log: log.FromContext(ctx).WithValues("statefulSet", req.NamespacedName), - } - - if err = r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, sts); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - - return workloadCompClusterReconcile(reqCtx, r.Client, sts, - func(cluster *appsv1alpha1.Cluster, componentSpec *appsv1alpha1.ClusterComponentSpec, component types.Component) (ctrl.Result, error) { - compCtx := newComponentContext(reqCtx, r.Client, r.Recorder, component, sts, componentSpec) - // patch the current componentSpec workload's custom labels - if err := patchWorkloadCustomLabel(reqCtx.Ctx, r.Client, cluster, componentSpec); err != nil { - reqCtx.Recorder.Event(cluster, corev1.EventTypeWarning, "StatefulSet Controller PatchWorkloadCustomLabelFailed", err.Error()) - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - reqCtx.Log.V(1).Info("before updateComponentStatusInClusterStatus", - "generation", sts.Generation, "observed generation", sts.Status.ObservedGeneration, - "replicas", sts.Status.Replicas) - if requeueAfter, err := updateComponentStatusInClusterStatus(compCtx, cluster); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } else if requeueAfter != 0 { - // if the reconcileAction need requeue, do it - return intctrlutil.RequeueAfter(requeueAfter, reqCtx.Log, "") - } - return intctrlutil.Reconciled() - }) -} - -// SetupWithManager sets up the controller with the Manager. -func (r *StatefulSetReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&appsv1.StatefulSet{}). - Owns(&corev1.Pod{}). - WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.WorkloadFilterPredicate)). - Complete(r) -} diff --git a/controllers/apps/components/stateful_set_controller_test.go b/controllers/apps/components/stateful_set_controller_test.go deleted file mode 100644 index e03ae6343..000000000 --- a/controllers/apps/components/stateful_set_controller_test.go +++ /dev/null @@ -1,208 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 components - -import ( - "fmt" - "time" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - intctrlutil "github.com/apecloud/kubeblocks/internal/generics" - testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" - testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" -) - -var _ = Describe("StatefulSet Controller", func() { - - var ( - randomStr = testCtx.GetRandomStr() - clusterName = "mysql-" + randomStr - clusterDefName = "cluster-definition-consensus-" + randomStr - clusterVersionName = "cluster-version-operations-" + randomStr - opsRequestName = "wesql-restart-test-" + randomStr - ) - - const ( - revisionID = "6fdd48d9cd" - consensusCompName = "consensus" - consensusCompType = "consensus" - ) - - cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, - // otherwise if later it needs to create some new resource objects with the same name, - // in race conditions, it will find the existence of old objects, resulting failure to - // create the new objects. - By("clean resources") - - // delete cluster(and all dependent sub-resources), clusterversion and clusterdef - testapps.ClearClusterResources(&testCtx) - - // clear rest resources - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // namespaced resources - testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml, client.GracePeriodSeconds(0)) - } - - BeforeEach(cleanAll) - - AfterEach(cleanAll) - - testUpdateStrategy := func(updateStrategy appsv1alpha1.UpdateStrategy, componentName string, index int) { - Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKey{Name: clusterDefName}, - func(clusterDef *appsv1alpha1.ClusterDefinition) { - clusterDef.Spec.ComponentDefs[0].ConsensusSpec.UpdateStrategy = appsv1alpha1.SerialStrategy - })()).Should(Succeed()) - - // mock consensus component is not ready - objectKey := client.ObjectKey{Name: clusterName + "-" + componentName, Namespace: testCtx.DefaultNamespace} - Expect(testapps.GetAndChangeObjStatus(&testCtx, objectKey, func(newSts *appsv1.StatefulSet) { - newSts.Status.UpdateRevision = fmt.Sprintf("%s-%s-%dfdd48d8cd", clusterName, componentName, index) - newSts.Status.ObservedGeneration = newSts.Generation - 1 - })()).Should(Succeed()) - } - - testUsingEnvTest := func(sts *appsv1.StatefulSet) []*corev1.Pod { - By("mock statefulset update completed") - updateRevision := fmt.Sprintf("%s-%s-%s", clusterName, consensusCompName, revisionID) - Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { - sts.Status.UpdateRevision = updateRevision - testk8s.MockStatefulSetReady(sts) - sts.Status.ObservedGeneration = 2 - })).Should(Succeed()) - - By("create pods of statefulset") - pods := testapps.MockConsensusComponentPods(testCtx, sts, clusterName, consensusCompName) - - By("Mock a pod without role label and it will wait for HandleProbeTimeoutWhenPodsReady") - leaderPod := pods[0] - Expect(testapps.ChangeObj(&testCtx, leaderPod, func() { - delete(leaderPod.Labels, constant.RoleLabelKey) - })).Should(Succeed()) - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(leaderPod), func(g Gomega, pod *corev1.Pod) { - g.Expect(pod.Labels[constant.RoleLabelKey] == "").Should(BeTrue()) - })).Should(Succeed()) - - By("mock restart component to trigger reconcile of StatefulSet controller") - Expect(testapps.ChangeObj(&testCtx, sts, func() { - sts.Spec.Template.Annotations = map[string]string{ - constant.RestartAnnotationKey: time.Now().Format(time.RFC3339), - } - })).Should(Succeed()) - - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(sts), - func(g Gomega, fetched *appsv1.StatefulSet) { - g.Expect(fetched.Status.UpdateRevision).To(Equal(updateRevision)) - })).Should(Succeed()) - - By("wait for component podsReady to be true and phase to be 'Rebooting'") - clusterKey := client.ObjectKey{Name: clusterName, Namespace: testCtx.DefaultNamespace} - Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { - compStatus := cluster.Status.Components[consensusCompName] - g.Expect(compStatus.Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) // original expecting value RebootingPhase - g.Expect(compStatus.PodsReady).ShouldNot(BeNil()) - g.Expect(*compStatus.PodsReady).Should(BeTrue()) - // REVIEW/TODO: ought add extra condtion check for RebootingPhase - })).Should(Succeed()) - - By("add leader role label for leaderPod to mock consensus component to be Running") - Expect(testapps.ChangeObj(&testCtx, leaderPod, func() { - leaderPod.Labels[constant.RoleLabelKey] = "leader" - })).Should(Succeed()) - return pods - } - - Context("test controller", func() { - It("test statefulSet controller", func() { - By("mock cluster object") - _, _, cluster := testapps.InitConsensusMysql(testCtx, clusterDefName, - clusterVersionName, clusterName, consensusCompType, consensusCompName) - - // REVIEW/TODO: "Rebooting" got refactored - By("mock cluster phase is 'Rebooting' and restart operation is running on cluster") - Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { - cluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase - cluster.Status.ObservedGeneration = 1 - cluster.Status.Components = map[string]appsv1alpha1.ClusterComponentStatus{ - consensusCompName: { - Phase: appsv1alpha1.SpecReconcilingClusterCompPhase, - }, - } - })).Should(Succeed()) - _ = testapps.CreateRestartOpsRequest(testCtx, clusterName, opsRequestName, []string{consensusCompName}) - Expect(testapps.ChangeObj(&testCtx, cluster, func() { - cluster.Annotations = map[string]string{ - constant.OpsRequestAnnotationKey: fmt.Sprintf(`[{"name":"%s","clusterPhase":"Updating"}]`, opsRequestName), - } - })).Should(Succeed()) - - // trigger statefulset controller Reconcile - sts := testapps.MockConsensusComponentStatefulSet(testCtx, clusterName, consensusCompName) - - By("mock the StatefulSet and pods are ready") - // mock statefulSet available and consensusSet component is running - pods := testUsingEnvTest(sts) - - By("check the component phase becomes Running") - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, consensusCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) - - By("mock component of cluster is stopping") - Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { - tmpCluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase - tmpCluster.Status.SetComponentStatus(consensusCompName, appsv1alpha1.ClusterComponentStatus{ - Phase: appsv1alpha1.SpecReconcilingClusterCompPhase, - }) - })()).Should(Succeed()) - - By("mock stop operation and processed successfully") - Expect(testapps.ChangeObj(&testCtx, cluster, func() { - cluster.Spec.ComponentSpecs[0].Replicas = 0 - })).Should(Succeed()) - Expect(testapps.ChangeObj(&testCtx, sts, func() { - replicas := int32(0) - sts.Spec.Replicas = &replicas - })).Should(Succeed()) - Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { - testk8s.MockStatefulSetReady(sts) - })).Should(Succeed()) - // delete all pods of components - for _, v := range pods { - testapps.DeleteObject(&testCtx, client.ObjectKeyFromObject(v), v) - } - - By("check the component phase becomes Stopped") - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterName, consensusCompName)).Should(Equal(appsv1alpha1.StoppedClusterCompPhase)) - - By("test updateStrategy with Serial") - testUpdateStrategy(appsv1alpha1.SerialStrategy, consensusCompName, 1) - - By("test updateStrategy with Parallel") - testUpdateStrategy(appsv1alpha1.ParallelStrategy, consensusCompName, 2) - }) - }) -}) diff --git a/controllers/apps/components/stateless/component_stateless.go b/controllers/apps/components/stateless/component_stateless.go new file mode 100644 index 000000000..ab706fe87 --- /dev/null +++ b/controllers/apps/components/stateless/component_stateless.go @@ -0,0 +1,283 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package stateless + +import ( + "fmt" + "reflect" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/controllers/apps/components/types" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +func NewStatelessComponent(cli client.Client, + recorder record.EventRecorder, + cluster *appsv1alpha1.Cluster, + clusterVersion *appsv1alpha1.ClusterVersion, + synthesizedComponent *component.SynthesizedComponent, + dag *graph.DAG) *statelessComponent { + comp := &statelessComponent{ + ComponentBase: internal.ComponentBase{ + Client: cli, + Recorder: recorder, + Cluster: cluster, + ClusterVersion: clusterVersion, + Component: synthesizedComponent, + ComponentSet: &Stateless{ + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: nil, + ComponentDef: nil, + Component: nil, + }, + }, + Dag: dag, + WorkloadVertex: nil, + }, + } + comp.ComponentSet.SetComponent(comp) + return comp +} + +type statelessComponent struct { + internal.ComponentBase + // runningWorkload can be nil, and the replicas of workload can be nil (zero) + runningWorkload *appsv1.Deployment +} + +var _ types.Component = &statelessComponent{} + +func (c *statelessComponent) newBuilder(reqCtx intctrlutil.RequestCtx, cli client.Client, + action *ictrltypes.LifecycleAction) internal.ComponentWorkloadBuilder { + builder := &statelessComponentWorkloadBuilder{ + ComponentWorkloadBuilderBase: internal.ComponentWorkloadBuilderBase{ + ReqCtx: reqCtx, + Client: cli, + Comp: c, + DefaultAction: action, + Error: nil, + EnvConfig: nil, + Workload: nil, + }, + } + builder.ConcreteBuilder = builder + return builder +} + +func (c *statelessComponent) init(reqCtx intctrlutil.RequestCtx, cli client.Client, builder internal.ComponentWorkloadBuilder, load bool) error { + var err error + if builder != nil { + if err = builder.BuildEnv(). + BuildWorkload(). + BuildPDB(). + BuildHeadlessService(). + BuildConfig(). + BuildTLSVolume(). + BuildVolumeMount(). + BuildService(). + BuildTLSCert(). + Complete(); err != nil { + return err + } + } + if load { + c.runningWorkload, err = c.loadRunningWorkload(reqCtx, cli) + if err != nil { + return err + } + } + return nil +} + +func (c *statelessComponent) loadRunningWorkload(reqCtx intctrlutil.RequestCtx, cli client.Client) (*appsv1.Deployment, error) { + deployList, err := util.ListDeployOwnedByComponent(reqCtx.Ctx, cli, c.GetNamespace(), c.GetMatchingLabels()) + if err != nil { + return nil, err + } + cnt := len(deployList) + if cnt == 1 { + return deployList[0], nil + } + if cnt == 0 { + return nil, nil + } else { + return nil, fmt.Errorf("more than one workloads found for the stateless component, cluster: %s, component: %s, cnt: %d", + c.GetClusterName(), c.GetName(), cnt) + } +} + +func (c *statelessComponent) GetWorkloadType() appsv1alpha1.WorkloadType { + return appsv1alpha1.Stateless +} + +func (c *statelessComponent) GetBuiltObjects(reqCtx intctrlutil.RequestCtx, cli client.Client) ([]client.Object, error) { + dag := c.Dag + defer func() { + c.Dag = dag + }() + + c.Dag = graph.NewDAG() + if err := c.init(intctrlutil.RequestCtx{}, nil, c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr()), false); err != nil { + return nil, err + } + + objs := make([]client.Object, 0) + for _, v := range c.Dag.Vertices() { + if vv, ok := v.(*ictrltypes.LifecycleVertex); ok { + objs = append(objs, vv.Obj) + } + } + return objs, nil +} + +func (c *statelessComponent) Create(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + if err := c.init(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionCreatePtr()), false); err != nil { + return err + } + + if err := c.ValidateObjectsAction(); err != nil { + return err + } + + c.SetStatusPhase(appsv1alpha1.CreatingClusterCompPhase, nil, "Create a new component") + + return nil +} + +func (c *statelessComponent) Delete(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + // TODO(impl): delete component owned resources + return nil +} + +func (c *statelessComponent) Update(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + if err := c.init(reqCtx, cli, c.newBuilder(reqCtx, cli, nil), true); err != nil { + return err + } + + if c.runningWorkload != nil { + if err := c.Restart(reqCtx, cli); err != nil { + return err + } + + // cluster.spec.componentSpecs[*].volumeClaimTemplates[*].spec.resources.requests[corev1.ResourceStorage] + if err := c.ExpandVolume(reqCtx, cli); err != nil { + return err + } + + // cluster.spec.componentSpecs[*].replicas + if err := c.HorizontalScale(reqCtx, cli); err != nil { + return err + } + } + + if err := c.updateUnderlyingResources(reqCtx, cli, c.runningWorkload); err != nil { + return err + } + + return c.ResolveObjectsAction(reqCtx, cli) +} + +func (c *statelessComponent) Status(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + if err := c.init(reqCtx, cli, c.newBuilder(reqCtx, cli, ictrltypes.ActionNoopPtr()), true); err != nil { + return err + } + if c.runningWorkload == nil { + return nil + } + return c.ComponentBase.StatusWorkload(reqCtx, cli, c.runningWorkload, nil) +} + +func (c *statelessComponent) ExpandVolume(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return nil +} + +func (c *statelessComponent) HorizontalScale(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + if c.runningWorkload.Spec.Replicas == nil && c.Component.Replicas > 0 { + reqCtx.Recorder.Eventf(c.Cluster, + corev1.EventTypeNormal, + "HorizontalScale", + "start horizontal scale component %s of cluster %s from %d to %d", + c.GetName(), c.GetClusterName(), 0, c.Component.Replicas) + } else if c.runningWorkload.Spec.Replicas != nil && *c.runningWorkload.Spec.Replicas != c.Component.Replicas { + reqCtx.Recorder.Eventf(c.Cluster, + corev1.EventTypeNormal, + "HorizontalScale", + "start horizontal scale component %s of cluster %s from %d to %d", + c.GetName(), c.GetClusterName(), *c.runningWorkload.Spec.Replicas, c.Component.Replicas) + } + return nil +} + +func (c *statelessComponent) Restart(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return util.RestartPod(&c.runningWorkload.Spec.Template) +} + +func (c *statelessComponent) Reconfigure(reqCtx intctrlutil.RequestCtx, cli client.Client) error { + return nil // TODO(impl) +} + +func (c *statelessComponent) updateUnderlyingResources(reqCtx intctrlutil.RequestCtx, cli client.Client, deployObj *appsv1.Deployment) error { + if deployObj == nil { + c.createWorkload() + } else { + c.updateWorkload(deployObj) + } + if err := c.UpdatePDB(reqCtx, cli); err != nil { + return err + } + if err := c.UpdateService(reqCtx, cli); err != nil { + return err + } + return nil +} + +func (c *statelessComponent) createWorkload() { + deployProto := c.WorkloadVertex.Obj.(*appsv1.Deployment) + c.WorkloadVertex.Obj = deployProto + c.WorkloadVertex.Action = ictrltypes.ActionCreatePtr() + c.SetStatusPhase(appsv1alpha1.SpecReconcilingClusterCompPhase, nil, "Component workload created") +} + +func (c *statelessComponent) updateWorkload(deployObj *appsv1.Deployment) { + deployObjCopy := deployObj.DeepCopy() + deployProto := c.WorkloadVertex.Obj.(*appsv1.Deployment) + + util.MergeAnnotations(deployObj.Spec.Template.Annotations, &deployProto.Spec.Template.Annotations) + util.BuildWorkLoadAnnotations(deployObjCopy, c.Cluster) + deployObjCopy.Spec = deployProto.Spec + if !reflect.DeepEqual(&deployObj.Spec, &deployObjCopy.Spec) { + // TODO(REVIEW): always return true and update component phase to Updating. deployObj.Spec contains default values which set by Kubernetes + c.WorkloadVertex.Obj = deployObjCopy + c.WorkloadVertex.Action = ictrltypes.ActionUpdatePtr() + c.SetStatusPhase(appsv1alpha1.SpecReconcilingClusterCompPhase, nil, "Component workload updated") + } +} diff --git a/controllers/apps/components/stateless/component_stateless_workload.go b/controllers/apps/components/stateless/component_stateless_workload.go new file mode 100644 index 000000000..66f628854 --- /dev/null +++ b/controllers/apps/components/stateless/component_stateless_workload.go @@ -0,0 +1,45 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package stateless + +import ( + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/controllers/apps/components/internal" + "github.com/apecloud/kubeblocks/internal/controller/builder" +) + +type statelessComponentWorkloadBuilder struct { + internal.ComponentWorkloadBuilderBase +} + +var _ internal.ComponentWorkloadBuilder = &statelessComponentWorkloadBuilder{} + +func (b *statelessComponentWorkloadBuilder) BuildWorkload() internal.ComponentWorkloadBuilder { + buildfn := func() ([]client.Object, error) { + deploy, err := builder.BuildDeployLow(b.ReqCtx, b.Comp.GetCluster(), b.Comp.GetSynthesizedComponent()) + if err != nil { + return nil, err + } + b.Workload = deploy + return nil, nil // don't return deployment here + } + return b.BuildWrapper(buildfn) +} diff --git a/controllers/apps/components/stateless/stateless.go b/controllers/apps/components/stateless/stateless.go index 2ce6decf6..ad7f12181 100644 --- a/controllers/apps/components/stateless/stateless.go +++ b/controllers/apps/components/stateless/stateless.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateless @@ -25,7 +28,6 @@ import ( appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" deploymentutil "k8s.io/kubectl/pkg/util/deployment" "k8s.io/kubectl/pkg/util/podutils" "sigs.k8s.io/controller-runtime/pkg/client" @@ -34,6 +36,7 @@ import ( "github.com/apecloud/kubeblocks/controllers/apps/components/types" "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) @@ -43,13 +46,21 @@ import ( const NewRSAvailableReason = "NewReplicaSetAvailable" type Stateless struct { - Cli client.Client - Cluster *appsv1alpha1.Cluster - Component *appsv1alpha1.ClusterComponentSpec - componentDef *appsv1alpha1.ClusterComponentDefinition + types.ComponentSetBase +} + +var _ types.ComponentSet = &Stateless{} + +func (stateless *Stateless) getReplicas() int32 { + if stateless.Component != nil { + return stateless.Component.GetReplicas() + } + return stateless.ComponentSpec.Replicas } -var _ types.Component = &Stateless{} +func (stateless *Stateless) SetComponent(comp types.Component) { + stateless.Component = comp +} func (stateless *Stateless) IsRunning(ctx context.Context, obj client.Object) (bool, error) { if stateless == nil { @@ -66,7 +77,8 @@ func (stateless *Stateless) PodsReady(ctx context.Context, obj client.Object) (b if !ok { return false, nil } - return deploymentIsReady(deploy, &stateless.Component.Replicas), nil + targetReplicas := stateless.getReplicas() + return deploymentIsReady(deploy, &targetReplicas), nil } func (stateless *Stateless) PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool { @@ -76,19 +88,17 @@ func (stateless *Stateless) PodIsAvailable(pod *corev1.Pod, minReadySeconds int3 return podutils.IsPodAvailable(pod, minReadySeconds, metav1.Time{Time: time.Now()}) } -// HandleProbeTimeoutWhenPodsReady the stateless component has no role detection, empty implementation here. -func (stateless *Stateless) HandleProbeTimeoutWhenPodsReady(ctx context.Context, - recorder record.EventRecorder) (bool, error) { - return false, nil +func (stateless *Stateless) GetPhaseWhenPodsReadyAndProbeTimeout(pods []*corev1.Pod) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap) { + return "", nil } // GetPhaseWhenPodsNotReady gets the component phase when the pods of component are not ready. func (stateless *Stateless) GetPhaseWhenPodsNotReady(ctx context.Context, - componentName string) (appsv1alpha1.ClusterComponentPhase, error) { + componentName string) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) { deployList := &appsv1.DeploymentList{} podList, err := util.GetCompRelatedObjectList(ctx, stateless.Cli, *stateless.Cluster, componentName, deployList) if err != nil || len(deployList.Items) == 0 { - return "", err + return "", nil, err } // if the failed pod is not controlled by the new ReplicaSetKind checkExistFailedPodOfNewRS := func(pod *corev1.Pod, workload metav1.Object) bool { @@ -96,31 +106,38 @@ func (stateless *Stateless) GetPhaseWhenPodsNotReady(ctx context.Context, return !intctrlutil.PodIsReady(pod) && belongToNewReplicaSet(d, pod) } deploy := &deployList.Items[0] - return util.GetComponentPhaseWhenPodsNotReady(podList, deploy, stateless.Component.Replicas, - deploy.Status.AvailableReplicas, checkExistFailedPodOfNewRS), nil + return util.GetComponentPhaseWhenPodsNotReady(podList, deploy, stateless.getReplicas(), + deploy.Status.AvailableReplicas, checkExistFailedPodOfNewRS), nil, nil } -func (stateless *Stateless) HandleUpdate(ctx context.Context, obj client.Object) error { - return nil +func (stateless *Stateless) HandleRestart(context.Context, client.Object) ([]graph.Vertex, error) { + return nil, nil } -func NewStateless( - cli client.Client, +func (stateless *Stateless) HandleRoleChange(context.Context, client.Object) ([]graph.Vertex, error) { + return nil, nil +} + +func (stateless *Stateless) HandleHA(ctx context.Context, obj client.Object) ([]graph.Vertex, error) { + return nil, nil +} + +func newStateless(cli client.Client, cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec, - componentDef appsv1alpha1.ClusterComponentDefinition) (*Stateless, error) { - if err := util.ComponentRuntimeReqArgsCheck(cli, cluster, component); err != nil { - return nil, err - } + spec *appsv1alpha1.ClusterComponentSpec, + def appsv1alpha1.ClusterComponentDefinition) *Stateless { return &Stateless{ - Cli: cli, - Cluster: cluster, - Component: component, - componentDef: &componentDef, - }, nil + ComponentSetBase: types.ComponentSetBase{ + Cli: cli, + Cluster: cluster, + ComponentSpec: spec, + ComponentDef: &def, + Component: nil, + }, + } } -// deploymentIsReady check deployment is ready +// deploymentIsReady checks deployment is ready func deploymentIsReady(deploy *appsv1.Deployment, targetReplicas *int32) bool { var ( componentIsRunning = true @@ -150,7 +167,7 @@ func deploymentIsReady(deploy *appsv1.Deployment, targetReplicas *int32) bool { return componentIsRunning } -// hasProgressDeadline checks if the Deployment d is expected to surface the reason +// hasProgressDeadline checks if the Deployment d is expected to suffice the reason // "ProgressDeadlineExceeded" when the Deployment progress takes longer than expected time. func hasProgressDeadline(d *appsv1.Deployment) bool { return d.Spec.ProgressDeadlineSeconds != nil && diff --git a/controllers/apps/components/stateless/stateless_test.go b/controllers/apps/components/stateless/stateless_test.go index b7b5c2428..842a8b1b9 100644 --- a/controllers/apps/components/stateless/stateless_test.go +++ b/controllers/apps/components/stateless/stateless_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateless @@ -43,12 +46,12 @@ var _ = Describe("Stateful Component", func() { ) const ( statelessCompName = "stateless" - statelessCompDefRef = "stateless" + statelessCompDefName = "stateless" defaultMinReadySeconds = 10 ) cleanAll := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -71,31 +74,28 @@ var _ = Describe("Stateful Component", func() { It("Stateless Component test", func() { By(" init cluster, deployment") clusterDef := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatelessNginxComponent, statelessCompDefRef). + AddComponentDef(testapps.StatelessNginxComponent, statelessCompDefName). Create(&testCtx).GetObject() cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(statelessCompName, statelessCompDefRef).SetReplicas(2).Create(&testCtx).GetObject() - deploy := testapps.MockStatelessComponentDeploy(testCtx, clusterName, statelessCompName) + AddComponent(statelessCompName, statelessCompDefName).SetReplicas(2).Create(&testCtx).GetObject() + deploy := testapps.MockStatelessComponentDeploy(&testCtx, clusterName, statelessCompName) clusterComponent := cluster.Spec.GetComponentByName(statelessCompName) componentDef := clusterDef.GetComponentDefByName(clusterComponent.ComponentDefRef) - statelessComponent, err := NewStateless(k8sClient, cluster, clusterComponent, *componentDef) - Expect(err).Should(Succeed()) + statelessComponent := newStateless(k8sClient, cluster, clusterComponent, *componentDef) By("test pods number of deploy is 0 ") - phase, _ := statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) + phase, _, _ := statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) Expect(phase == appsv1alpha1.FailedClusterCompPhase).Should(BeTrue()) By("test pod is ready") rsName := deploy.Name + "-5847cb795c" - pod := testapps.MockStatelessPod(testCtx, deploy, clusterName, statelessCompName, rsName+randomStr) + pod := testapps.MockStatelessPod(&testCtx, deploy, clusterName, statelessCompName, rsName+randomStr) lastTransTime := metav1.NewTime(time.Now().Add(-1 * (defaultMinReadySeconds + 1) * time.Second)) testk8s.MockPodAvailable(pod, lastTransTime) Expect(statelessComponent.PodIsAvailable(pod, defaultMinReadySeconds)).Should(BeTrue()) By("test a part pods of deploy are not ready") // mock pod is not ready - Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { - pod.Status.Conditions = nil - })).Should(Succeed()) + testk8s.UpdatePodStatusScheduleFailed(ctx, testCtx, pod.Name, pod.Namespace) // mock deployment is processing rs Expect(testapps.ChangeObjStatus(&testCtx, deploy, func() { deploy.Status.Conditions = []appsv1.DeploymentCondition{ @@ -115,31 +115,32 @@ var _ = Describe("Stateful Component", func() { deploy.Status.Replicas = availableReplicas })).Should(Succeed()) podsReady, _ := statelessComponent.PodsReady(ctx, deploy) - Expect(podsReady == false).Should(BeTrue()) - phase, _ = statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) - Expect(phase == appsv1alpha1.AbnormalClusterCompPhase).Should(BeTrue()) + Expect(podsReady).Should(BeFalse()) + phase, _, _ = statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) + Expect(phase).Should(Equal(appsv1alpha1.AbnormalClusterCompPhase)) By("test pods of deployment are ready") testk8s.MockDeploymentReady(deploy, NewRSAvailableReason, rsName) podsReady, _ = statelessComponent.PodsReady(ctx, deploy) - Expect(podsReady == true).Should(BeTrue()) + Expect(podsReady).Should(BeTrue()) By("test component.replicas is inconsistent with deployment.spec.replicas") oldReplicas := clusterComponent.Replicas replicas := int32(4) clusterComponent.Replicas = replicas isRunning, _ := statelessComponent.IsRunning(ctx, deploy) - Expect(isRunning == false).Should(BeTrue()) + Expect(isRunning).Should(BeFalse()) // reset replicas clusterComponent.Replicas = oldReplicas By("test component is running") isRunning, _ = statelessComponent.IsRunning(ctx, deploy) - Expect(isRunning == true).Should(BeTrue()) + Expect(isRunning).Should(BeTrue()) - By("test handle probe timed out") - requeue, _ := statelessComponent.HandleProbeTimeoutWhenPodsReady(ctx, nil) - Expect(requeue == false).Should(BeTrue()) + // TODO(refactor): probe timed-out pod + // By("test handle probe timed out") + // requeue, _ := statelessComponent.HandleProbeTimeoutWhenPodsReady(ctx, nil) + // Expect(requeue == false).Should(BeTrue()) By("test pod is not ready and not controlled by new ReplicaSet of deployment") Expect(testapps.ChangeObjStatus(&testCtx, deploy, func() { @@ -152,8 +153,8 @@ var _ = Describe("Stateful Component", func() { }, } })).Should(Succeed()) - phase, _ = statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) - Expect(len(phase) == 0).Should(BeTrue()) + phase, _, _ = statelessComponent.GetPhaseWhenPodsNotReady(ctx, statelessCompName) + Expect(string(phase)).Should(Equal("")) }) }) diff --git a/controllers/apps/components/stateless/suite_test.go b/controllers/apps/components/stateless/suite_test.go index a68bb40ea..4b6f6179f 100644 --- a/controllers/apps/components/stateless/suite_test.go +++ b/controllers/apps/components/stateless/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package stateless diff --git a/controllers/apps/components/suite_test.go b/controllers/apps/components/suite_test.go index d6adda579..f233f36c1 100644 --- a/controllers/apps/components/suite_test.go +++ b/controllers/apps/components/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package components @@ -101,18 +104,10 @@ var _ = BeforeSuite(func() { }) Expect(err).ToNot(HaveOccurred()) - err = (&StatefulSetReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("stateful-set-controller"), - }).SetupWithManager(k8sManager) + err = NewStatefulSetReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) - err = (&DeploymentReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("deployment-controller"), - }).SetupWithManager(k8sManager) + err = NewDeploymentReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) testCtx = testutil.NewDefaultTestContext(ctx, k8sClient, testEnv) diff --git a/controllers/apps/components/types/component.go b/controllers/apps/components/types/component.go index cfb0e78b8..e14b91f27 100644 --- a/controllers/apps/components/types/component.go +++ b/controllers/apps/components/types/component.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package types @@ -21,14 +24,71 @@ import ( "time" corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +const ( + // ComponentPhaseTransition the event reason indicates that the component transits to a new phase. + ComponentPhaseTransition = "ComponentPhaseTransition" + + // PodContainerFailedTimeout the timeout for container of pod failures, the component phase will be set to Failed/Abnormal after this time. + PodContainerFailedTimeout = time.Minute + + // PodScheduledFailedTimeout timeout for scheduling failure. + PodScheduledFailedTimeout = 30 * time.Second ) -// Component is the interface to use for component status type Component interface { + GetName() string + GetNamespace() string + GetClusterName() string + GetDefinitionName() string + GetWorkloadType() appsv1alpha1.WorkloadType + + GetCluster() *appsv1alpha1.Cluster + GetClusterVersion() *appsv1alpha1.ClusterVersion + GetSynthesizedComponent() *component.SynthesizedComponent + + GetMatchingLabels() client.MatchingLabels + + GetReplicas() int32 + + GetConsensusSpec() *appsv1alpha1.ConsensusSetSpec + GetPrimaryIndex() int32 + + GetPhase() appsv1alpha1.ClusterComponentPhase + // GetStatus() appsv1alpha1.ClusterComponentStatus + + // GetBuiltObjects returns all objects that will be created by this component + GetBuiltObjects(reqCtx intctrlutil.RequestCtx, cli client.Client) ([]client.Object, error) + + Create(reqCtx intctrlutil.RequestCtx, cli client.Client) error + Delete(reqCtx intctrlutil.RequestCtx, cli client.Client) error + Update(reqCtx intctrlutil.RequestCtx, cli client.Client) error + Status(reqCtx intctrlutil.RequestCtx, cli client.Client) error + + Restart(reqCtx intctrlutil.RequestCtx, cli client.Client) error + + ExpandVolume(reqCtx intctrlutil.RequestCtx, cli client.Client) error + + HorizontalScale(reqCtx intctrlutil.RequestCtx, cli client.Client) error + + // TODO(impl): impl-related, replace them with component workload + SetWorkload(obj client.Object, action *ictrltypes.LifecycleAction, parent *ictrltypes.LifecycleVertex) + AddResource(obj client.Object, action *ictrltypes.LifecycleAction, parent *ictrltypes.LifecycleVertex) *ictrltypes.LifecycleVertex +} + +// TODO(impl): replace it with ComponentWorkload and <*>Set implementation. + +type ComponentSet interface { + SetComponent(component Component) + // IsRunning when relevant k8s workloads changes, it checks whether the component is running. // you can also reconcile the pods of component till the component is Running here. IsRunning(ctx context.Context, obj client.Object) (bool, error) @@ -44,24 +104,26 @@ type Component interface { // if the component is ConsensusSet,it will be available when the pod is ready and labeled with its role. PodIsAvailable(pod *corev1.Pod, minReadySeconds int32) bool - // HandleProbeTimeoutWhenPodsReady if the component has no role probe, return false directly. otherwise, - // we should handle the component phase when the role probe timeout and return a bool. - // if return true, means probe is not timing out and need to requeue after an interval time to handle probe timeout again. - // else return false, means probe has timed out and needs to update the component phase to Failed or Abnormal. - HandleProbeTimeoutWhenPodsReady(ctx context.Context, recorder record.EventRecorder) (bool, error) + // GetPhaseWhenPodsReadyAndProbeTimeout when the pods of component are ready but the probe timed-out, + // calculate the component phase is Failed or Abnormal. + GetPhaseWhenPodsReadyAndProbeTimeout(pods []*corev1.Pod) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap) // GetPhaseWhenPodsNotReady when the pods of component are not ready, calculate the component phase is Failed or Abnormal. // if return an empty phase, means the pods of component are ready and skips it. - GetPhaseWhenPodsNotReady(ctx context.Context, componentName string) (appsv1alpha1.ClusterComponentPhase, error) + GetPhaseWhenPodsNotReady(ctx context.Context, componentName string) (appsv1alpha1.ClusterComponentPhase, appsv1alpha1.ComponentMessageMap, error) - // HandleUpdate handles component updating when basic workloads of the components are updated - HandleUpdate(ctx context.Context, obj client.Object) error -} + HandleRestart(ctx context.Context, obj client.Object) ([]graph.Vertex, error) -const ( - // RoleProbeTimeoutReason the event reason when all pods of the component role probe timed out. - RoleProbeTimeoutReason = "RoleProbeTimeout" + HandleRoleChange(ctx context.Context, obj client.Object) ([]graph.Vertex, error) - // PodContainerFailedTimeout the timeout for container of pod failures, the component phase will be set to Failed/Abnormal after this time. - PodContainerFailedTimeout = time.Minute -) + HandleHA(ctx context.Context, obj client.Object) ([]graph.Vertex, error) +} + +// ComponentSetBase is a common component set base struct. +type ComponentSetBase struct { + Cli client.Client + Cluster *appsv1alpha1.Cluster + ComponentSpec *appsv1alpha1.ClusterComponentSpec + ComponentDef *appsv1alpha1.ClusterComponentDefinition + Component Component +} diff --git a/controllers/apps/components/types/component_workload.go b/controllers/apps/components/types/component_workload.go new file mode 100644 index 000000000..cc48507de --- /dev/null +++ b/controllers/apps/components/types/component_workload.go @@ -0,0 +1,22 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package types + +type ComponentWorkload interface{} diff --git a/controllers/apps/components/util/component_utils.go b/controllers/apps/components/util/component_utils.go index 237d490ca..553d3e3fe 100644 --- a/controllers/apps/components/util/component_utils.go +++ b/controllers/apps/components/util/component_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -20,28 +23,30 @@ import ( "context" "errors" "fmt" + "reflect" + "sort" + "strconv" "strings" "time" + "golang.org/x/exp/maps" "golang.org/x/exp/slices" appsv1 "k8s.io/api/apps/v1" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/util/podutils" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/constant" client2 "github.com/apecloud/kubeblocks/internal/controller/client" componentutil "github.com/apecloud/kubeblocks/internal/controller/component" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/generics" ) -const ( - ComponentStatusDefaultPodName = "Unknown" -) - var ( ErrReqCtrlClient = errors.New("required arg client.Client is nil") ErrReqClusterObj = errors.New("required arg *appsv1alpha1.Cluster is nil") @@ -49,21 +54,106 @@ var ( ErrReqClusterComponentSpecObj = errors.New("required arg *appsv1alpha1.ClusterComponentSpec is nil") ) -func ComponentRuntimeReqArgsCheck(cli client.Client, - cluster *appsv1alpha1.Cluster, - component *appsv1alpha1.ClusterComponentSpec) error { - if cli == nil { - return ErrReqCtrlClient +func ListObjWithLabelsInNamespace[T generics.Object, PT generics.PObject[T], L generics.ObjList[T], PL generics.PObjList[T, L]]( + ctx context.Context, cli client.Client, _ func(T, L), namespace string, labels client.MatchingLabels) ([]PT, error) { + var objList L + if err := cli.List(ctx, PL(&objList), labels, client.InNamespace(namespace)); err != nil { + return nil, err } - if cluster == nil { - return ErrReqCtrlClient + + objs := make([]PT, 0) + items := reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T) + for i := range items { + objs = append(objs, &items[i]) + } + return objs, nil +} + +func ListStsOwnedByComponent(ctx context.Context, cli client.Client, namespace string, labels client.MatchingLabels) ([]*appsv1.StatefulSet, error) { + return ListObjWithLabelsInNamespace(ctx, cli, generics.StatefulSetSignature, namespace, labels) +} + +func ListDeployOwnedByComponent(ctx context.Context, cli client.Client, namespace string, labels client.MatchingLabels) ([]*appsv1.Deployment, error) { + return ListObjWithLabelsInNamespace(ctx, cli, generics.DeploymentSignature, namespace, labels) +} + +func ListPodOwnedByComponent(ctx context.Context, cli client.Client, namespace string, labels client.MatchingLabels) ([]*corev1.Pod, error) { + return ListObjWithLabelsInNamespace(ctx, cli, generics.PodSignature, namespace, labels) +} + +func PodIsAvailable(workloadType appsv1alpha1.WorkloadType, pod *corev1.Pod, minReadySeconds int32) bool { + if pod == nil { + return false } - if component == nil { - return ErrReqClusterComponentSpecObj + switch workloadType { + case appsv1alpha1.Consensus, appsv1alpha1.Replication: + return intctrlutil.PodIsReadyWithLabel(*pod) + case appsv1alpha1.Stateful, appsv1alpha1.Stateless: + return podutils.IsPodAvailable(pod, minReadySeconds, metav1.Time{Time: time.Now()}) + default: + panic("unknown workload type") + } +} + +// RestartPod restarts a Pod through updating the pod's annotation +func RestartPod(podTemplate *corev1.PodTemplateSpec) error { + if podTemplate.Annotations == nil { + podTemplate.Annotations = map[string]string{} + } + + startTimestamp := time.Now() // TODO(impl): opsRes.OpsRequest.Status.StartTimestamp + restartTimestamp := podTemplate.Annotations[constant.RestartAnnotationKey] + // if res, _ := time.Parse(time.RFC3339, restartTimestamp); startTimestamp.After(res) { + if res, _ := time.Parse(time.RFC3339, restartTimestamp); startTimestamp.Before(res) { + podTemplate.Annotations[constant.RestartAnnotationKey] = startTimestamp.Format(time.RFC3339) } return nil } +// MergeAnnotations keeps the original annotations. +// if annotations exist and are replaced, the Deployment/StatefulSet will be updated. +func MergeAnnotations(originalAnnotations map[string]string, targetAnnotations *map[string]string) { + if targetAnnotations == nil { + return + } + if *targetAnnotations == nil { + *targetAnnotations = map[string]string{} + } + for k, v := range originalAnnotations { + // if the annotation not exist in targetAnnotations, copy it from original. + if _, ok := (*targetAnnotations)[k]; !ok { + (*targetAnnotations)[k] = v + } + } +} + +// BuildWorkLoadAnnotations builds the annotations for Deployment/StatefulSet +func BuildWorkLoadAnnotations(obj client.Object, cluster *appsv1alpha1.Cluster) { + workloadAnnotations := obj.GetAnnotations() + if workloadAnnotations == nil { + workloadAnnotations = map[string]string{} + } + // record the cluster generation to check if the sts is latest + workloadAnnotations[constant.KubeBlocksGenerationKey] = strconv.FormatInt(cluster.Generation, 10) + obj.SetAnnotations(workloadAnnotations) +} + +// MergeServiceAnnotations keeps the original annotations except prometheus scrape annotations. +// if annotations exist and are replaced, the Service will be updated. +func MergeServiceAnnotations(originalAnnotations, targetAnnotations map[string]string) map[string]string { + if len(originalAnnotations) == 0 { + return targetAnnotations + } + tmpAnnotations := make(map[string]string, len(originalAnnotations)+len(targetAnnotations)) + for k, v := range originalAnnotations { + if !strings.HasPrefix(k, "prometheus.io") { + tmpAnnotations[k] = v + } + } + maps.Copy(tmpAnnotations, targetAnnotations) + return tmpAnnotations +} + // GetClusterByObject gets cluster by related k8s workloads. func GetClusterByObject(ctx context.Context, cli client.Client, @@ -110,12 +200,11 @@ func GetComponentStatusMessageKey(kind, name string) string { } // IsProbeTimeout checks if the application of the pod is probe timed out. -func IsProbeTimeout(componentDef *appsv1alpha1.ClusterComponentDefinition, podsReadyTime *metav1.Time) bool { +func IsProbeTimeout(probes *appsv1alpha1.ClusterDefinitionProbes, podsReadyTime *metav1.Time) bool { if podsReadyTime == nil { return false } - probes := componentDef.Probes - if probes == nil || probes.RoleChangedProbe == nil { + if probes == nil || probes.RoleProbe == nil { return false } roleProbeTimeout := time.Duration(appsv1alpha1.DefaultRoleProbeTimeoutAfterPodsReady) * time.Second @@ -136,20 +225,23 @@ func GetComponentPhase(isFailed, isAbnormal bool) appsv1alpha1.ClusterComponentP } // GetObjectListByComponentName gets k8s workload list with component -func GetObjectListByComponentName(ctx context.Context, cli client2.ReadonlyClient, cluster appsv1alpha1.Cluster, objectList client.ObjectList, componentName string) error { +func GetObjectListByComponentName(ctx context.Context, cli client2.ReadonlyClient, cluster appsv1alpha1.Cluster, + objectList client.ObjectList, componentName string) error { matchLabels := GetComponentMatchLabels(cluster.Name, componentName) inNamespace := client.InNamespace(cluster.Namespace) return cli.List(ctx, objectList, client.MatchingLabels(matchLabels), inNamespace) } // GetObjectListByCustomLabels gets k8s workload list with custom labels -func GetObjectListByCustomLabels(ctx context.Context, cli client.Client, cluster appsv1alpha1.Cluster, objectList client.ObjectList, matchLabels client.ListOption) error { +func GetObjectListByCustomLabels(ctx context.Context, cli client.Client, cluster appsv1alpha1.Cluster, + objectList client.ObjectList, matchLabels client.ListOption) error { inNamespace := client.InNamespace(cluster.Namespace) return cli.List(ctx, objectList, matchLabels, inNamespace) } // GetComponentDefByCluster gets component from ClusterDefinition with compDefName -func GetComponentDefByCluster(ctx context.Context, cli client2.ReadonlyClient, cluster appsv1alpha1.Cluster, compDefName string) (*appsv1alpha1.ClusterComponentDefinition, error) { +func GetComponentDefByCluster(ctx context.Context, cli client2.ReadonlyClient, cluster appsv1alpha1.Cluster, + compDefName string) (*appsv1alpha1.ClusterComponentDefinition, error) { clusterDef := &appsv1alpha1.ClusterDefinition{} if err := cli.Get(ctx, client.ObjectKey{Name: cluster.Spec.ClusterDefRef}, clusterDef); err != nil { return nil, err @@ -176,7 +268,7 @@ func GetClusterComponentSpecByName(cluster appsv1alpha1.Cluster, compSpecName st func InitClusterComponentStatusIfNeed( cluster *appsv1alpha1.Cluster, componentName string, - componentDef appsv1alpha1.ClusterComponentDefinition) error { + workloadType appsv1alpha1.WorkloadType) error { if cluster == nil { return ErrReqClusterObj } @@ -188,14 +280,14 @@ func InitClusterComponentStatusIfNeed( // }) // } componentStatus := cluster.Status.Components[componentName] - switch componentDef.WorkloadType { + switch workloadType { case appsv1alpha1.Consensus: if componentStatus.ConsensusSetStatus != nil { break } componentStatus.ConsensusSetStatus = &appsv1alpha1.ConsensusSetStatus{ Leader: appsv1alpha1.ConsensusMemberStatus{ - Pod: ComponentStatusDefaultPodName, + Pod: constant.ComponentStatusDefaultPodName, AccessMode: appsv1alpha1.None, Name: "", }, @@ -206,7 +298,7 @@ func InitClusterComponentStatusIfNeed( } componentStatus.ReplicationSetStatus = &appsv1alpha1.ReplicationSetStatus{ Primary: appsv1alpha1.ReplicationMemberStatus{ - Pod: ComponentStatusDefaultPodName, + Pod: constant.ComponentStatusDefaultPodName, }, } } @@ -393,8 +485,8 @@ func PatchGVRCustomLabels(ctx context.Context, cli client.Client, cluster *appsv if err := GetObjectListByCustomLabels(ctx, cli, *cluster, objectList, client.MatchingLabels(matchLabels)); err != nil { return err } - labelKey = replaceKBEnvPlaceholderTokens(cluster.Name, componentName, labelKey) - labelValue = replaceKBEnvPlaceholderTokens(cluster.Name, componentName, labelValue) + labelKey = replaceKBEnvPlaceholderTokens(cluster, componentName, labelKey) + labelValue = replaceKBEnvPlaceholderTokens(cluster, componentName, labelValue) switch gvk.Kind { case constant.StatefulSetKind: stsList := objectList.(*appsv1.StatefulSetList) @@ -486,6 +578,22 @@ func GetCustomLabelWorkloadKind() []string { } } +// SortPods sorts pods by their role priority +func SortPods(pods []corev1.Pod, priorityMap map[string]int, idLabelKey string) { + // make a Serial pod list, + // e.g.: unknown -> empty -> learner -> follower1 -> follower2 -> leader, with follower1.Name < follower2.Name + sort.SliceStable(pods, func(i, j int) bool { + roleI := pods[i].Labels[idLabelKey] + roleJ := pods[j].Labels[idLabelKey] + if priorityMap[roleI] == priorityMap[roleJ] { + _, ordinal1 := intctrlutil.GetParentNameAndOrdinal(&pods[i]) + _, ordinal2 := intctrlutil.GetParentNameAndOrdinal(&pods[j]) + return ordinal1 < ordinal2 + } + return priorityMap[roleI] < priorityMap[roleJ] + }) +} + // getObjectListMapOfResourceKind returns the mapping of resource kind and its object list. func getObjectListMapOfResourceKind() map[string]client.ObjectList { return map[string]client.ObjectList{ @@ -500,7 +608,7 @@ func getObjectListMapOfResourceKind() map[string]client.ObjectList { } // replaceKBEnvPlaceholderTokens replaces the placeholder tokens in the string strToReplace with builtInEnvMap and return new string. -func replaceKBEnvPlaceholderTokens(clusterName, componentName, strToReplace string) string { - builtInEnvMap := componentutil.GetReplacementMapForBuiltInEnv(clusterName, componentName) +func replaceKBEnvPlaceholderTokens(cluster *appsv1alpha1.Cluster, componentName, strToReplace string) string { + builtInEnvMap := componentutil.GetReplacementMapForBuiltInEnv(cluster.Name, string(cluster.UID), componentName) return componentutil.ReplaceNamedVars(builtInEnvMap, strToReplace, -1, true) } diff --git a/controllers/apps/components/util/component_utils_test.go b/controllers/apps/components/util/component_utils_test.go index 3b49bd6d7..0ac97b8aa 100644 --- a/controllers/apps/components/util/component_utils_test.go +++ b/controllers/apps/components/util/component_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -46,11 +49,11 @@ func TestIsProbeTimeout(t *testing.T) { podsReadyTime := &metav1.Time{Time: time.Now().Add(-10 * time.Minute)} compDef := &appsv1alpha1.ClusterComponentDefinition{ Probes: &appsv1alpha1.ClusterDefinitionProbes{ - RoleChangedProbe: &appsv1alpha1.ClusterDefinitionProbe{}, + RoleProbe: &appsv1alpha1.ClusterDefinitionProbe{}, RoleProbeTimeoutAfterPodsReady: appsv1alpha1.DefaultRoleProbeTimeoutAfterPodsReady, }, } - if !IsProbeTimeout(compDef, podsReadyTime) { + if !IsProbeTimeout(compDef.Probes, podsReadyTime) { t.Error("probe timed out should be true") } } @@ -159,11 +162,11 @@ var _ = Describe("Consensus Component", func() { Context("Consensus Component test", func() { It("Consensus Component test", func() { By(" init cluster, statefulSet, pods") - _, _, cluster := testapps.InitClusterWithHybridComps(testCtx, clusterDefName, + _, _, cluster := testapps.InitClusterWithHybridComps(&testCtx, clusterDefName, clusterVersionName, clusterName, statelessCompName, "stateful", consensusCompName) - sts := testapps.MockConsensusComponentStatefulSet(testCtx, clusterName, consensusCompName) - testapps.MockStatelessComponentDeploy(testCtx, clusterName, statelessCompName) - _ = testapps.MockConsensusComponentPods(testCtx, sts, clusterName, consensusCompName) + sts := testapps.MockConsensusComponentStatefulSet(&testCtx, clusterName, consensusCompName) + testapps.MockStatelessComponentDeploy(&testCtx, clusterName, statelessCompName) + _ = testapps.MockConsensusComponentPods(&testCtx, sts, clusterName, consensusCompName) By("test GetComponentDefByCluster function") componentDef, _ := GetComponentDefByCluster(ctx, k8sClient, *cluster, consensusCompDefRef) @@ -174,17 +177,17 @@ var _ = Describe("Consensus Component", func() { Expect(newCluster != nil).Should(BeTrue()) By("test consensusSet InitClusterComponentStatusIfNeed function") - err := InitClusterComponentStatusIfNeed(cluster, consensusCompName, *componentDef) + err := InitClusterComponentStatusIfNeed(cluster, consensusCompName, componentDef.WorkloadType) Expect(err).Should(Succeed()) Expect(cluster.Status.Components[consensusCompName].ConsensusSetStatus).ShouldNot(BeNil()) - Expect(cluster.Status.Components[consensusCompName].ConsensusSetStatus.Leader.Pod).Should(Equal(ComponentStatusDefaultPodName)) + Expect(cluster.Status.Components[consensusCompName].ConsensusSetStatus.Leader.Pod).Should(Equal(constant.ComponentStatusDefaultPodName)) By("test ReplicationSet InitClusterComponentStatusIfNeed function") componentDef.WorkloadType = appsv1alpha1.Replication - err = InitClusterComponentStatusIfNeed(cluster, consensusCompName, *componentDef) + err = InitClusterComponentStatusIfNeed(cluster, consensusCompName, componentDef.WorkloadType) Expect(err).Should(Succeed()) Expect(cluster.Status.Components[consensusCompName].ReplicationSetStatus).ShouldNot(BeNil()) - Expect(cluster.Status.Components[consensusCompName].ReplicationSetStatus.Primary.Pod).Should(Equal(ComponentStatusDefaultPodName)) + Expect(cluster.Status.Components[consensusCompName].ReplicationSetStatus.Primary.Pod).Should(Equal(constant.ComponentStatusDefaultPodName)) By("test GetObjectListByComponentName function") stsList := &appsv1.StatefulSetList{} @@ -201,16 +204,6 @@ var _ = Describe("Consensus Component", func() { clusterComp := GetClusterComponentSpecByName(*cluster, consensusCompName) Expect(clusterComp).ShouldNot(BeNil()) - By("test ComponentRuntimeReqArgsCheck function") - err = ComponentRuntimeReqArgsCheck(k8sClient, cluster, clusterComp) - Expect(err).Should(Succeed()) - By("test ComponentRuntimeReqArgsCheck function when cluster nil") - err = ComponentRuntimeReqArgsCheck(k8sClient, nil, clusterComp) - Expect(err).ShouldNot(Succeed()) - By("test ComponentRuntimeReqArgsCheck function when clusterComp nil") - err = ComponentRuntimeReqArgsCheck(k8sClient, cluster, nil) - Expect(err).ShouldNot(Succeed()) - By("test UpdateObjLabel function") stsObj := stsList.Items[0] err = UpdateObjLabel(ctx, k8sClient, stsObj, "test", "test") @@ -326,3 +319,53 @@ var _ = Describe("Consensus Component", func() { }) }) }) + +var _ = Describe("Component utils test", func() { + Context("test mergeServiceAnnotations", func() { + It("original and target annotations are nil", func() { + Expect(MergeServiceAnnotations(nil, nil)).Should(BeNil()) + }) + It("target annotations is nil", func() { + originalAnnotations := map[string]string{"k1": "v1"} + Expect(MergeServiceAnnotations(originalAnnotations, nil)).To(Equal(originalAnnotations)) + }) + It("original annotations is nil", func() { + targetAnnotations := map[string]string{"k1": "v1"} + Expect(MergeServiceAnnotations(nil, targetAnnotations)).To(Equal(targetAnnotations)) + }) + It("original annotations have prometheus annotations which should be removed", func() { + originalAnnotations := map[string]string{"k1": "v1", "prometheus.io/path": "/metrics"} + targetAnnotations := map[string]string{"k2": "v2"} + expectAnnotations := map[string]string{"k1": "v1", "k2": "v2"} + Expect(MergeServiceAnnotations(originalAnnotations, targetAnnotations)).To(Equal(expectAnnotations)) + }) + It("target annotations should override original annotations", func() { + originalAnnotations := map[string]string{"k1": "v1", "prometheus.io/path": "/metrics"} + targetAnnotations := map[string]string{"k1": "v11"} + expectAnnotations := map[string]string{"k1": "v11"} + Expect(MergeServiceAnnotations(originalAnnotations, targetAnnotations)).To(Equal(expectAnnotations)) + }) + + It("should merge annotations from original that not exist in target to final result", func() { + originalKey := "only-existing-in-original" + targetKey := "only-existing-in-target" + updatedKey := "updated-in-target" + originalAnnotations := map[string]string{ + originalKey: "true", + updatedKey: "false", + } + targetAnnotations := map[string]string{ + targetKey: "true", + updatedKey: "true", + } + MergeAnnotations(originalAnnotations, &targetAnnotations) + Expect(targetAnnotations[targetKey]).ShouldNot(BeEmpty()) + Expect(targetAnnotations[originalKey]).ShouldNot(BeEmpty()) + Expect(targetAnnotations[updatedKey]).Should(Equal("true")) + By("merging with target being nil") + var nilAnnotations map[string]string + MergeAnnotations(originalAnnotations, &nilAnnotations) + Expect(nilAnnotations).ShouldNot(BeNil()) + }) + }) +}) diff --git a/controllers/apps/components/util/plan.go b/controllers/apps/components/util/plan.go index e0ce54d59..e186adfeb 100644 --- a/controllers/apps/components/util/plan.go +++ b/controllers/apps/components/util/plan.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -28,7 +31,14 @@ type Step struct { type WalkFunc func(obj interface{}) (bool, error) +// WalkOneStep process plan stepping +// @return isCompleted +// @return err func (p *Plan) WalkOneStep() (bool, error) { + if p == nil { + return true, nil + } + if len(p.Start.NextSteps) == 0 { return true, nil } @@ -43,7 +53,6 @@ func (p *Plan) WalkOneStep() (bool, error) { shouldStop = true } } - if shouldStop { return false, nil } @@ -60,7 +69,6 @@ func (p *Plan) WalkOneStep() (bool, error) { } } } - return plan.WalkOneStep() } @@ -70,6 +78,5 @@ func containStep(steps []*Step, step *Step) bool { return true } } - return false } diff --git a/controllers/apps/components/util/plan_test.go b/controllers/apps/components/util/plan_test.go index a2e76553c..fd9ab1189 100644 --- a/controllers/apps/components/util/plan_test.go +++ b/controllers/apps/components/util/plan_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/controllers/apps/components/util/stateful_set_utils.go b/controllers/apps/components/util/stateful_set_utils.go index 4fa1f4adc..1189c5914 100644 --- a/controllers/apps/components/util/stateful_set_utils.go +++ b/controllers/apps/components/util/stateful_set_utils.go @@ -1,18 +1,20 @@ /* -Copyright ApeCloud, Inc. -Copyright 2016 The Kubernetes Authors. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -49,7 +51,7 @@ func IsMemberOf(set *appsv1.StatefulSet, pod *corev1.Pod) bool { return getParentName(pod) == set.Name } -// IsStsAndPodsRevisionConsistent checks if StatefulSet and pods of the StatefuleSet have the same revison, +// IsStsAndPodsRevisionConsistent checks if StatefulSet and pods of the StatefulSet have the same revision. func IsStsAndPodsRevisionConsistent(ctx context.Context, cli client.Client, sts *appsv1.StatefulSet) (bool, error) { pods, err := GetPodListByStatefulSet(ctx, cli, sts) if err != nil { @@ -70,17 +72,19 @@ func IsStsAndPodsRevisionConsistent(ctx context.Context, cli client.Client, sts return revisionConsistent, nil } -// DeleteStsPods deletes pods of the StatefulSet manually -func DeleteStsPods(ctx context.Context, cli client.Client, sts *appsv1.StatefulSet) error { +// GetPods4Delete gets all pods for delete +func GetPods4Delete(ctx context.Context, cli client.Client, sts *appsv1.StatefulSet) ([]*corev1.Pod, error) { if sts.Spec.UpdateStrategy.Type == appsv1.RollingUpdateStatefulSetStrategyType { - return nil + return nil, nil } pods, err := GetPodListByStatefulSet(ctx, cli, sts) if err != nil { - return err + return nil, nil } - for _, pod := range pods { + + podList := make([]*corev1.Pod, 0) + for i, pod := range pods { // do nothing if the pod is terminating if pod.DeletionTimestamp != nil { continue @@ -89,8 +93,21 @@ func DeleteStsPods(ctx context.Context, cli client.Client, sts *appsv1.StatefulS if intctrlutil.GetPodRevision(&pod) == sts.Status.UpdateRevision { continue } + + podList = append(podList, &pods[i]) + } + return podList, nil +} + +// DeleteStsPods deletes pods of the StatefulSet manually +func DeleteStsPods(ctx context.Context, cli client.Client, sts *appsv1.StatefulSet) error { + pods, err := GetPods4Delete(ctx, cli, sts) + if err != nil { + return err + } + for _, pod := range pods { // delete the pod to trigger associate StatefulSet to re-create it - if err := cli.Delete(ctx, &pod); err != nil && !apierrors.IsNotFound(err) { + if err := cli.Delete(ctx, pod); err != nil && !apierrors.IsNotFound(err) { return err } } @@ -102,7 +119,6 @@ func StatefulSetOfComponentIsReady(sts *appsv1.StatefulSet, statefulStatusRevisi if targetReplicas == nil { targetReplicas = sts.Spec.Replicas } - // judge whether statefulSet is ready return StatefulSetPodsAreReady(sts, *targetReplicas) && statefulStatusRevisionIsEquals } @@ -123,40 +139,19 @@ func ConvertToStatefulSet(obj client.Object) *appsv1.StatefulSet { return nil } -// Len is the implementation of the sort.Interface, calculate the length of the list of DescendingOrdinalSts. -func (dos DescendingOrdinalSts) Len() int { - return len(dos) -} - -// Swap is the implementation of the sort.Interface, exchange two items in DescendingOrdinalSts. -func (dos DescendingOrdinalSts) Swap(i, j int) { - dos[i], dos[j] = dos[j], dos[i] -} - -// Less is the implementation of the sort.Interface, sort the size of the statefulSet ordinal in descending order. -func (dos DescendingOrdinalSts) Less(i, j int) bool { - return GetOrdinalSts(dos[i]) > GetOrdinalSts(dos[j]) -} - -// GetOrdinalSts gets StatefulSet's ordinal. If StatefulSet has no ordinal, -1 is returned. -func GetOrdinalSts(sts *appsv1.StatefulSet) int { - _, ordinal := getParentNameAndOrdinalSts(sts) - return ordinal -} - -// getParentNameAndOrdinalSts gets the name of cluster-component and StatefulSet's ordinal as extracted from its Name. If +// ParseParentNameAndOrdinal gets the name of cluster-component and StatefulSet's ordinal as extracted from its Name. If // the StatefulSet's Name was not match a statefulSetRegex, its parent is considered to be empty string, // and its ordinal is considered to be -1. -func getParentNameAndOrdinalSts(sts *appsv1.StatefulSet) (string, int) { +func ParseParentNameAndOrdinal(s string) (string, int32) { parent := "" - ordinal := -1 - subMatches := statefulSetRegex.FindStringSubmatch(sts.Name) + ordinal := int32(-1) + subMatches := statefulSetRegex.FindStringSubmatch(s) if len(subMatches) < 3 { return parent, ordinal } parent = subMatches[1] if i, err := strconv.ParseInt(subMatches[2], 10, 32); err == nil { - ordinal = int(i) + ordinal = int32(i) } return parent, ordinal } @@ -181,6 +176,25 @@ func GetPodListByStatefulSet(ctx context.Context, cli client.Client, stsObj *app return pods, nil } +// GetPodOwnerReferencesSts gets the owner reference statefulSet of the pod. +func GetPodOwnerReferencesSts(ctx context.Context, cli client.Client, podObj *corev1.Pod) (*appsv1.StatefulSet, error) { + stsList := &appsv1.StatefulSetList{} + if err := cli.List(ctx, stsList, + &client.ListOptions{Namespace: podObj.Namespace}, + client.MatchingLabels{ + constant.KBAppComponentLabelKey: podObj.Labels[constant.KBAppComponentLabelKey], + constant.AppInstanceLabelKey: podObj.Labels[constant.AppInstanceLabelKey], + }); err != nil { + return nil, err + } + for _, sts := range stsList.Items { + if IsMemberOf(&sts, podObj) { + return &sts, nil + } + } + return nil, nil +} + // MarkPrimaryStsToReconcile marks the primary statefulSet annotation to be reconciled. func MarkPrimaryStsToReconcile(ctx context.Context, cli client.Client, sts *appsv1.StatefulSet) error { patch := client.MergeFrom(sts.DeepCopy()) diff --git a/controllers/apps/components/util/stateful_set_utils_test.go b/controllers/apps/components/util/stateful_set_utils_test.go index 3977222b0..c5f5c39cf 100644 --- a/controllers/apps/components/util/stateful_set_utils_test.go +++ b/controllers/apps/components/util/stateful_set_utils_test.go @@ -1,18 +1,20 @@ /* -Copyright ApeCloud, Inc. -Copyright 2016 The Kubernetes Authors. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -130,7 +132,7 @@ var _ = Describe("StatefulSet utils test", func() { Create(&testCtx).GetObject() By("Creating pods by the StatefulSet") - testapps.MockReplicationComponentPods(testCtx, sts, clusterName, testapps.DefaultRedisCompName, role) + testapps.MockReplicationComponentPods(nil, testCtx, sts, clusterName, testapps.DefaultRedisCompName, nil) Expect(IsStsAndPodsRevisionConsistent(testCtx.Ctx, k8sClient, sts)).Should(BeTrue()) By("Updating the StatefulSet's UpdateRevision") @@ -140,6 +142,11 @@ var _ = Describe("StatefulSet utils test", func() { Expect(err).To(Succeed()) Expect(len(podList)).To(Equal(1)) + By("Testing get the StatefulSet of the pod") + ownerSts, err := GetPodOwnerReferencesSts(ctx, k8sClient, &podList[0]) + Expect(err).To(Succeed()) + Expect(ownerSts).ShouldNot(BeNil()) + By("Deleting the pods of StatefulSet") Expect(DeleteStsPods(testCtx.Ctx, k8sClient, sts)).Should(Succeed()) podList, err = GetPodListByStatefulSet(ctx, k8sClient, sts) @@ -147,7 +154,7 @@ var _ = Describe("StatefulSet utils test", func() { Expect(len(podList)).To(Equal(0)) By("Creating new pods by StatefulSet with new UpdateRevision") - testapps.MockReplicationComponentPods(testCtx, sts, clusterName, testapps.DefaultRedisCompName, role) + testapps.MockReplicationComponentPods(nil, testCtx, sts, clusterName, testapps.DefaultRedisCompName, nil) Expect(IsStsAndPodsRevisionConsistent(testCtx.Ctx, k8sClient, sts)).Should(BeTrue()) }) }) diff --git a/controllers/apps/components/util/suite_test.go b/controllers/apps/components/util/suite_test.go index e31ef3f96..08de72226 100644 --- a/controllers/apps/components/util/suite_test.go +++ b/controllers/apps/components/util/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/controllers/apps/configuration/config_annotation.go b/controllers/apps/configuration/config_annotation.go index e56636c58..a93d2a44d 100644 --- a/controllers/apps/configuration/config_annotation.go +++ b/controllers/apps/configuration/config_annotation.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -25,12 +28,13 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) func checkEnableCfgUpgrade(object client.Object) bool { - // check user disable upgrade + // check user's upgrade switch // config.kubeblocks.io/disable-reconfigure = "false" annotations := object.GetAnnotations() value, ok := annotations[constant.DisableUpgradeInsConfigurationAnnotationKey] @@ -69,7 +73,7 @@ func checkAndApplyConfigsChanged(client client.Client, ctx intctrlutil.RequestCt return false, err } - lastConfig, ok := annotations[constant.LastAppliedConfigAnnotation] + lastConfig, ok := annotations[constant.LastAppliedConfigAnnotationKey] if !ok { return updateAppliedConfigs(client, ctx, cm, configData, cfgcore.ReconfigureCreatedPhase) } @@ -77,7 +81,7 @@ func checkAndApplyConfigsChanged(client client.Client, ctx intctrlutil.RequestCt return lastConfig == string(configData), nil } -// updateAppliedConfigs update hash label and last applied config +// updateAppliedConfigs updates hash label and last applied config func updateAppliedConfigs(cli client.Client, ctx intctrlutil.RequestCtx, config *corev1.ConfigMap, configData []byte, reconfigurePhase string) (bool, error) { patch := client.MergeFrom(config.DeepCopy()) @@ -85,8 +89,8 @@ func updateAppliedConfigs(cli client.Client, ctx intctrlutil.RequestCtx, config config.ObjectMeta.Annotations = map[string]string{} } - config.ObjectMeta.Annotations[constant.LastAppliedConfigAnnotation] = string(configData) - hash, err := cfgcore.ComputeHash(config.Data) + config.ObjectMeta.Annotations[constant.LastAppliedConfigAnnotationKey] = string(configData) + hash, err := util.ComputeHash(config.Data) if err != nil { return false, err } @@ -103,7 +107,6 @@ func updateAppliedConfigs(cli client.Client, ctx intctrlutil.RequestCtx, config // delete reconfigure-policy delete(config.ObjectMeta.Annotations, constant.UpgradePolicyAnnotationKey) - delete(config.ObjectMeta.Annotations, constant.KBParameterUpdateSourceAnnotationKey) if err := cli.Patch(ctx.Ctx, config, patch); err != nil { return false, err } @@ -113,7 +116,7 @@ func updateAppliedConfigs(cli client.Client, ctx intctrlutil.RequestCtx, config func getLastVersionConfig(cm *corev1.ConfigMap) (map[string]string, error) { data := make(map[string]string, 0) - cfgContent, ok := cm.GetAnnotations()[constant.LastAppliedConfigAnnotation] + cfgContent, ok := cm.GetAnnotations()[constant.LastAppliedConfigAnnotationKey] if !ok { return data, nil } diff --git a/controllers/apps/configuration/config_util.go b/controllers/apps/configuration/config_util.go index 1f7bd6ec3..4d862dfe8 100644 --- a/controllers/apps/configuration/config_util.go +++ b/controllers/apps/configuration/config_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -316,7 +319,7 @@ func updateLabelsByConfigSpec[T generics.Object, PT generics.PObject[T]](cli cli } func validateConfigTemplate(cli client.Client, ctx intctrlutil.RequestCtx, configSpecs []appsv1alpha1.ComponentConfigSpec) (bool, error) { - // check ConfigTemplate Validate + // validate ConfigTemplate foundAndCheckConfigSpec := func(configSpec appsv1alpha1.ComponentConfigSpec, logger logr.Logger) (*appsv1alpha1.ConfigConstraint, error) { if _, err := getConfigMapByTemplateName(cli, ctx, configSpec.TemplateRef, configSpec.Namespace); err != nil { logger.Error(err, "failed to get config template cm object!") @@ -361,13 +364,20 @@ func validateConfigTemplate(cli client.Client, ctx intctrlutil.RequestCtx, confi } func validateConfigConstraintStatus(ccStatus appsv1alpha1.ConfigConstraintStatus) bool { - return ccStatus.Phase == appsv1alpha1.AvailablePhase + return ccStatus.Phase == appsv1alpha1.CCAvailablePhase } func usingComponentConfigSpec(annotations map[string]string, key, value string) bool { return len(annotations) != 0 && annotations[key] == value } +func updateConfigConstraintStatus(cli client.Client, ctx intctrlutil.RequestCtx, configConstraint *appsv1alpha1.ConfigConstraint, phase appsv1alpha1.ConfigConstraintPhase) error { + patch := client.MergeFrom(configConstraint.DeepCopy()) + configConstraint.Status.Phase = phase + configConstraint.Status.ObservedGeneration = configConstraint.Generation + return cli.Status().Patch(ctx.Ctx, configConstraint, patch) +} + func getAssociatedComponentsByConfigmap(stsList *appv1.StatefulSetList, cfg client.ObjectKey, configSpecName string) ([]appv1.StatefulSet, []string) { managerContainerName := constant.ConfigSidecarName stsLen := len(stsList.Items) @@ -414,7 +424,7 @@ func updateConfigSchema(cc *appsv1alpha1.ConfigConstraint, cli client.Client, ct return nil } - // Because the conversion of cue to openAPISchema is constraint, and the definition of some cue may not be converted into openAPISchema, and won't return error. + // Because the conversion of cue to openAPISchema is restricted, and the definition of some cue may not be converted into openAPISchema, and won't return error. openAPISchema, err := cfgcore.GenerateOpenAPISchema(schema.CUE, cc.Spec.CfgSchemaTopLevelName) if err != nil { return err @@ -428,31 +438,6 @@ func updateConfigSchema(cc *appsv1alpha1.ConfigConstraint, cli client.Client, ct return cli.Patch(ctx, cc, ccPatch) } -func NeedReloadVolume(config appsv1alpha1.ComponentConfigSpec) bool { - // TODO distinguish between scripts and configuration - return config.ConfigConstraintRef != "" -} - -func GetReloadOptions(cli client.Client, ctx context.Context, configSpecs []appsv1alpha1.ComponentConfigSpec) (*appsv1alpha1.ReloadOptions, *appsv1alpha1.FormatterConfig, error) { - for _, configSpec := range configSpecs { - if !NeedReloadVolume(configSpec) { - continue - } - ccKey := client.ObjectKey{ - Namespace: "", - Name: configSpec.ConfigConstraintRef, - } - cfgConst := &appsv1alpha1.ConfigConstraint{} - if err := cli.Get(ctx, ccKey, cfgConst); err != nil { - return nil, nil, cfgcore.WrapError(err, "failed to get ConfigConstraint, key[%v]", ccKey) - } - if cfgConst.Spec.ReloadOptions != nil { - return cfgConst.Spec.ReloadOptions, cfgConst.Spec.FormatterConfig, nil - } - } - return nil, nil, nil -} - func getComponentFromClusterDefinition( ctx context.Context, cli client.Client, diff --git a/controllers/apps/configuration/config_util_test.go b/controllers/apps/configuration/config_util_test.go index 33868a45f..63a20440a 100644 --- a/controllers/apps/configuration/config_util_test.go +++ b/controllers/apps/configuration/config_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -38,11 +41,8 @@ import ( var _ = Describe("ConfigWrapper util test", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" - - const statefulCompType = "replicasets" - + const statefulCompDefName = "replicasets" const configSpecName = "mysql-config-tpl" - const configVolumeName = "mysql-config" var ( @@ -64,7 +64,7 @@ var _ = Describe("ConfigWrapper util test", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -98,13 +98,13 @@ var _ = Describe("ConfigWrapper util test", func() { By("Create a clusterDefinition obj") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). AddConfigTemplate(configSpecName, configMapObj.Name, configConstraintObj.Name, testCtx.DefaultNamespace, configVolumeName). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType). + AddComponentVersion(statefulCompDefName). Create(&testCtx).GetObject() }) @@ -118,7 +118,7 @@ var _ = Describe("ConfigWrapper util test", func() { Context("clusterdefinition CR test", func() { It("Should success without error", func() { availableTPL := configConstraintObj.DeepCopy() - availableTPL.Status.Phase = appsv1alpha1.AvailablePhase + availableTPL.Status.Phase = appsv1alpha1.CCAvailablePhase k8sMockClient.MockPatchMethod(testutil.WithSucceed()) k8sMockClient.MockListMethod(testutil.WithSucceed()) @@ -190,7 +190,7 @@ var _ = Describe("ConfigWrapper util test", func() { Expect(err).Should(Succeed()) availableTPL := configConstraintObj.DeepCopy() - availableTPL.Status.Phase = appsv1alpha1.AvailablePhase + availableTPL.Status.Phase = appsv1alpha1.CCAvailablePhase k8sMockClient.MockGetMethod(testutil.WithGetReturned(testutil.WithConstructSequenceResult( map[client.ObjectKey][]testutil.MockGetReturned{ @@ -233,7 +233,7 @@ var _ = Describe("ConfigWrapper util test", func() { It("Should success without error", func() { updateAVTemplates() availableTPL := configConstraintObj.DeepCopy() - availableTPL.Status.Phase = appsv1alpha1.AvailablePhase + availableTPL.Status.Phase = appsv1alpha1.CCAvailablePhase k8sMockClient.MockPatchMethod(testutil.WithSucceed()) k8sMockClient.MockListMethod(testutil.WithSucceed()) @@ -363,7 +363,7 @@ var _ = Describe("ConfigWrapper util test", func() { }, testutil.WithMaxTimes(len(tests)))) for _, tt := range tests { - got, _, err := GetReloadOptions(k8sMockClient.Client(), ctx, tt.tpls) + got, _, err := cfgcore.GetReloadOptions(k8sMockClient.Client(), ctx, tt.tpls) Expect(err != nil).Should(BeEquivalentTo(tt.wantErr)) Expect(reflect.DeepEqual(got, tt.want)).Should(BeTrue()) } diff --git a/controllers/apps/configuration/configconstraint_controller.go b/controllers/apps/configuration/configconstraint_controller.go index 9a66e041a..d53f2b4e6 100644 --- a/controllers/apps/configuration/configconstraint_controller.go +++ b/controllers/apps/configuration/configconstraint_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -69,7 +72,15 @@ func (r *ConfigConstraintReconciler) Reconcile(ctx context.Context, req ctrl.Req res, err := intctrlutil.HandleCRDeletion(reqCtx, r, configConstraint, constant.ConfigurationTemplateFinalizerName, func() (*ctrl.Result, error) { recordEvent := func() { r.Recorder.Event(configConstraint, corev1.EventTypeWarning, "ExistsReferencedResources", - "cannot be deleted because of existing referencing ClusterDefinition or ClusterVersion.") + "cannot be deleted because of existing referencing of ClusterDefinition or ClusterVersion.") + } + if configConstraint.Status.Phase != appsv1alpha1.CCDeletingPhase { + err := updateConfigConstraintStatus(r.Client, reqCtx, configConstraint, appsv1alpha1.CCDeletingPhase) + // if fail to update ConfigConstraint status, return error, + // so that it can be retried + if err != nil { + return nil, err + } } if res, err := intctrlutil.ValidateReferenceCR(reqCtx, r.Client, configConstraint, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(configConstraint.GetName()), @@ -83,7 +94,7 @@ func (r *ConfigConstraintReconciler) Reconcile(ctx context.Context, req ctrl.Req return *res, err } - if configConstraint.Status.ObservedGeneration == configConstraint.Generation { + if configConstraint.Status.ObservedGeneration == configConstraint.Generation && configConstraint.Status.IsConfigConstraintTerminalPhases() { return intctrlutil.Reconciled() } @@ -96,14 +107,11 @@ func (r *ConfigConstraintReconciler) Reconcile(ctx context.Context, req ctrl.Req return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "failed to generate openAPISchema") } - statusPatch := client.MergeFrom(configConstraint.DeepCopy()) - configConstraint.Status.ObservedGeneration = configConstraint.Generation - configConstraint.Status.Phase = appsv1alpha1.AvailablePhase - if err = r.Client.Status().Patch(reqCtx.Ctx, configConstraint, statusPatch); err != nil { + err = updateConfigConstraintStatus(r.Client, reqCtx, configConstraint, appsv1alpha1.CCAvailablePhase) + if err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") } intctrlutil.RecordCreatedEvent(r.Recorder, configConstraint) - return ctrl.Result{}, nil } diff --git a/controllers/apps/configuration/configconstraint_controller_test.go b/controllers/apps/configuration/configconstraint_controller_test.go index 0e087b79f..c909e1f39 100644 --- a/controllers/apps/configuration/configconstraint_controller_test.go +++ b/controllers/apps/configuration/configconstraint_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -36,15 +39,12 @@ import ( var _ = Describe("ConfigConstraint Controller", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" - - const statefulCompType = "replicasets" - + const statefulCompDefName = "replicasets" const configSpecName = "mysql-config-tpl" - const configVolumeName = "mysql-config" cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -81,7 +81,7 @@ var _ = Describe("ConfigConstraint Controller", func() { By("Create a clusterDefinition obj") clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). AddConfigTemplate(configSpecName, configmap.Name, constraint.Name, testCtx.DefaultNamespace, configVolumeName). AddLabels(cfgcore.GenerateTPLUniqLabelKeyWithConfig(configSpecName), configmap.Name, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(constraint.Name), constraint.Name). @@ -89,7 +89,7 @@ var _ = Describe("ConfigConstraint Controller", func() { By("Create a clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType). + AddComponentVersion(statefulCompDefName). AddLabels(cfgcore.GenerateTPLUniqLabelKeyWithConfig(configSpecName), configmap.Name, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(constraint.Name), constraint.Name). Create(&testCtx).GetObject() @@ -108,6 +108,12 @@ var _ = Describe("ConfigConstraint Controller", func() { log.Log.Info("expect that ConfigConstraint is not deleted.") Consistently(testapps.CheckObjExists(&testCtx, constraintKey, &appsv1alpha1.ConfigConstraint{}, true)).Should(Succeed()) + By("check ConfigConstraint status should be deleting") + Eventually(testapps.CheckObj(&testCtx, constraintKey, + func(g Gomega, tpl *appsv1alpha1.ConfigConstraint) { + g.Expect(tpl.Status.Phase).To(BeEquivalentTo(appsv1alpha1.CCDeletingPhase)) + })).Should(Succeed()) + By("By delete referencing clusterdefinition and clusterversion") Expect(k8sClient.Delete(testCtx.Ctx, clusterVersionObj)).Should(Succeed()) Expect(k8sClient.Delete(testCtx.Ctx, clusterDefObj)).Should(Succeed()) diff --git a/controllers/apps/configuration/parallel_upgrade_policy.go b/controllers/apps/configuration/parallel_upgrade_policy.go index c32f39bec..bf01a9325 100644 --- a/controllers/apps/configuration/parallel_upgrade_policy.go +++ b/controllers/apps/configuration/parallel_upgrade_policy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -36,7 +39,7 @@ func (p *parallelUpgradePolicy) Upgrade(params reconfigureParams) (ReturnedStatu switch params.WorkloadType() { default: - return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not support component workload type[%s]", params.WorkloadType()) + return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not supported component workload type[%s]", params.WorkloadType()) case appsv1alpha1.Consensus: funcs = GetConsensusRollingUpgradeFuncs() case appsv1alpha1.Stateful: diff --git a/controllers/apps/configuration/parallel_upgrade_policy_test.go b/controllers/apps/configuration/parallel_upgrade_policy_test.go index beb490a55..14e5ca80a 100644 --- a/controllers/apps/configuration/parallel_upgrade_policy_test.go +++ b/controllers/apps/configuration/parallel_upgrade_policy_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -192,7 +195,7 @@ var _ = Describe("Reconfigure ParallelPolicy", func() { }) }) - Context("parallel reconfigure policy test without not support component", func() { + Context("parallel reconfigure policy test for not supported component", func() { It("Should failed", func() { // not support type mockParam := newMockReconfigureParams("parallelPolicy", nil, @@ -207,7 +210,7 @@ var _ = Describe("Reconfigure ParallelPolicy", func() { }}})) status, err := parallelPolicy.Upgrade(mockParam) Expect(err).ShouldNot(Succeed()) - Expect(err.Error()).Should(ContainSubstring("not support component workload type")) + Expect(err.Error()).Should(ContainSubstring("not supported component workload type")) Expect(status.Status).Should(BeEquivalentTo(ESNotSupport)) }) }) diff --git a/controllers/apps/configuration/policy_util.go b/controllers/apps/configuration/policy_util.go index 4a1f9f0e5..ebaace75c 100644 --- a/controllers/apps/configuration/policy_util.go +++ b/controllers/apps/configuration/policy_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -26,7 +29,7 @@ import ( "github.com/spf13/viper" corev1 "k8s.io/api/core/v1" - "github.com/apecloud/kubeblocks/controllers/apps/components/consensusset" + "github.com/apecloud/kubeblocks/controllers/apps/components/consensus" "github.com/apecloud/kubeblocks/controllers/apps/components/util" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" cfgproto "github.com/apecloud/kubeblocks/internal/configuration/proto" @@ -80,7 +83,7 @@ func GetDeploymentRollingUpgradeFuncs() RollingUpgradeFuncs { } func getDeploymentRollingPods(params reconfigureParams) ([]corev1.Pod, error) { - // util.GetComponentPodList support deployment + // util.GetComponentPodList supports deployment return getReplicationSetPods(params) } @@ -94,7 +97,7 @@ func getReplicationSetPods(params reconfigureParams) ([]corev1.Pod, error) { return podList.Items, nil } -// GetComponentPods get all pods of the component. +// GetComponentPods gets all pods of the component. func GetComponentPods(params reconfigureParams) ([]corev1.Pod, error) { componentPods := make([]corev1.Pod, 0) for i := range params.ComponentUnits { @@ -125,7 +128,7 @@ func CheckReconfigureUpdateProgress(pods []corev1.Pod, configKey, version string func getStatefulSetPods(params reconfigureParams) ([]corev1.Pod, error) { if len(params.ComponentUnits) != 1 { - return nil, cfgcore.MakeError("statefulSet component require only one statefulset, actual %d component", len(params.ComponentUnits)) + return nil, cfgcore.MakeError("statefulSet component require only one statefulset, actual %d components", len(params.ComponentUnits)) } stsObj := ¶ms.ComponentUnits[0] @@ -144,7 +147,7 @@ func getStatefulSetPods(params reconfigureParams) ([]corev1.Pod, error) { func getConsensusPods(params reconfigureParams) ([]corev1.Pod, error) { if len(params.ComponentUnits) > 1 { - return nil, cfgcore.MakeError("consensus component require only one statefulset, actual %d component", len(params.ComponentUnits)) + return nil, cfgcore.MakeError("consensus component require only one statefulset, actual %d components", len(params.ComponentUnits)) } if len(params.ComponentUnits) == 0 { @@ -157,8 +160,8 @@ func getConsensusPods(params reconfigureParams) ([]corev1.Pod, error) { return nil, err } - // sort pods - consensusset.SortPods(pods, consensusset.ComposeRolePriorityMap(*params.Component)) + // TODO: should resolve the dependency on consensus module + util.SortPods(pods, consensus.ComposeRolePriorityMap(params.Component.ConsensusSpec), constant.RoleLabelKey) r := make([]corev1.Pod, 0, len(pods)) for i := len(pods); i > 0; i-- { r = append(r, pods[i-1:i]...) diff --git a/controllers/apps/configuration/policy_util_test.go b/controllers/apps/configuration/policy_util_test.go index ca5dc03da..b8ce5e5d6 100644 --- a/controllers/apps/configuration/policy_util_test.go +++ b/controllers/apps/configuration/policy_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/controllers/apps/configuration/reconfigure_policy.go b/controllers/apps/configuration/reconfigure_policy.go index a0d42bff7..cc504d8b0 100644 --- a/controllers/apps/configuration/reconfigure_policy.go +++ b/controllers/apps/configuration/reconfigure_policy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -29,6 +32,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" cfgproto "github.com/apecloud/kubeblocks/internal/configuration/proto" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) @@ -60,14 +64,14 @@ type reconfigurePolicy interface { // Upgrade is to enable the configuration to take effect. Upgrade(params reconfigureParams) (ReturnedStatus, error) - // GetPolicyName return name of policy. + // GetPolicyName returns name of policy. GetPolicyName() string } type AutoReloadPolicy struct{} type reconfigureParams struct { - // Only support restart pod or container. + // Only supports restart pod or container. Restart bool // Name is a config template name. @@ -85,7 +89,7 @@ type reconfigureParams struct { // For grpc factory ReconfigureClientFactory createReconfigureClient - // List of container, using this config volume. + // List of containers using this config volume. ContainerNames []string Client client.Client @@ -98,12 +102,12 @@ type reconfigureParams struct { // Associated component for clusterdefinition. Component *appsv1alpha1.ClusterComponentDefinition - // List of StatefulSet, using this config template. + // List of StatefulSets using this config template. ComponentUnits []appv1.StatefulSet } var ( - // lazy create grpc connection + // lazy creation of grpc connection // TODO support connection pool newGRPCClient = func(addr string) (cfgproto.ReconfigureClient, error) { conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials())) @@ -134,9 +138,9 @@ func (param *reconfigureParams) getConfigKey() string { } func (param *reconfigureParams) getTargetVersionHash() string { - hash, err := cfgcore.ComputeHash(param.ConfigMap.Data) + hash, err := util.ComputeHash(param.ConfigMap.Data) if err != nil { - param.Ctx.Log.Error(err, "failed to cal configuration version!") + param.Ctx.Log.Error(err, "failed to get configuration version!") return "" } @@ -150,22 +154,22 @@ func (param *reconfigureParams) maxRollingReplicas() int32 { replicas = param.getTargetReplicas() ) - if param.Component.MaxUnavailable == nil { + if param.Component.GetMaxUnavailable() == nil { return defaultRolling } - v, isPercent, err := intctrlutil.GetIntOrPercentValue(param.Component.MaxUnavailable) + v, isPercentage, err := intctrlutil.GetIntOrPercentValue(param.Component.GetMaxUnavailable()) if err != nil { - param.Ctx.Log.Error(err, "failed to get MaxUnavailable!") + param.Ctx.Log.Error(err, "failed to get maxUnavailable!") return defaultRolling } - if isPercent { + if isPercentage { r = int32(math.Floor(float64(v) * float64(replicas) / 100)) } else { - r = int32(cfgcore.Min(v, param.getTargetReplicas())) + r = int32(util.Min(v, param.getTargetReplicas())) } - return cfgcore.Max(r, defaultRolling) + return util.Max(r, defaultRolling) } func (param *reconfigureParams) getTargetReplicas() int { @@ -174,7 +178,7 @@ func (param *reconfigureParams) getTargetReplicas() int { func (param *reconfigureParams) podMinReadySeconds() int32 { minReadySeconds := param.ComponentUnits[0].Spec.MinReadySeconds - return cfgcore.Max(minReadySeconds, viper.GetInt32(constant.PodMinReadySecondsEnv)) + return util.Max(minReadySeconds, viper.GetInt32(constant.PodMinReadySecondsEnv)) } func RegisterPolicy(policy appsv1alpha1.UpgradePolicy, action reconfigurePolicy) { @@ -212,7 +216,7 @@ func NewReconfigurePolicy(cc *appsv1alpha1.ConfigConstraintSpec, cfgPatch *cfgco if action, ok := upgradePolicyMap[policy]; ok { return action, nil } - return nil, cfgcore.MakeError("not support upgrade policy:[%s]", policy) + return nil, cfgcore.MakeError("not supported upgrade policy:[%s]", policy) } func enableAutoDecision(restart bool, policy appsv1alpha1.UpgradePolicy) bool { diff --git a/controllers/apps/configuration/reconfigurerequest_controller.go b/controllers/apps/configuration/reconfigurerequest_controller.go index 75740c037..c96c37967 100644 --- a/controllers/apps/configuration/reconfigurerequest_controller.go +++ b/controllers/apps/configuration/reconfigurerequest_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -82,7 +85,7 @@ func (r *ReconfigureRequestReconciler) Reconcile(ctx context.Context, req ctrl.R config := &corev1.ConfigMap{} if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, config); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "not find configmap") + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "cannot find configmap") } if !checkConfigurationObject(config) { @@ -104,7 +107,7 @@ func (r *ReconfigureRequestReconciler) Reconcile(ctx context.Context, req ctrl.R } if cfgConstraintsName, ok := config.Labels[constant.CMConfigurationConstraintsNameLabelKey]; !ok || len(cfgConstraintsName) == 0 { - reqCtx.Log.V(1).Info("configuration not set ConfigConstraints, not support reconfigure.") + reqCtx.Log.V(1).Info("configuration without ConfigConstraints, does not support reconfigure.") return intctrlutil.Reconciled() } @@ -163,7 +166,7 @@ func (r *ReconfigureRequestReconciler) sync(reqCtx intctrlutil.RequestCtx, confi return intctrlutil.RequeueWithErrorAndRecordEvent(config, r.Recorder, err, reqCtx.Log) } - // Not any parameters updated + // No parameters updated if !configPatch.IsModify { return r.updateConfigCMStatus(reqCtx, config, cfgcore.ReconfigureNoChangeType) } @@ -294,7 +297,7 @@ func (r *ReconfigureRequestReconciler) handleConfigEvent(params reconfigureParam ) if len(cm.Annotations) != 0 { - lastOpsRequest = cm.Annotations[constant.LastAppliedOpsCRAnnotation] + lastOpsRequest = cm.Annotations[constant.LastAppliedOpsCRAnnotationKey] } eventContext := cfgcore.ConfigEventContext{ diff --git a/controllers/apps/configuration/reconfigurerequest_controller_test.go b/controllers/apps/configuration/reconfigurerequest_controller_test.go index e48bb3f8d..f67b0f4e6 100644 --- a/controllers/apps/configuration/reconfigurerequest_controller_test.go +++ b/controllers/apps/configuration/reconfigurerequest_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -36,22 +39,17 @@ var _ = Describe("Reconfigure Controller", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" const clusterName = "test-cluster" - - const statefulCompType = "replicasets" + const statefulCompDefName = "replicasets" const statefulCompName = "mysql" - const statefulSetName = "mysql-statefulset" - const configSpecName = "mysql-config-tpl" - const configVolumeName = "mysql-config" - const cmName = "mysql-tree-node-template-8.0" var ctx = context.Background() cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -88,7 +86,10 @@ var _ = Describe("Reconfigure Controller", func() { constant.CMConfigurationConstraintsNameLabelKey, cmName, constant.CMConfigurationSpecProviderLabelKey, configSpecName, constant.CMConfigurationTypeLabelKey, constant.ConfigInstanceType, - )) + ), + testapps.WithAnnotations(constant.KBParameterUpdateSourceAnnotationKey, + constant.ReconfigureManagerSource, + constant.CMInsEnableRerenderTemplateKey, "true")) constraint := testapps.CreateCustomizedObj(&testCtx, "resources/mysql-config-constraint.yaml", @@ -96,7 +97,7 @@ var _ = Describe("Reconfigure Controller", func() { By("Create a clusterDefinition obj") clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). AddConfigTemplate(configSpecName, configmap.Name, constraint.Name, testCtx.DefaultNamespace, configVolumeName). AddLabels(cfgcore.GenerateTPLUniqLabelKeyWithConfig(configSpecName), configmap.Name, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(constraint.Name), constraint.Name). @@ -104,7 +105,7 @@ var _ = Describe("Reconfigure Controller", func() { By("Create a clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType). + AddComponentVersion(statefulCompDefName). AddLabels(cfgcore.GenerateTPLUniqLabelKeyWithConfig(configSpecName), configmap.Name, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(constraint.Name), constraint.Name). Create(&testCtx).GetObject() @@ -112,7 +113,7 @@ var _ = Describe("Reconfigure Controller", func() { By("Creating a cluster") clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(statefulCompName, statefulCompType).Create(&testCtx).GetObject() + AddComponent(statefulCompName, statefulCompDefName).Create(&testCtx).GetObject() container := corev1.Container{ Name: "mock-container", diff --git a/controllers/apps/configuration/rolling_upgrade_policy.go b/controllers/apps/configuration/rolling_upgrade_policy.go index 71341d026..34f9aa8e7 100644 --- a/controllers/apps/configuration/rolling_upgrade_policy.go +++ b/controllers/apps/configuration/rolling_upgrade_policy.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration import ( "context" + "fmt" "os" "github.com/spf13/viper" @@ -26,6 +30,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/constant" podutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) @@ -59,7 +64,7 @@ func (r *rollingUpgradePolicy) Upgrade(params reconfigureParams) (ReturnedStatus case appsv1alpha1.Stateful: funcs = GetStatefulSetRollingUpgradeFuncs() default: - return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not support component workload type[%s]", cType) + return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not supported component workload type[%s]", cType) } return performRollingUpgrade(params, funcs) } @@ -74,11 +79,11 @@ func canPerformUpgrade(pods []corev1.Pod, params reconfigureParams) bool { return true } if params.WorkloadType() == appsv1alpha1.Consensus { - params.Ctx.Log.Info("wait to consensus component ready.") + params.Ctx.Log.Info(fmt.Sprintf("wait for consensus component is ready, %d pods are ready, and the expected replicas is %d.", len(pods), target)) return false } if len(pods) < target { - params.Ctx.Log.Info("component pod not all ready.") + params.Ctx.Log.Info(fmt.Sprintf("component pods are not all ready, %d pods are ready, which is less than the expected replicas(%d).", len(pods), target)) return false } return true @@ -103,7 +108,7 @@ func performRollingUpgrade(params reconfigureParams, funcs RollingUpgradeFuncs) podStats := staticPodStats(pods, params.getTargetReplicas(), params.podMinReadySeconds()) podWins := markDynamicCursor(pods, podStats, configKey, configVersion, rollingReplicas) if !validPodState(podWins) { - params.Ctx.Log.Info("wait pod stat ready.") + params.Ctx.Log.Info("wait for pod stat ready.") return makeReturnedStatus(ESRetry), nil } @@ -114,7 +119,7 @@ func performRollingUpgrade(params reconfigureParams, funcs RollingUpgradeFuncs) for _, pod := range waitRollingPods { if podStats.isUpdating(&pod) { - params.Ctx.Log.Info("pod is rolling updating.", "pod name", pod.Name) + params.Ctx.Log.Info("pod is in rolling update.", "pod name", pod.Name) continue } if err := funcs.RestartContainerFunc(&pod, params.Ctx.Ctx, params.ContainerNames, params.ReconfigureClientFactory); err != nil { @@ -163,7 +168,7 @@ func markDynamicCursor(pods []corev1.Pod, podsStats *componentPodStats, configKe podsStats.updated[pod.Name] = pod } - podWindows.begin = cfgcore.Max[int](podWindows.end-int(rollingReplicas), 0) + podWindows.begin = util.Max[int](podWindows.end-int(rollingReplicas), 0) for i := podWindows.begin; i < podWindows.end; i++ { pod := &pods[i] if podutil.IsMatchConfigVersion(pod, configKey, currentVersion) { diff --git a/controllers/apps/configuration/rolling_upgrade_policy_test.go b/controllers/apps/configuration/rolling_upgrade_policy_test.go index 67a41b109..070e49a81 100644 --- a/controllers/apps/configuration/rolling_upgrade_policy_test.go +++ b/controllers/apps/configuration/rolling_upgrade_policy_test.go @@ -1,26 +1,29 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration import ( + "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - "github.com/golang/mock/gomock" + apps "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" metautil "k8s.io/apimachinery/pkg/util/intstr" @@ -172,7 +175,13 @@ var _ = Describe("Reconfigure RollingPolicy", func() { var pods []corev1.Pod { mockParam.Component.WorkloadType = appsv1alpha1.Stateful - mockParam.Component.MaxUnavailable = func() *metautil.IntOrString { v := metautil.FromString("100%"); return &v }() + mockParam.Component.StatefulSpec = &appsv1alpha1.StatefulSetSpec{ + LLUpdateStrategy: &apps.StatefulSetUpdateStrategy{ + RollingUpdate: &apps.RollingUpdateStatefulSetStrategy{ + MaxUnavailable: func() *metautil.IntOrString { v := metautil.FromString("100%"); return &v }(), + }, + }, + } pods = newMockPodsWithStatefulSet(&mockParam.ComponentUnits[0], defaultReplica) } @@ -221,15 +230,15 @@ var _ = Describe("Reconfigure RollingPolicy", func() { }) }) - Context("rolling reconfigure policy test without not support component", func() { + Context("rolling reconfigure policy test for not supported component", func() { It("Should failed", func() { - // not support type + // not supported type _ = mockParam k8sMockClient.MockListMethod(testutil.WithSucceed(testutil.WithTimes(0))) status, err := rollingPolicy.Upgrade(createReconfigureParam(appsv1alpha1.Stateless, defaultReplica)) Expect(err).ShouldNot(Succeed()) - Expect(err.Error()).Should(ContainSubstring("not support component workload type")) + Expect(err.Error()).Should(ContainSubstring("not supported component workload type")) Expect(status.Status).Should(BeEquivalentTo(ESNotSupport)) }) }) diff --git a/controllers/apps/configuration/simple_policy.go b/controllers/apps/configuration/simple_policy.go index 0c42141ba..5a264ff01 100644 --- a/controllers/apps/configuration/simple_policy.go +++ b/controllers/apps/configuration/simple_policy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -40,7 +43,7 @@ func (s *simplePolicy) Upgrade(params reconfigureParams) (ReturnedStatus, error) case appsv1alpha1.Stateful, appsv1alpha1.Consensus, appsv1alpha1.Replication: return rollingStatefulSets(params) default: - return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not support component workload type:[%s]", params.WorkloadType()) + return makeReturnedStatus(ESNotSupport), cfgcore.MakeError("not supported component workload type:[%s]", params.WorkloadType()) } } diff --git a/controllers/apps/configuration/simple_policy_test.go b/controllers/apps/configuration/simple_policy_test.go index f13751975..11655ae44 100644 --- a/controllers/apps/configuration/simple_policy_test.go +++ b/controllers/apps/configuration/simple_policy_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -151,7 +154,7 @@ var _ = Describe("Reconfigure simplePolicy", func() { }) }) - Context("simple reconfigure policy test without not support component", func() { + Context("simple reconfigure policy test for not supported component", func() { It("Should failed", func() { // not support type mockParam := newMockReconfigureParams("simplePolicy", nil, @@ -166,7 +169,7 @@ var _ = Describe("Reconfigure simplePolicy", func() { }}})) status, err := simplePolicy.Upgrade(mockParam) Expect(err).ShouldNot(Succeed()) - Expect(err.Error()).Should(ContainSubstring("not support component workload type")) + Expect(err.Error()).Should(ContainSubstring("not supported component workload type")) Expect(status.Status).Should(BeEquivalentTo(ESNotSupport)) }) }) diff --git a/controllers/apps/configuration/suite_test.go b/controllers/apps/configuration/suite_test.go index 481fb5d9c..72dc7841c 100644 --- a/controllers/apps/configuration/suite_test.go +++ b/controllers/apps/configuration/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/controllers/apps/configuration/sync_upgrade_policy.go b/controllers/apps/configuration/sync_upgrade_policy.go index 6cfed37b7..da9fc14ea 100644 --- a/controllers/apps/configuration/sync_upgrade_policy.go +++ b/controllers/apps/configuration/sync_upgrade_policy.go @@ -1,22 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration import ( + "fmt" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -104,13 +109,13 @@ func sync(params reconfigureParams, updatedParameters map[string]string, pods [] return makeReturnedStatus(ESAndRetryFailed), err } if len(pods) == 0 { - params.Ctx.Log.Info("no pods to update, and retry, selector: %v, current all pod: %v", params.ConfigConstraint.Selector) + params.Ctx.Log.Info(fmt.Sprintf("no pods to update, and retry, selector: %s", params.ConfigConstraint.Selector.String())) return makeReturnedStatus(ESRetry), nil } requireUpdatedCount := int32(len(pods)) for _, pod := range pods { - params.Ctx.Log.V(1).Info("sync pod: %s", pod.Name) + params.Ctx.Log.V(1).Info(fmt.Sprintf("sync pod: %s", pod.Name)) if podutil.IsMatchConfigVersion(&pod, configKey, versionHash) { progress++ continue diff --git a/controllers/apps/configuration/sync_upgrade_policy_test.go b/controllers/apps/configuration/sync_upgrade_policy_test.go index 24ff2d9d0..f045380ec 100644 --- a/controllers/apps/configuration/sync_upgrade_policy_test.go +++ b/controllers/apps/configuration/sync_upgrade_policy_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/controllers/apps/const.go b/controllers/apps/const.go index 23eef527b..9deaaa32c 100644 --- a/controllers/apps/const.go +++ b/controllers/apps/const.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -21,7 +24,6 @@ const ( maxConcurReconClusterDefKey = "MAXCONCURRENTRECONCILES_CLUSTERDEF" // name of our custom finalizer - dbClusterFinalizerName = "cluster.kubeblocks.io/finalizer" dbClusterDefFinalizerName = "clusterdefinition.kubeblocks.io/finalizer" clusterVersionFinalizerName = "clusterversion.kubeblocks.io/finalizer" opsRequestFinalizerName = "opsrequest.kubeblocks.io/finalizer" @@ -35,9 +37,16 @@ const ( lifecycleAnnotationKey = "cluster.kubeblocks.io/lifecycle" // debugClusterAnnotationKey is used when one wants to debug the cluster. // If debugClusterAnnotationKey = 'on', - // logs will be recorded in more detail, and some ephemeral pods (esp. those created by jobs) will retain after execution. + // logs will be recorded in more details, and some ephemeral pods (esp. those created by jobs) will retain after execution. debugClusterAnnotationKey = "cluster.kubeblocks.io/debug" // annotations values lifecycleDeletePVCAnnotation = "delete-pvc" ) + +const ( + reasonOpsCancelActionNotSupported = "CancelActionNotSupported" + reasonOpsCancelActionFailed = "CancelActionFailed" + reasonOpsReconcileStatusFailed = "ReconcileStatusFailed" + reasonOpsDoActionFailed = "DoActionFailed" +) diff --git a/controllers/apps/operations/expose.go b/controllers/apps/operations/expose.go index c6cee17c6..b4f114c3a 100644 --- a/controllers/apps/operations/expose.go +++ b/controllers/apps/operations/expose.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -37,7 +40,7 @@ type ExposeOpsHandler struct { var _ OpsHandler = ExposeOpsHandler{} func init() { - // ToClusterPhase is not defined, because expose not affect the cluster status. + // ToClusterPhase is not defined, because 'expose' does not affect the cluster status. exposeBehavior := OpsBehaviour{ // REVIEW: can do opsrequest if not running? FromClusterPhases: appsv1alpha1.GetClusterUpRunningPhases(), @@ -103,7 +106,7 @@ func (e ExposeOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli cli opsRequest.Status.Progress = fmt.Sprintf("%d/%d", actualProgressCount, expectProgressCount) // patch OpsRequest.status.components - if !reflect.DeepEqual(oldOpsRequestStatus, opsRequest.Status) { + if !reflect.DeepEqual(*oldOpsRequestStatus, opsRequest.Status) { if err := cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil { return opsRequestPhase, 0, err } diff --git a/controllers/apps/operations/expose_test.go b/controllers/apps/operations/expose_test.go index 6cc723ef9..281e8b285 100644 --- a/controllers/apps/operations/expose_test.go +++ b/controllers/apps/operations/expose_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -42,7 +45,7 @@ var _ = Describe("", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. diff --git a/controllers/apps/operations/horizontal_scaling.go b/controllers/apps/operations/horizontal_scaling.go index 7720c3778..86db9260f 100644 --- a/controllers/apps/operations/horizontal_scaling.go +++ b/controllers/apps/operations/horizontal_scaling.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -32,12 +35,14 @@ type horizontalScalingOpsHandler struct{} var _ OpsHandler = horizontalScalingOpsHandler{} func init() { + hsHandler := horizontalScalingOpsHandler{} horizontalScalingBehaviour := OpsBehaviour{ - // if cluster is Abnormal or Failed, new opsRequest may can repair it. + // if cluster is Abnormal or Failed, new opsRequest may repair it. // TODO: we should add "force" flag for these opsRequest. FromClusterPhases: appsv1alpha1.GetClusterUpRunningPhases(), ToClusterPhase: appsv1alpha1.SpecReconcilingClusterPhase, - OpsHandler: horizontalScalingOpsHandler{}, + OpsHandler: hsHandler, + CancelFunc: hsHandler.Cancel, ProcessingReasonInClusterCondition: ProcessingReasonHorizontalScaling, } opsMgr := GetOpsManager() @@ -60,10 +65,8 @@ func (hs horizontalScalingOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli if horizontalScaling, ok = horizontalScalingMap[component.Name]; !ok { continue } - if horizontalScaling.Replicas != 0 { - r := horizontalScaling.Replicas - opsRes.Cluster.Spec.ComponentSpecs[index].Replicas = r - } + r := horizontalScaling.Replicas + opsRes.Cluster.Spec.ComponentSpecs[index].Replicas = r } return cli.Update(reqCtx.Ctx, opsRes.Cluster) } @@ -79,7 +82,7 @@ func (hs horizontalScalingOpsHandler) ReconcileAction(reqCtx intctrlutil.Request compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, int32, error) { return handleComponentProgressForScalingReplicas(reqCtx, cli, opsRes, pgRes, compStatus, hs.getExpectReplicas) } - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) } // GetRealAffectedComponentMap gets the real affected component map for the operation @@ -149,3 +152,22 @@ func getCompPodNamesBeforeScaleDownReplicas(reqCtx intctrlutil.RequestCtx, } return podNames, nil } + +// Cancel this function defines the cancel horizontalScaling action. +func (hs horizontalScalingOpsHandler) Cancel(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error { + return cancelComponentOps(reqCtx.Ctx, cli, opsRes, func(lastConfig *appsv1alpha1.LastComponentConfiguration, comp *appsv1alpha1.ClusterComponentSpec) error { + if lastConfig.Replicas == nil { + return nil + } + podNames, err := getCompPodNamesBeforeScaleDownReplicas(reqCtx, cli, *opsRes.Cluster, comp.Name) + if err != nil { + return err + } + if lastConfig.TargetResources == nil { + lastConfig.TargetResources = map[appsv1alpha1.ComponentResourceKey][]string{} + } + lastConfig.TargetResources[appsv1alpha1.PodsCompResourceKey] = podNames + comp.Replicas = *lastConfig.Replicas + return nil + }) +} diff --git a/controllers/apps/operations/horizontal_scaling_test.go b/controllers/apps/operations/horizontal_scaling_test.go index 11ec55172..0ba1a5dc9 100644 --- a/controllers/apps/operations/horizontal_scaling_test.go +++ b/controllers/apps/operations/horizontal_scaling_test.go @@ -1,27 +1,32 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( "fmt" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -30,6 +35,7 @@ import ( intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" ) var _ = Describe("HorizontalScaling OpsRequest", func() { @@ -42,7 +48,7 @@ var _ = Describe("HorizontalScaling OpsRequest", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -64,7 +70,7 @@ var _ = Describe("HorizontalScaling OpsRequest", func() { AfterEach(cleanEnv) - initClusterForOps := func(opsRes *OpsResource) { + initClusterAnnotationAndPhaseForOps := func(opsRes *OpsResource) { Expect(opsutil.PatchClusterOpsAnnotations(ctx, k8sClient, opsRes.Cluster, nil)).Should(Succeed()) Expect(testapps.ChangeObjStatus(&testCtx, opsRes.Cluster, func() { opsRes.Cluster.Status.Phase = appsv1alpha1.RunningClusterPhase @@ -72,77 +78,129 @@ var _ = Describe("HorizontalScaling OpsRequest", func() { } Context("Test OpsRequest", func() { - It("Test HorizontalScaling OpsRequest", func() { - By("init operations resources ") - reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + + commonHScaleConsensusCompTest := func(reqCtx intctrlutil.RequestCtx, replicas int) (*OpsResource, []corev1.Pod) { + By("init operations resources with CLusterDefinition/ClusterVersion/Hybrid components Cluster/consensus Pods") opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) + podList := initConsensusPods(ctx, k8sClient, opsRes, clusterName) - By("Test HorizontalScaling with scale down replicas") - opsRes.OpsRequest = createHorizontalScaling(clusterName, 1) - mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp) // appsv1alpha1.VerticalScalingPhase - initClusterForOps(opsRes) + By(fmt.Sprintf("create opsRequest for scaling down replicas of consensus component from 3 to %d", replicas)) + initClusterAnnotationAndPhaseForOps(opsRes) + opsRes.OpsRequest = createHorizontalScaling(clusterName, replicas) + mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp) - By("mock HorizontalScaling OpsRequest phase is Creating and do action") + By("expect for opsRequest phase is Creating after doing action") _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) + By(fmt.Sprintf("expect for the replicas of consensus component is %d after doing action again when opsRequest phase is Creating", replicas)) + _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(opsRes.Cluster), func(g Gomega, tmpCluster *appsv1alpha1.Cluster) { + g.Expect(tmpCluster.Spec.GetComponentByName(consensusComp).Replicas).Should(BeEquivalentTo(replicas)) + })).Should(Succeed()) + By("Test OpsManager.Reconcile function when horizontal scaling OpsRequest is Running") - opsRes.Cluster.Status.Phase = appsv1alpha1.RunningClusterPhase + opsRes.OpsRequest.Status.Phase = appsv1alpha1.OpsRequestKind _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) - - By("test GetOpsRequestAnnotation function") - Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func() { - opsAnnotationString := fmt.Sprintf(`[{"name":"%s","clusterPhase":"Updating"},{"name":"test-not-exists-ops","clusterPhase":"Updating"}]`, - opsRes.OpsRequest.Name) - opsRes.Cluster.Annotations = map[string]string{ - constant.OpsRequestAnnotationKey: opsAnnotationString, - } - })).ShouldNot(HaveOccurred()) - _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) - Expect(err.Error()).Should(ContainSubstring("existing OpsRequest:")) - - // reset cluster annotation - Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func() { - opsRes.Cluster.Annotations = map[string]string{} - })).ShouldNot(HaveOccurred()) - - By("Test HorizontalScaling with scale up replicax") - initClusterForOps(opsRes) - expectClusterComponentReplicas := int32(2) - Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func() { - opsRes.Cluster.Spec.ComponentSpecs[1].Replicas = expectClusterComponentReplicas - })).ShouldNot(HaveOccurred()) - - // mock pod created according to horizontalScaling replicas - for _, v := range []int{1, 2} { - podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, v) - testapps.MockConsensusComponentStsPod(testCtx, nil, clusterName, consensusComp, podName, "follower", "ReadOnly") - } - - opsRes.OpsRequest = createHorizontalScaling(clusterName, 3) - _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + return opsRes, podList + } + + checkOpsRequestPhaseIsSucceed := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource) { + By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running") + // mock consensus component is Running + mockConsensusCompToRunning(opsRes) + _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) - Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) + Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) + } - // do h-scale action - _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + checkCancelledSucceed := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource) { + _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) + Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancelledPhase)) + opsProgressDetails := opsRes.OpsRequest.Status.Components[consensusComp].ProgressDetails + Expect(len(opsProgressDetails)).Should(Equal(1)) + Expect(opsProgressDetails[0].Status).Should(Equal(appsv1alpha1.SucceedProgressStatus)) + } - _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(err).ShouldNot(HaveOccurred()) + It("test scaling down replicas", func() { + reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + opsRes, podList := commonHScaleConsensusCompTest(reqCtx, 1) + By("mock two pods are deleted") + for i := 0; i < 2; i++ { + pod := &podList[i] + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + testk8s.RemovePodFinalizer(ctx, testCtx, pod) + } + checkOpsRequestPhaseIsSucceed(reqCtx, opsRes) + }) + It("test scaling out replicas", func() { + reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + opsRes, _ := commonHScaleConsensusCompTest(reqCtx, 5) + By("mock two pods are created") + for i := 3; i < 5; i++ { + podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, i) + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "follower", "Readonly") + } + checkOpsRequestPhaseIsSucceed(reqCtx, opsRes) + // TODO: remove it By("test GetRealAffectedComponentMap function") h := horizontalScalingOpsHandler{} Expect(len(h.GetRealAffectedComponentMap(opsRes.OpsRequest))).Should(Equal(1)) }) + It("test canceling HScale opsRequest which scales down replicas of component", func() { + reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + opsRes, podList := commonHScaleConsensusCompTest(reqCtx, 1) + + By("mock one pod has been deleted") + pod := &podList[0] + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + testk8s.RemovePodFinalizer(ctx, testCtx, pod) + + By("cancel HScale opsRequest after one pod has been deleted") + cancelOpsRequest(reqCtx, opsRes, time.Now().Add(-1*time.Second)) + + By("re-create the deleted pod") + podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, 0) + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "leader", "ReadWrite") + + By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running") + mockConsensusCompToRunning(opsRes) + checkCancelledSucceed(reqCtx, opsRes) + }) + + It("test canceling HScale opsRequest which scales out replicas of component", func() { + reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + opsRes, _ := commonHScaleConsensusCompTest(reqCtx, 5) + + By("mock one pod is created") + podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, 3) + pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, podName, "follower", "Readonly") + + By("cancel HScale opsRequest after pne pod is created") + cancelOpsRequest(reqCtx, opsRes, time.Now().Add(-1*time.Second)) + + By("delete the created pod") + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + testk8s.RemovePodFinalizer(ctx, testCtx, pod) + + By("expect for opsRequest phase is Succeed after pods has been scaled and component phase is Running") + mockConsensusCompToRunning(opsRes) + checkCancelledSucceed(reqCtx, opsRes) + }) }) }) func createHorizontalScaling(clusterName string, replicas int) *appsv1alpha1.OpsRequest { - horizontalOpsName := "horizontalscaling-ops-" + testCtx.GetRandomStr() + horizontalOpsName := "horizontal-scaling-ops-" + testCtx.GetRandomStr() ops := testapps.NewOpsRequestObj(horizontalOpsName, testCtx.DefaultNamespace, clusterName, appsv1alpha1.HorizontalScalingType) ops.Spec.HorizontalScalingList = []appsv1alpha1.HorizontalScaling{ @@ -153,3 +211,21 @@ func createHorizontalScaling(clusterName string, replicas int) *appsv1alpha1.Ops } return testapps.CreateOpsRequest(ctx, testCtx, ops) } + +func cancelOpsRequest(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource, cancelTime time.Time) { + opsRequest := opsRes.OpsRequest + opsRequest.Spec.Cancel = true + opsBehaviour := GetOpsManager().OpsMap[opsRequest.Spec.Type] + Expect(testapps.ChangeObjStatus(&testCtx, opsRequest, func() { + opsRequest.Status.CancelTimestamp = metav1.Time{Time: cancelTime} + opsRequest.Status.Phase = appsv1alpha1.OpsCancellingPhase + })).Should(Succeed()) + Expect(opsBehaviour.CancelFunc(reqCtx, k8sClient, opsRes)).ShouldNot(HaveOccurred()) +} + +func mockConsensusCompToRunning(opsRes *OpsResource) { + // mock consensus component is Running + compStatus := opsRes.Cluster.Status.Components[consensusComp] + compStatus.Phase = appsv1alpha1.RunningClusterCompPhase + opsRes.Cluster.Status.Components[consensusComp] = compStatus +} diff --git a/controllers/apps/operations/ops_manager.go b/controllers/apps/operations/ops_manager.go index 9142b85f7..11e877e72 100644 --- a/controllers/apps/operations/ops_manager.go +++ b/controllers/apps/operations/ops_manager.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -32,7 +35,7 @@ var ( opsManager *OpsManager ) -// RegisterOps register operation with OpsType and OpsBehaviour +// RegisterOps registers operation with OpsType and OpsBehaviour func (opsMgr *OpsManager) RegisterOps(opsType appsv1alpha1.OpsType, opsBehaviour OpsBehaviour) { opsManager.OpsMap[opsType] = opsBehaviour appsv1alpha1.OpsRequestBehaviourMapper[opsType] = appsv1alpha1.OpsRequestBehaviour{ @@ -42,7 +45,7 @@ func (opsMgr *OpsManager) RegisterOps(opsType appsv1alpha1.OpsType, opsBehaviour } } -// Do the common entry function for handling OpsRequest +// Do the entry function for handling OpsRequest func (opsMgr *OpsManager) Do(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (*ctrl.Result, error) { var ( opsBehaviour OpsBehaviour @@ -56,14 +59,14 @@ func (opsMgr *OpsManager) Do(reqCtx intctrlutil.RequestCtx, cli client.Client, o // validate OpsRequest.spec if err = opsRequest.Validate(reqCtx.Ctx, cli, opsRes.Cluster, true); err != nil { - if patchErr := PatchValidateErrorCondition(reqCtx.Ctx, cli, opsRes, err.Error()); patchErr != nil { + if patchErr := patchValidateErrorCondition(reqCtx.Ctx, cli, opsRes, err.Error()); patchErr != nil { return nil, patchErr } return nil, err } if opsRequest.Status.Phase != appsv1alpha1.OpsCreatingPhase { // If the operation causes the cluster phase to change, the cluster needs to be locked. - // At the same time, only one operation is running if these operations are mutex(exist opsBehaviour.ToClusterPhase). + // At the same time, only one operation is running if these operations are mutually exclusive(exist opsBehaviour.ToClusterPhase). if err = addOpsRequestAnnotationToCluster(reqCtx.Ctx, cli, opsRes, opsBehaviour); err != nil { return nil, err } @@ -101,15 +104,22 @@ func (opsMgr *OpsManager) Reconcile(reqCtx intctrlutil.RequestCtx, cli client.Cl return 0, patchOpsHandlerNotSupported(reqCtx.Ctx, cli, opsRes) } opsRes.ToClusterPhase = opsBehaviour.ToClusterPhase - if opsRequestPhase, requeueAfter, err = opsBehaviour.OpsHandler.ReconcileAction(reqCtx, cli, opsRes); err != nil && !isOpsRequestFailedPhase(opsRequestPhase) { + if opsRequestPhase, requeueAfter, err = opsBehaviour.OpsHandler.ReconcileAction(reqCtx, cli, opsRes); err != nil && + !isOpsRequestFailedPhase(opsRequestPhase) { // if the opsRequest phase is Failed, skipped return requeueAfter, err } switch opsRequestPhase { case appsv1alpha1.OpsSucceedPhase: - return requeueAfter, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, opsRequestPhase, appsv1alpha1.NewSucceedCondition(opsRequest)) + if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + return 0, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, appsv1alpha1.OpsCancelledPhase, appsv1alpha1.NewCancelSucceedCondition(opsRequest.Name)) + } + return 0, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, opsRequestPhase, appsv1alpha1.NewSucceedCondition(opsRequest)) case appsv1alpha1.OpsFailedPhase: - return requeueAfter, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, opsRequestPhase, appsv1alpha1.NewFailedCondition(opsRequest, err)) + if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + return 0, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, appsv1alpha1.OpsCancelledPhase, appsv1alpha1.NewCancelFailedCondition(opsRequest, err)) + } + return 0, PatchOpsStatus(reqCtx.Ctx, cli, opsRes, opsRequestPhase, appsv1alpha1.NewFailedCondition(opsRequest, err)) default: return requeueAfter, nil } diff --git a/controllers/apps/operations/ops_progress_util.go b/controllers/apps/operations/ops_progress_util.go index 70b8c83fa..ef9e16f11 100644 --- a/controllers/apps/operations/ops_progress_util.go +++ b/controllers/apps/operations/ops_progress_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -28,29 +31,27 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/controllers/apps/components" - "github.com/apecloud/kubeblocks/controllers/apps/components/stateless" - "github.com/apecloud/kubeblocks/controllers/apps/components/types" "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// GetProgressObjectKey gets progress object key from the object of client.Object. -func GetProgressObjectKey(kind, name string) string { +// getProgressObjectKey gets progress object key from the client.Object. +func getProgressObjectKey(kind, name string) string { return fmt.Sprintf("%s/%s", kind, name) } -// isCompletedProgressStatus the progress detail is in final state, either Failed or Succeed. +// isCompletedProgressStatus checks the progress detail with final state, either Failed or Succeed. func isCompletedProgressStatus(status appsv1alpha1.ProgressStatus) bool { return slices.Contains([]appsv1alpha1.ProgressStatus{appsv1alpha1.SucceedProgressStatus, appsv1alpha1.FailedProgressStatus}, status) } -// SetComponentStatusProgressDetail sets the corresponding progressDetail in progressDetails to newProgressDetail. +// setComponentStatusProgressDetail sets the corresponding progressDetail in progressDetails to newProgressDetail. // progressDetails must be non-nil. // 1. the startTime and endTime will be filled automatically. // 2. if the progressDetail of the specified objectKey does not exist, it will be appended to the progressDetails. -func SetComponentStatusProgressDetail( +func setComponentStatusProgressDetail( recorder record.EventRecorder, opsRequest *appsv1alpha1.OpsRequest, progressDetails *[]appsv1alpha1.ProgressStatusDetail, @@ -58,14 +59,15 @@ func SetComponentStatusProgressDetail( if progressDetails == nil { return } - existingProgressDetail := FindStatusProgressDetail(*progressDetails, newProgressDetail.ObjectKey) + existingProgressDetail := findStatusProgressDetail(*progressDetails, newProgressDetail.ObjectKey) if existingProgressDetail == nil { updateProgressDetailTime(&newProgressDetail) *progressDetails = append(*progressDetails, newProgressDetail) sendProgressDetailEvent(recorder, opsRequest, newProgressDetail) return } - if existingProgressDetail.Status == newProgressDetail.Status { + if existingProgressDetail.Status == newProgressDetail.Status && + existingProgressDetail.Message == newProgressDetail.Message { return } // if existing progress detail is 'Failed' and new progress detail is not 'Succeed', ignores the new one. @@ -79,8 +81,8 @@ func SetComponentStatusProgressDetail( sendProgressDetailEvent(recorder, opsRequest, newProgressDetail) } -// FindStatusProgressDetail finds the progressDetail of the specified objectKey in progressDetails. -func FindStatusProgressDetail(progressDetails []appsv1alpha1.ProgressStatusDetail, +// findStatusProgressDetail finds the progressDetail of the specified objectKey in progressDetails. +func findStatusProgressDetail(progressDetails []appsv1alpha1.ProgressStatusDetail, objectKey string) *appsv1alpha1.ProgressStatusDetail { for i := range progressDetails { if progressDetails[i].ObjectKey == objectKey { @@ -139,16 +141,16 @@ func updateProgressDetailTime(progressDetail *appsv1alpha1.ProgressStatusDetail) func convertPodObjectKeyMap(podList *corev1.PodList) map[string]struct{} { podObjectKeyMap := map[string]struct{}{} for _, v := range podList.Items { - objectKey := GetProgressObjectKey(v.Kind, v.Name) + objectKey := getProgressObjectKey(v.Kind, v.Name) podObjectKeyMap[objectKey] = struct{}{} } return podObjectKeyMap } -// removeStatelessExpiredPod if the object of progressDetail is not existing in k8s cluster, it indicates the pod is deleted. +// removeStatelessExpiredPods if the object of progressDetail is not existing in k8s cluster, it indicates the pod is deleted. // For example, a replicaSet may attempt to create a pod multiple times till it succeeds. // so some pod may be expired, we should clear them. -func removeStatelessExpiredPod(podList *corev1.PodList, +func removeStatelessExpiredPods(podList *corev1.PodList, progressDetails []appsv1alpha1.ProgressStatusDetail) []appsv1alpha1.ProgressStatusDetail { podObjectKeyMap := convertPodObjectKeyMap(podList) newProgressDetails := make([]appsv1alpha1.ProgressStatusDetail, 0) @@ -161,7 +163,7 @@ func removeStatelessExpiredPod(podList *corev1.PodList, } // handleComponentStatusProgress handles the component status progressDetails. -// if all the pods of the component are affected, use this common function to reconcile the progressDetails. +// if all the pods of the component are affected, use this function to reconcile the progressDetails. func handleComponentStatusProgress( reqCtx intctrlutil.RequestCtx, cli client.Client, @@ -185,7 +187,12 @@ func handleComponentStatusProgress( default: completedCount, err = handleStatefulSetProgress(reqCtx, cli, opsRes, podList, pgRes, compStatus) } - return clusterComponent.Replicas, completedCount, err + expectReplicas := clusterComponent.Replicas + if opsRes.OpsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + // only rollback the actual re-created pod during cancelling. + expectReplicas = int32(len(compStatus.ProgressDetails)) + } + return expectReplicas, completedCount, err } // handleStatelessProgress handles the stateless component progressDetails. @@ -199,83 +206,104 @@ func handleStatelessProgress(reqCtx intctrlutil.RequestCtx, if compStatus.Phase == appsv1alpha1.RunningClusterCompPhase && pgRes.clusterComponent.Replicas != int32(len(podList.Items)) { return 0, intctrlutil.NewError(intctrlutil.ErrorWaitCacheRefresh, "wait for the pods of deployment to be synchronized") } - - currComponent, err := stateless.NewStateless(cli, opsRes.Cluster, - pgRes.clusterComponent, *pgRes.clusterComponentDef) + minReadySeconds, err := util.GetComponentDeployMinReadySeconds(reqCtx.Ctx, cli, *opsRes.Cluster, pgRes.clusterComponent.Name) if err != nil { return 0, err } + completedCount := handleRollingUpdateProgress(opsRes, podList, pgRes, compStatus, minReadySeconds) + compStatus.ProgressDetails = removeStatelessExpiredPods(podList, compStatus.ProgressDetails) + return completedCount, nil +} - if currComponent == nil { - return 0, nil - } - var componentName = pgRes.clusterComponent.Name - minReadySeconds, err := util.GetComponentDeployMinReadySeconds(reqCtx.Ctx, - cli, *opsRes.Cluster, componentName) +// handleStatefulSetProgress handles the component progressDetails which using statefulSet workloads. +func handleStatefulSetProgress(reqCtx intctrlutil.RequestCtx, + cli client.Client, + opsRes *OpsResource, + podList *corev1.PodList, + pgRes progressResource, + compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, error) { + minReadySeconds, err := util.GetComponentStsMinReadySeconds(reqCtx.Ctx, cli, *opsRes.Cluster, pgRes.clusterComponent.Name) if err != nil { return 0, err } - var completedCount int32 + return handleRollingUpdateProgress(opsRes, podList, pgRes, compStatus, minReadySeconds), nil +} + +// handleRollingUpdateProgress handles the component progressDetails during rolling update. +func handleRollingUpdateProgress( + opsRes *OpsResource, + podList *corev1.PodList, + pgRes progressResource, + compStatus *appsv1alpha1.OpsRequestComponentStatus, + minReadySeconds int32) int32 { + if opsRes.OpsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + return handleCancelProgressForPodsRollingUpdate(opsRes, podList, pgRes, compStatus, minReadySeconds) + } + return handleProgressForPodsRollingUpdate(opsRes, podList, pgRes, compStatus, minReadySeconds) +} + +// handleProgressForPodsRollingUpdate handles the progress of pods during rolling update. +func handleProgressForPodsRollingUpdate( + opsRes *OpsResource, + podList *corev1.PodList, + pgRes progressResource, + compStatus *appsv1alpha1.OpsRequestComponentStatus, + minReadySeconds int32) int32 { + workloadType := pgRes.clusterComponentDef.WorkloadType opsRequest := opsRes.OpsRequest opsStartTime := opsRequest.Status.StartTimestamp + var completedCount int32 for _, v := range podList.Items { - objectKey := GetProgressObjectKey(v.Kind, v.Name) + objectKey := getProgressObjectKey(v.Kind, v.Name) progressDetail := appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey} - if podIsPendingDuringOperation(opsStartTime, &v, compStatus.Phase) { - handlePendingProgressDetail(opsRes, compStatus, progressDetail) - continue - } - - if podProcessedSuccessful(currComponent, opsStartTime, &v, - minReadySeconds, compStatus.Phase, pgRes.opsIsCompleted) { + if podProcessedSuccessful(workloadType, opsStartTime, &v, minReadySeconds, compStatus.Phase, pgRes.opsIsCompleted) { completedCount += 1 handleSucceedProgressDetail(opsRes, pgRes, compStatus, progressDetail) continue } + if podIsPendingDuringOperation(opsStartTime, &v) { + handlePendingProgressDetail(opsRes, compStatus, progressDetail) + continue + } completedCount += handleFailedOrProcessingProgressDetail(opsRes, pgRes, compStatus, progressDetail, &v) } - compStatus.ProgressDetails = removeStatelessExpiredPod(podList, compStatus.ProgressDetails) - return completedCount, err + return completedCount } -// REVIEW/TOD: similar code pattern (do de-dupe) -// handleStatefulSetProgress handles the component progressDetails which using statefulSet workloads. -func handleStatefulSetProgress(reqCtx intctrlutil.RequestCtx, - cli client.Client, +// handleCancelProgressForPodsRollingUpdate handles the cancel progress of pods during rolling update. +func handleCancelProgressForPodsRollingUpdate( opsRes *OpsResource, podList *corev1.PodList, pgRes progressResource, - compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, error) { - currComponent, err := components.NewComponentByType(cli, - opsRes.Cluster, pgRes.clusterComponent, *pgRes.clusterComponentDef) - if err != nil { - return 0, err - } - var componentName = pgRes.clusterComponent.Name - minReadySeconds, err := util.GetComponentStsMinReadySeconds(reqCtx.Ctx, - cli, *opsRes.Cluster, componentName) - if err != nil { - return 0, err + compStatus *appsv1alpha1.OpsRequestComponentStatus, + minReadySeconds int32) int32 { + var newProgressDetails []appsv1alpha1.ProgressStatusDetail + for _, v := range compStatus.ProgressDetails { + // remove the pending progressDetail + if v.Status != appsv1alpha1.PendingProgressStatus { + newProgressDetails = append(newProgressDetails, v) + } } - opsRequest := opsRes.OpsRequest - opsStartTime := opsRequest.Status.StartTimestamp + compStatus.ProgressDetails = newProgressDetails + opsCancelTime := opsRes.OpsRequest.Status.CancelTimestamp + workloadType := pgRes.clusterComponentDef.WorkloadType + pgRes.opsMessageKey = fmt.Sprintf("%s with rollback", pgRes.opsMessageKey) var completedCount int32 - for _, v := range podList.Items { - objectKey := GetProgressObjectKey(v.Kind, v.Name) + for _, pod := range podList.Items { + objectKey := getProgressObjectKey(pod.Kind, pod.Name) progressDetail := appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey} - if podIsPendingDuringOperation(opsStartTime, &v, compStatus.Phase) { - handlePendingProgressDetail(opsRes, compStatus, progressDetail) - continue - } - if podProcessedSuccessful(currComponent, opsStartTime, &v, - minReadySeconds, compStatus.Phase, pgRes.opsIsCompleted) { + if !pod.CreationTimestamp.Before(&opsCancelTime) && + components.PodIsAvailable(workloadType, &pod, minReadySeconds) { completedCount += 1 handleSucceedProgressDetail(opsRes, pgRes, compStatus, progressDetail) continue } - completedCount += handleFailedOrProcessingProgressDetail(opsRes, pgRes, compStatus, progressDetail, &v) + if podIsPendingDuringOperation(opsCancelTime, &pod) { + continue + } + completedCount += handleFailedOrProcessingProgressDetail(opsRes, pgRes, compStatus, progressDetail, &pod) } - return completedCount, err + return completedCount } // handlePendingProgressDetail handles the pending progressDetail and sets it to progressDetails. @@ -284,7 +312,7 @@ func handlePendingProgressDetail(opsRes *OpsResource, progressDetail appsv1alpha1.ProgressStatusDetail, ) { progressDetail.Status = appsv1alpha1.PendingProgressStatus - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) } @@ -296,7 +324,7 @@ func handleSucceedProgressDetail(opsRes *OpsResource, ) { progressDetail.SetStatusAndMessage(appsv1alpha1.SucceedProgressStatus, getProgressSucceedMessage(pgRes.opsMessageKey, progressDetail.ObjectKey, pgRes.clusterComponent.Name)) - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) } @@ -321,15 +349,14 @@ func handleFailedOrProcessingProgressDetail(opsRes *OpsResource, progressDetail.SetStatusAndMessage(appsv1alpha1.ProcessingProgressStatus, getProgressProcessingMessage(pgRes.opsMessageKey, progressDetail.ObjectKey, componentName)) } - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) return completedCount } -// podIsPendingDuringOperation checks if pod is pending during the component is doing operation. -func podIsPendingDuringOperation(opsStartTime metav1.Time, pod *corev1.Pod, componentPhase appsv1alpha1.ClusterComponentPhase) bool { - return pod.CreationTimestamp.Before(&opsStartTime) && pod.DeletionTimestamp.IsZero() && - !slices.Contains(appsv1alpha1.GetComponentTerminalPhases(), componentPhase) +// podIsPendingDuringOperation checks if pod is pending during the component's operation. +func podIsPendingDuringOperation(opsStartTime metav1.Time, pod *corev1.Pod) bool { + return pod.CreationTimestamp.Before(&opsStartTime) && pod.DeletionTimestamp.IsZero() } // podIsFailedDuringOperation checks if pod is failed during operation. @@ -347,13 +374,13 @@ func podIsFailedDuringOperation( // podProcessedSuccessful checks if the pod has been processed successfully: // 1. the pod is recreated after OpsRequest.status.startTime and pod is available. // 2. the component is running and pod is available. -func podProcessedSuccessful(componentImpl types.Component, +func podProcessedSuccessful(workloadType appsv1alpha1.WorkloadType, opsStartTime metav1.Time, pod *corev1.Pod, minReadySeconds int32, componentPhase appsv1alpha1.ClusterComponentPhase, opsIsCompleted bool) bool { - if !componentImpl.PodIsAvailable(pod, minReadySeconds) { + if !components.PodIsAvailable(workloadType, pod, minReadySeconds) { return false } return (opsIsCompleted && componentPhase == appsv1alpha1.RunningClusterCompPhase) || !pod.CreationTimestamp.Before(&opsStartTime) @@ -387,57 +414,73 @@ func getComponentLastReplicas(opsRequest *appsv1alpha1.OpsRequest, componentName } // handleComponentProgressDetails handles the component progressDetails when scale the replicas. +// @return expectProgressCount, +// @return completedCount +// @return error func handleComponentProgressForScalingReplicas(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource, pgRes progressResource, compStatus *appsv1alpha1.OpsRequestComponentStatus, - getExpectReplicas func(opsRequest *appsv1alpha1.OpsRequest, componentName string) *int32) (expectProgressCount int32, completedCount int32, err error) { + getExpectReplicas func(opsRequest *appsv1alpha1.OpsRequest, componentName string) *int32) (int32, int32, error) { var ( podList *corev1.PodList clusterComponent = pgRes.clusterComponent opsRequest = opsRes.OpsRequest - isScaleOut bool + err error ) if clusterComponent == nil || pgRes.clusterComponentDef == nil { - return + return 0, 0, nil } expectReplicas := getExpectReplicas(opsRequest, clusterComponent.Name) if expectReplicas == nil { - return + return 0, 0, nil } lastComponentReplicas := getComponentLastReplicas(opsRequest, clusterComponent.Name) if lastComponentReplicas == nil { - return + return 0, 0, nil } // if replicas are not changed, return if *lastComponentReplicas == *expectReplicas { - return + return 0, 0, nil } if podList, err = util.GetComponentPodList(reqCtx.Ctx, cli, *opsRes.Cluster, clusterComponent.Name); err != nil { - return + return 0, 0, err } - if compStatus.Phase == appsv1alpha1.RunningClusterCompPhase && pgRes.clusterComponent.Replicas != int32(len(podList.Items)) { - err = intctrlutil.NewError(intctrlutil.ErrorWaitCacheRefresh, "wait for the pods of component to be synchronized") - return + actualPodsLen := int32(len(podList.Items)) + if compStatus.Phase == appsv1alpha1.RunningClusterCompPhase && pgRes.clusterComponent.Replicas != actualPodsLen { + return 0, 0, intctrlutil.NewError(intctrlutil.ErrorWaitCacheRefresh, "wait for the pods of component to be synchronized") } - dValue := *expectReplicas - *lastComponentReplicas + if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + expectReplicas = lastComponentReplicas + // lastComponentPods is the snapshot of component pods at cancelling, + // use this count as the last component replicas during canceling. + lastComponentPodNames := getTargetResourcesOfLastComponent(opsRes.OpsRequest.Status.LastConfiguration, + pgRes.clusterComponent.Name, appsv1alpha1.PodsCompResourceKey) + lastComponentPodCount := int32(len(lastComponentPodNames)) + lastComponentReplicas = &lastComponentPodCount + } + var ( + isScaleOut bool + expectProgressCount int32 + completedCount int32 + dValue = *expectReplicas - *lastComponentReplicas + ) if dValue > 0 { expectProgressCount = dValue isScaleOut = true } else { expectProgressCount = dValue * -1 } - if !isScaleOut { - completedCount, err = handleScaleDownProgress(opsRes, pgRes, podList, compStatus) - expectProgressCount = getFinalExpectCount(compStatus, expectProgressCount) - return - } - completedCount, err = handleScaleOutProgress(reqCtx, cli, opsRes, pgRes, podList, compStatus) - // if the workload type is Stateless, remove the progressDetails of the expired pods. - // because a replicaSet may attempt to create a pod multiple times till it succeeds when scale out the replicas. - if pgRes.clusterComponentDef.WorkloadType == appsv1alpha1.Stateless { - compStatus.ProgressDetails = removeStatelessExpiredPod(podList, compStatus.ProgressDetails) + if isScaleOut { + completedCount, err = handleScaleOutProgress(reqCtx, cli, opsRes, pgRes, podList, compStatus) + // if the workload type is Stateless, remove the progressDetails of the expired pods. + // because ReplicaSet may attempt to create a pod multiple times till it succeeds when scale out the replicas. + if pgRes.clusterComponentDef.WorkloadType == appsv1alpha1.Stateless { + compStatus.ProgressDetails = removeStatelessExpiredPods(podList, compStatus.ProgressDetails) + } + } else { + completedCount, err = handleScaleDownProgress(reqCtx, cli, opsRes, pgRes, podList, compStatus) } return getFinalExpectCount(compStatus, expectProgressCount), completedCount, err } @@ -450,13 +493,8 @@ func handleScaleOutProgress(reqCtx intctrlutil.RequestCtx, podList *corev1.PodList, compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, error) { var componentName = pgRes.clusterComponent.Name - currComponent, err := components.NewComponentByType(cli, - opsRes.Cluster, pgRes.clusterComponent, *pgRes.clusterComponentDef) - if err != nil { - return 0, err - } - minReadySeconds, err := util.GetComponentWorkloadMinReadySeconds(reqCtx.Ctx, - cli, *opsRes.Cluster, pgRes.clusterComponentDef.WorkloadType, componentName) + var workloadType = pgRes.clusterComponentDef.WorkloadType + minReadySeconds, err := util.GetComponentWorkloadMinReadySeconds(reqCtx.Ctx, cli, *opsRes.Cluster, workloadType, componentName) if err != nil { return 0, err } @@ -466,71 +504,105 @@ func handleScaleOutProgress(reqCtx intctrlutil.RequestCtx, if v.CreationTimestamp.Before(&opsRes.OpsRequest.Status.StartTimestamp) { continue } - objectKey := GetProgressObjectKey(v.Kind, v.Name) + objectKey := getProgressObjectKey(v.Kind, v.Name) progressDetail := appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey} - if currComponent.PodIsAvailable(&v, minReadySeconds) { + pgRes.opsMessageKey = "create" + if components.PodIsAvailable(workloadType, &v, minReadySeconds) { completedCount += 1 - message := fmt.Sprintf("Successfully created pod: %s in Component: %s", objectKey, componentName) - progressDetail.SetStatusAndMessage(appsv1alpha1.SucceedProgressStatus, message) - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, - &compStatus.ProgressDetails, progressDetail) + handleSucceedProgressDetail(opsRes, pgRes, compStatus, progressDetail) continue } - - if util.IsFailedOrAbnormal(compStatus.Phase) { - // means the pod is failed. - podMessage := getFailedPodMessage(opsRes.Cluster, componentName, &v) - message := fmt.Sprintf("Failed to create pod: %s in Component: %s, message: %s", objectKey, componentName, podMessage) - progressDetail.SetStatusAndMessage(appsv1alpha1.FailedProgressStatus, message) - completedCount += 1 - } else { - progressDetail.SetStatusAndMessage(appsv1alpha1.ProcessingProgressStatus, "Start to create pod: "+objectKey) - } - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, - &compStatus.ProgressDetails, progressDetail) + completedCount += handleFailedOrProcessingProgressDetail(opsRes, pgRes, compStatus, progressDetail, &v) } return completedCount, nil } // handleScaleDownProgress handles the progressDetails of scaled down replicas. func handleScaleDownProgress( + reqCtx intctrlutil.RequestCtx, + cli client.Client, opsRes *OpsResource, pgRes progressResource, podList *corev1.PodList, compStatus *appsv1alpha1.OpsRequestComponentStatus) (completedCount int32, err error) { - podMap := map[string]struct{}{} + podMap := map[string]corev1.Pod{} // record the deleting pod progressDetail for _, v := range podList.Items { - objectKey := GetProgressObjectKey(constant.PodKind, v.Name) - podMap[objectKey] = struct{}{} + objectKey := getProgressObjectKey(constant.PodKind, v.Name) + podMap[objectKey] = v if v.DeletionTimestamp.IsZero() { continue } - progressDetail := appsv1alpha1.ProgressStatusDetail{ - ObjectKey: objectKey, - Status: appsv1alpha1.ProcessingProgressStatus, - Message: fmt.Sprintf("Start to delete pod: %s in Component: %s", objectKey, pgRes.clusterComponent.Name), - } - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, - &compStatus.ProgressDetails, progressDetail) + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, + &compStatus.ProgressDetails, appsv1alpha1.ProgressStatusDetail{ + ObjectKey: objectKey, + Status: appsv1alpha1.ProcessingProgressStatus, + Message: fmt.Sprintf("Start to delete pod: %s in Component: %s", objectKey, pgRes.clusterComponent.Name), + }) + } + var workloadType = pgRes.clusterComponentDef.WorkloadType + var componentName = pgRes.clusterComponent.Name + minReadySeconds, err := util.GetComponentStsMinReadySeconds(reqCtx.Ctx, cli, *opsRes.Cluster, componentName) + if err != nil { + return 0, err } - lastComponentConfigs := opsRes.OpsRequest.Status.LastConfiguration.Components[pgRes.clusterComponent.Name] - lastComponentPodNames := lastComponentConfigs.TargetResources[appsv1alpha1.PodsCompResourceKey] - for _, v := range lastComponentPodNames { - objectKey := GetProgressObjectKey(constant.PodKind, v) - if _, ok := podMap[objectKey]; ok { - continue - } + + handleDeletionSuccessful := func(objectKey string) { // if the pod is not in the podList, it means the pod has been deleted. progressDetail := appsv1alpha1.ProgressStatusDetail{ ObjectKey: objectKey, Status: appsv1alpha1.SucceedProgressStatus, - Message: fmt.Sprintf("Successfully deleted pod: %s in Component: %s", objectKey, pgRes.clusterComponent.Name), + Message: fmt.Sprintf("Successfully delete pod: %s in Component: %s", objectKey, pgRes.clusterComponent.Name), } completedCount += 1 - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) } + + handleProgressDetails := func() { + for _, progressDetail := range compStatus.ProgressDetails { + if isCompletedProgressStatus(progressDetail.Status) { + completedCount += 1 + continue + } + // if pod not exists, means successful deletion. + pod, ok := podMap[progressDetail.ObjectKey] + if !ok { + handleDeletionSuccessful(progressDetail.ObjectKey) + continue + } + // handle the re-created pods if these pods are failed before doing horizontal scaling. + pgRes.opsMessageKey = "re-create" + if components.PodIsAvailable(workloadType, &pod, minReadySeconds) { + completedCount += 1 + handleSucceedProgressDetail(opsRes, pgRes, compStatus, progressDetail) + continue + } + if pod.DeletionTimestamp.IsZero() { + completedCount += handleFailedOrProcessingProgressDetail(opsRes, pgRes, compStatus, progressDetail, &pod) + } + } + } + + handleDeletedPodNotInProgressDetails := func() { + // pod may not be recorded in the progressDetails if deleted quickly or due to unknown reasons, but it has actually been deleted. + // compare with the last pods and current pods to check if pod is deleted. + lastComponentPodNames := getTargetResourcesOfLastComponent(opsRes.OpsRequest.Status.LastConfiguration, componentName, appsv1alpha1.PodsCompResourceKey) + for _, v := range lastComponentPodNames { + objectKey := getProgressObjectKey(constant.PodKind, v) + progressDetail := findStatusProgressDetail(compStatus.ProgressDetails, objectKey) + // if recorded in progressDetails, continue + if progressDetail != nil { + continue + } + if _, ok := podMap[objectKey]; ok { + continue + } + handleDeletionSuccessful(objectKey) + } + } + handleProgressDetails() + handleDeletedPodNotInProgressDetails() return completedCount, nil } diff --git a/controllers/apps/operations/ops_progress_util_test.go b/controllers/apps/operations/ops_progress_util_test.go index 19d5a9dbf..5f0ca75a7 100644 --- a/controllers/apps/operations/ops_progress_util_test.go +++ b/controllers/apps/operations/ops_progress_util_test.go @@ -1,23 +1,25 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( - "fmt" "time" . "github.com/onsi/ginkgo/v2" @@ -45,7 +47,7 @@ var _ = Describe("Ops ProgressDetails", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -81,7 +83,7 @@ var _ = Describe("Ops ProgressDetails", func() { By("mock one pod of StatefulSet to update successfully") testk8s.RemovePodFinalizer(ctx, testCtx, pod) - testapps.MockConsensusComponentStsPod(testCtx, nil, clusterName, consensusComp, + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, pod.Name, "leader", "ReadWrite") _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) @@ -92,7 +94,7 @@ var _ = Describe("Ops ProgressDetails", func() { testProgressDetailsWithStatelessPodUpdating := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource) { By("create a new pod") newPodName := "busybox-" + testCtx.GetRandomStr() - testapps.MockStatelessPod(testCtx, nil, clusterName, statelessComp, newPodName) + testapps.MockStatelessPod(&testCtx, nil, clusterName, statelessComp, newPodName) newPod := &corev1.Pod{} Expect(k8sClient.Get(ctx, client.ObjectKey{Name: newPodName, Namespace: testCtx.DefaultNamespace}, newPod)).Should(Succeed()) _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) @@ -111,16 +113,14 @@ var _ = Describe("Ops ProgressDetails", func() { } Context("Test Ops ProgressDetails", func() { - - It("Test Ops ProgressDetails", func() { + It("Test Ops ProgressDetails for rolling update", func() { By("init operations resources ") reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) By("create restart ops and pods of consensus component") opsRes.OpsRequest = createRestartOpsObj(clusterName, "restart-"+randomStr) - mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp, statelessComp) // appsv1alpha1.RebootingPhase - // TODO: add RebootingPhase status condition + mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp, statelessComp) podList := initConsensusPods(ctx, k8sClient, opsRes, clusterName) By("mock restart OpsRequest is Running") @@ -133,43 +133,65 @@ var _ = Describe("Ops ProgressDetails", func() { By("test the progressDetails when stateless pod updates during restart operation") Expect(opsRes.OpsRequest.Status.Components[statelessComp].Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) // appsv1alpha1.RebootingPhase - // TODO: check RebootingPhase status condition testProgressDetailsWithStatelessPodUpdating(reqCtx, opsRes) + }) + + It("Test Ops ProgressDetails with horizontally scaling replicas", func() { + By("init operations resources ") + reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) + podList := initConsensusPods(ctx, k8sClient, opsRes, clusterName) + By("create horizontalScaling operation to test the progressDetails when scaling down the replicas") - opsRes.OpsRequest = createHorizontalScaling(clusterName, 1) + opsRes.OpsRequest = createHorizontalScaling(clusterName, 2) mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp) // appsv1alpha1.HorizontalScalingPhase - // TODO: add HorizontalScalingPhase status condition initClusterForOps(opsRes) By("mock HorizontalScaling OpsRequest phase is running") - _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) // do h-scale action _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) - By("mock the pod is terminating") - pod := &podList[0] - pod.Kind = constant.PodKind - testk8s.MockPodIsTerminating(ctx, testCtx, pod) + By("mock the pod is terminating, pod[0] is target pod to delete. and mock pod[1] is failed and deleted by stateful controller") + for i := 0; i < 2; i++ { + pod := &podList[i] + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) + Expect(getProgressDetailStatus(opsRes, consensusComp, pod)).Should(Equal(appsv1alpha1.ProcessingProgressStatus)) + + } + By("mock the target pod is deleted and progressDetail status should be succeed") + targetPod := &podList[0] + testk8s.RemovePodFinalizer(ctx, testCtx, targetPod) _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(getProgressDetailStatus(opsRes, consensusComp, pod)).Should(Equal(appsv1alpha1.ProcessingProgressStatus)) + Expect(getProgressDetailStatus(opsRes, consensusComp, targetPod)).Should(Equal(appsv1alpha1.SucceedProgressStatus)) + Expect(opsRes.OpsRequest.Status.Progress).Should(Equal("1/2")) - By("mock the pod is deleted and progressDetail status should be succeed") + By("mock the pod[1] to re-create") + pod := &podList[1] testk8s.RemovePodFinalizer(ctx, testCtx, pod) + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, + pod.Name, "Follower", "ReadWrite") + // expect the progress is 2/2 _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(getProgressDetailStatus(opsRes, consensusComp, pod)).Should(Equal(appsv1alpha1.SucceedProgressStatus)) - Expect(opsRes.OpsRequest.Status.Progress).Should(Equal("1/2")) + Expect(getProgressDetailStatus(opsRes, consensusComp, targetPod)).Should(Equal(appsv1alpha1.SucceedProgressStatus)) + Expect(opsRes.OpsRequest.Status.Progress).Should(Equal("2/2")) By("create horizontalScaling operation to test the progressDetails when scaling up the replicas ") initClusterForOps(opsRes) expectClusterComponentReplicas := int32(2) - Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func() { - opsRes.Cluster.Spec.ComponentSpecs[1].Replicas = expectClusterComponentReplicas + Expect(testapps.ChangeObj(&testCtx, opsRes.Cluster, func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs[1].Replicas = expectClusterComponentReplicas })).ShouldNot(HaveOccurred()) + // ops will use the startTimestamp to make decision, start time should not equal the pod createTime during testing. + time.Sleep(time.Second) opsRes.OpsRequest = createHorizontalScaling(clusterName, 3) + mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp, statelessComp) // update ops phase to Running first _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) @@ -179,23 +201,20 @@ var _ = Describe("Ops ProgressDetails", func() { Expect(err).ShouldNot(HaveOccurred()) By("test the progressDetails when scaling up replicas") - podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusComp, 0) - testapps.MockConsensusComponentStsPod(testCtx, nil, clusterName, consensusComp, - podName, "leader", "ReadWrite") - pod = &corev1.Pod{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: podName, Namespace: testCtx.DefaultNamespace}, pod)).Should(Succeed()) + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, + targetPod.Name, "leader", "ReadWrite") + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: targetPod.Name, Namespace: testCtx.DefaultNamespace}, targetPod)).Should(Succeed()) _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(getProgressDetailStatus(opsRes, consensusComp, pod)).Should(Equal(appsv1alpha1.SucceedProgressStatus)) + Expect(getProgressDetailStatus(opsRes, consensusComp, targetPod)).Should(Equal(appsv1alpha1.SucceedProgressStatus)) Expect(opsRes.OpsRequest.Status.Progress).Should(Equal("1/1")) }) - }) }) func getProgressDetailStatus(opsRes *OpsResource, componentName string, pod *corev1.Pod) appsv1alpha1.ProgressStatus { - objectKey := GetProgressObjectKey(pod.Kind, pod.Name) + objectKey := getProgressObjectKey(pod.Kind, pod.Name) progressDetails := opsRes.OpsRequest.Status.Components[componentName].ProgressDetails - progressDetail := FindStatusProgressDetail(progressDetails, objectKey) + progressDetail := findStatusProgressDetail(progressDetails, objectKey) var status appsv1alpha1.ProgressStatus if progressDetail != nil { status = progressDetail.Status diff --git a/controllers/apps/operations/ops_util.go b/controllers/apps/operations/ops_util.go index b097ee6f5..497f46a8a 100644 --- a/controllers/apps/operations/ops_util.go +++ b/controllers/apps/operations/ops_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -42,10 +45,9 @@ type handleStatusProgressWithComponent func(reqCtx intctrlutil.RequestCtx, type handleReconfigureOpsStatus func(cmStatus *appsv1alpha1.ConfigurationStatus) error -// ReconcileActionWithComponentOps will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed. -// if OpsRequest.spec.componentOps is not null, you can use it to OpsBehaviour.ReconcileAction. -// return the OpsRequest.status.phase -func ReconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, +// reconcileActionWithComponentOps will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed. +// the common function to reconcile opsRequest status when the opsRequest will affect the lifecycle of the components. +func reconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource, opsMessageKey string, @@ -55,7 +57,7 @@ func ReconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, return "", 0, nil } opsRequestPhase := appsv1alpha1.OpsRunningPhase - clusterDef, err := GetClusterDefByName(reqCtx.Ctx, cli, + clusterDef, err := getClusterDefByName(reqCtx.Ctx, cli, opsRes.Cluster.Spec.ClusterDefRef) if err != nil { return opsRequestPhase, 0, err @@ -78,7 +80,7 @@ func ReconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, if opsRequest.Status.Components == nil { opsRequest.Status.Components = map[string]appsv1alpha1.OpsRequestComponentStatus{} } - opsIsCompleted := opsRequestIsComponent(*opsRes) + opsIsCompleted := opsRequestHasProcessed(*opsRes) for k, v := range opsRes.Cluster.Status.Components { if _, ok = componentNameMap[k]; !ok && !checkAllClusterComponent { continue @@ -111,17 +113,17 @@ func ReconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, opsRequest.Status.Components[k] = compStatus } opsRequest.Status.Progress = fmt.Sprintf("%d/%d", completedProgressCount, expectProgressCount) - if !reflect.DeepEqual(opsRequest.Status, oldOpsRequestStatus) { + if !reflect.DeepEqual(opsRequest.Status, *oldOpsRequestStatus) { if err = cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil { return opsRequestPhase, 0, err } } - if opsRes.ToClusterPhase == opsRes.Cluster.Status.Phase { - // wait for the cluster to finish processing ops. + // check if the cluster has applied the changes of the opsRequest and wait for the cluster to finish processing the ops. + if opsRes.ToClusterPhase == opsRes.Cluster.Status.Phase || + opsRes.Cluster.Status.ObservedGeneration < opsRes.OpsRequest.Status.ClusterGeneration { return opsRequestPhase, 0, nil } - // TODO: judge whether ops is Failed according to whether progressDetail has failed pods. - // now we check the ops is Failed by the component phase, it may be not accurate during h-scale replicas. + if isFailed { return appsv1alpha1.OpsFailedPhase, 0, nil } @@ -131,14 +133,14 @@ func ReconcileActionWithComponentOps(reqCtx intctrlutil.RequestCtx, return appsv1alpha1.OpsSucceedPhase, 0, nil } -// opsRequestIsComponent checks if the opsRequest is completed. -func opsRequestIsComponent(opsRes OpsResource) bool { +// opsRequestHasProcessed checks if the opsRequest has been processed. +func opsRequestHasProcessed(opsRes OpsResource) bool { return opsRes.ToClusterPhase != opsRes.Cluster.Status.Phase && opsRes.Cluster.Status.ObservedGeneration >= opsRes.OpsRequest.Status.ClusterGeneration } -// GetClusterDefByName gets the ClusterDefinition object by the name. -func GetClusterDefByName(ctx context.Context, cli client.Client, clusterDefName string) (*appsv1alpha1.ClusterDefinition, error) { +// getClusterDefByName gets the ClusterDefinition object by the name. +func getClusterDefByName(ctx context.Context, cli client.Client, clusterDefName string) (*appsv1alpha1.ClusterDefinition, error) { clusterDef := &appsv1alpha1.ClusterDefinition{} if err := cli.Get(ctx, client.ObjectKey{Name: clusterDefName}, clusterDef); err != nil { return nil, err @@ -146,11 +148,7 @@ func GetClusterDefByName(ctx context.Context, cli client.Client, clusterDefName return clusterDef, nil } -// opsRequestIsCompleted checks if OpsRequest is completed -func opsRequestIsCompleted(phase appsv1alpha1.OpsPhase) bool { - return slices.Index([]appsv1alpha1.OpsPhase{appsv1alpha1.OpsFailedPhase, appsv1alpha1.OpsSucceedPhase}, phase) != -1 -} - +// PatchOpsStatusWithOpsDeepCopy patches OpsRequest.status with the deepCopy opsRequest. func PatchOpsStatusWithOpsDeepCopy(ctx context.Context, cli client.Client, opsRes *OpsResource, @@ -165,16 +163,16 @@ func PatchOpsStatusWithOpsDeepCopy(ctx context.Context, continue } opsRequest.SetStatusCondition(*v) - // provide an event + // emit an event eventType := corev1.EventTypeNormal if phase == appsv1alpha1.OpsFailedPhase { eventType = corev1.EventTypeWarning } opsRes.Recorder.Event(opsRequest, eventType, v.Reason, v.Message) } - if opsRequestIsCompleted(phase) { + if opsRequest.IsComplete(phase) { opsRequest.Status.CompletionTimestamp = metav1.Time{Time: time.Now()} - // when OpsRequest is completed, do it + // when OpsRequest is completed, remove it from annotation if err := DeleteOpsRequestAnnotationInCluster(ctx, cli, opsRes); err != nil { return err } @@ -209,23 +207,12 @@ func patchOpsHandlerNotSupported(ctx context.Context, cli client.Client, opsRes return PatchOpsStatus(ctx, cli, opsRes, appsv1alpha1.OpsFailedPhase, condition) } -// PatchValidateErrorCondition patches ValidateError condition to the OpsRequest.status.conditions. -func PatchValidateErrorCondition(ctx context.Context, cli client.Client, opsRes *OpsResource, errMessage string) error { +// patchValidateErrorCondition patches ValidateError condition to the OpsRequest.status.conditions. +func patchValidateErrorCondition(ctx context.Context, cli client.Client, opsRes *OpsResource, errMessage string) error { condition := appsv1alpha1.NewValidateFailedCondition(appsv1alpha1.ReasonValidateFailed, errMessage) return PatchOpsStatus(ctx, cli, opsRes, appsv1alpha1.OpsFailedPhase, condition) } -// getOpsRequestNameFromAnnotation gets OpsRequest.name from cluster.annotations -func getOpsRequestNameFromAnnotation(cluster *appsv1alpha1.Cluster, opsType appsv1alpha1.OpsType) *string { - opsRequestSlice, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) - for _, v := range opsRequestSlice { - if v.Type == opsType { - return &v.Name - } - } - return nil -} - // GetOpsRecorderFromSlice gets OpsRequest recorder from slice by target cluster phase func GetOpsRecorderFromSlice(opsRequestSlice []appsv1alpha1.OpsRecorder, opsRequestName string) (int, appsv1alpha1.OpsRecorder) { @@ -282,8 +269,8 @@ func patchClusterStatusAndRecordEvent(reqCtx intctrlutil.RequestCtx, return nil } -// DeleteOpsRequestAnnotationInCluster when OpsRequest.status.phase is Succeed or Failed -// we should delete the OpsRequest Annotation in cluster, unlock cluster +// DeleteOpsRequestAnnotationInCluster when OpsRequest.status.phase is Succeeded or Failed +// we should remove the OpsRequest Annotation of cluster, then unlock cluster func DeleteOpsRequestAnnotationInCluster(ctx context.Context, cli client.Client, opsRes *OpsResource) error { var ( opsRequestSlice []appsv1alpha1.OpsRecorder @@ -357,9 +344,9 @@ func updateReconfigureStatusByCM(reconfiguringStatus *appsv1alpha1.Reconfiguring // // NOTES: // opsStatus describes status of OpsRequest. -// reconfiguringStatus describes status of reconfiguring operation, which contains multi configuration templates. -// cmStatus describes status of configmap, it is uniquely associated with a configuration template, which contains multi key, each key represents name of a configuration file. -// execStatus describes the result of the execution of the state machine, which is designed to solve how to do the reconfiguring operation, such as whether to restart, how to send a signal to the process. +// reconfiguringStatus describes status of reconfiguring operation, which contains multiple configuration templates. +// cmStatus describes status of configmap, it is uniquely associated with a configuration template, which contains multiple keys, each key is name of a configuration file. +// execStatus describes the result of the execution of the state machine, which is designed to solve how to conduct the reconfiguring operation, such as whether to restart, how to send a signal to the process. func patchReconfigureOpsStatus( opsRes *OpsResource, tplName string, @@ -375,7 +362,7 @@ func patchReconfigureOpsStatus( return updateReconfigureStatusByCM(reconfiguringStatus, tplName, handleReconfigureStatus) } -// getSlowestReconfiguringProgress calculate the progress of the reconfiguring operations. +// getSlowestReconfiguringProgress gets the progress of the reconfiguring operations. func getSlowestReconfiguringProgress(status []appsv1alpha1.ConfigurationStatus) string { slowest := appsv1alpha1.ConfigurationStatus{ SucceedCount: math.MaxInt32, @@ -389,3 +376,32 @@ func getSlowestReconfiguringProgress(status []appsv1alpha1.ConfigurationStatus) } return fmt.Sprintf("%d/%d", slowest.SucceedCount, slowest.ExpectedCount) } + +func getTargetResourcesOfLastComponent(lastConfiguration appsv1alpha1.LastConfiguration, compName string, resourceKey appsv1alpha1.ComponentResourceKey) []string { + lastComponentConfigs := lastConfiguration.Components[compName] + return lastComponentConfigs.TargetResources[resourceKey] +} + +// cancelComponentOps the common function to cancel th opsRequest which updates the component attributes. +func cancelComponentOps(ctx context.Context, + cli client.Client, + opsRes *OpsResource, + updateComp func(lastConfig *appsv1alpha1.LastComponentConfiguration, comp *appsv1alpha1.ClusterComponentSpec) error) error { + opsRequest := opsRes.OpsRequest + lastCompInfos := opsRequest.Status.LastConfiguration.Components + if lastCompInfos == nil { + return nil + } + for index, comp := range opsRes.Cluster.Spec.ComponentSpecs { + lastConfig, ok := lastCompInfos[comp.Name] + if !ok { + continue + } + if err := updateComp(&lastConfig, &comp); err != nil { + return err + } + opsRes.Cluster.Spec.ComponentSpecs[index] = comp + lastCompInfos[comp.Name] = lastConfig + } + return cli.Update(ctx, opsRes.Cluster) +} diff --git a/controllers/apps/operations/ops_util_test.go b/controllers/apps/operations/ops_util_test.go index eb2352d3a..d95a3bea9 100644 --- a/controllers/apps/operations/ops_util_test.go +++ b/controllers/apps/operations/ops_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -37,7 +40,7 @@ var _ = Describe("OpsUtil functions", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -64,7 +67,7 @@ var _ = Describe("OpsUtil functions", func() { By("Test the functions in ops_util.go") opsRes.OpsRequest = createHorizontalScaling(clusterName, 1) - Expect(PatchValidateErrorCondition(ctx, k8sClient, opsRes, "validate error")).Should(Succeed()) + Expect(patchValidateErrorCondition(ctx, k8sClient, opsRes, "validate error")).Should(Succeed()) Expect(patchOpsHandlerNotSupported(ctx, k8sClient, opsRes)).Should(Succeed()) Expect(isOpsRequestFailedPhase(appsv1alpha1.OpsFailedPhase)).Should(BeTrue()) Expect(PatchClusterNotFound(ctx, k8sClient, opsRes)).Should(Succeed()) diff --git a/controllers/apps/operations/reconfigure.go b/controllers/apps/operations/reconfigure.go index 56b39928e..4aa9759f5 100644 --- a/controllers/apps/operations/reconfigure.go +++ b/controllers/apps/operations/reconfigure.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -51,7 +54,6 @@ func (r *reconfigureAction) ActionStartedCondition(opsRequest *appsv1alpha1.OpsR return appsv1alpha1.NewReconfigureCondition(opsRequest) } -// SaveLastConfiguration this operation can not change in Cluster.spec. func (r *reconfigureAction) SaveLastConfiguration(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error { return nil } @@ -291,7 +293,7 @@ func (r *reconfigureAction) doMergeAndPersist(reqCtx intctrlutil.RequestCtx, configSpec := foundConfigSpec(config.Name) if configSpec == nil { return processMergedFailed(resource, true, - cfgcore.MakeError("failed to reconfigure, not exist config[%s], all configs: %v", config.Name, getConfigSpecName(configSpecs))) + cfgcore.MakeError("failed to reconfigure, not existed config[%s], all configs: %v", config.Name, getConfigSpecName(configSpecs))) } if len(configSpec.ConfigConstraintRef) == 0 { return processMergedFailed(resource, true, diff --git a/controllers/apps/operations/reconfigure_test.go b/controllers/apps/operations/reconfigure_test.go index 03e828292..3ab43ffce 100644 --- a/controllers/apps/operations/reconfigure_test.go +++ b/controllers/apps/operations/reconfigure_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -43,7 +46,7 @@ var _ = Describe("Reconfigure OpsRequest", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. diff --git a/controllers/apps/operations/reconfigure_util.go b/controllers/apps/operations/reconfigure_util.go index 829403f17..4550e7cce 100644 --- a/controllers/apps/operations/reconfigure_util.go +++ b/controllers/apps/operations/reconfigure_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -37,7 +40,7 @@ type reconfiguringResult struct { err error } -// updateCfgParams merge parameters of the config into the configmap, and verify final configuration file. +// updateCfgParams merges parameters of the config into the configmap, and verifies final configuration file. func updateCfgParams(config appsv1alpha1.Configuration, tpl appsv1alpha1.ComponentConfigSpec, cmKey client.ObjectKey, @@ -92,7 +95,7 @@ func persistCfgCM(cmObj *corev1.ConfigMap, newCfg map[string]string, cli client. if cmObj.Annotations == nil { cmObj.Annotations = make(map[string]string) } - cmObj.Annotations[constant.LastAppliedOpsCRAnnotation] = opsCrName + cmObj.Annotations[constant.LastAppliedOpsCRAnnotationKey] = opsCrName cfgcore.SetParametersUpdateSource(cmObj, constant.ReconfigureUserSource) return cli.Patch(ctx, cmObj, patch) } diff --git a/controllers/apps/operations/reconfigure_util_test.go b/controllers/apps/operations/reconfigure_util_test.go index 887a7246f..6a44d88d8 100644 --- a/controllers/apps/operations/reconfigure_util_test.go +++ b/controllers/apps/operations/reconfigure_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations diff --git a/controllers/apps/operations/restart.go b/controllers/apps/operations/restart.go index 342ab10f6..db5f4fcc6 100644 --- a/controllers/apps/operations/restart.go +++ b/controllers/apps/operations/restart.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -36,10 +39,10 @@ var _ OpsHandler = restartOpsHandler{} func init() { restartBehaviour := OpsBehaviour{ - // if cluster is Abnormal or Failed, new opsRequest may can repair it. + // if cluster is Abnormal or Failed, new opsRequest may repair it. // TODO: we should add "force" flag for these opsRequest. - FromClusterPhases: appsv1alpha1.GetClusterTerminalPhases(), - ToClusterPhase: appsv1alpha1.SpecReconcilingClusterPhase, // appsv1alpha1.RebootingPhase, + FromClusterPhases: appsv1alpha1.GetClusterUpRunningPhases(), + ToClusterPhase: appsv1alpha1.SpecReconcilingClusterPhase, OpsHandler: restartOpsHandler{}, MaintainClusterPhaseBySelf: true, ProcessingReasonInClusterCondition: ProcessingReasonRestarting, @@ -67,9 +70,9 @@ func (r restartOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli client.Clie } // ReconcileAction will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed. -// the Reconcile function for volume expansion opsRequest. +// the Reconcile function for restart opsRequest. func (r restartOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "restart", handleComponentStatusProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "restart", handleComponentStatusProgress) } // GetRealAffectedComponentMap gets the real affected component map for the operation @@ -77,7 +80,7 @@ func (r restartOpsHandler) GetRealAffectedComponentMap(opsRequest *appsv1alpha1. return realAffectedComponentMap(opsRequest.Spec.GetRestartComponentNameSet()) } -// SaveLastConfiguration this operation only restart the pods of the component, no changes in Cluster.spec. +// SaveLastConfiguration this operation only restart the pods of the component, no changes for Cluster.spec. // empty implementation here. func (r restartOpsHandler) SaveLastConfiguration(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error { return nil diff --git a/controllers/apps/operations/restart_test.go b/controllers/apps/operations/restart_test.go index 90517b9a1..f81d202c9 100644 --- a/controllers/apps/operations/restart_test.go +++ b/controllers/apps/operations/restart_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -19,7 +22,6 @@ package operations import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -38,7 +40,7 @@ var _ = Describe("Restart OpsRequest", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -59,11 +61,18 @@ var _ = Describe("Restart OpsRequest", func() { AfterEach(cleanEnv) Context("Test OpsRequest", func() { - It("Test restart OpsRequest", func() { + var ( + opsRes *OpsResource + cluster *appsv1alpha1.Cluster + reqCtx intctrlutil.RequestCtx + ) + BeforeEach(func() { By("init operations resources ") - reqCtx := intctrlutil.RequestCtx{Ctx: testCtx.Ctx} - opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) + opsRes, _, cluster = initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) + reqCtx = intctrlutil.RequestCtx{Ctx: testCtx.Ctx} + }) + It("Test restart OpsRequest", func() { By("create Restart opsRequest") opsRes.OpsRequest = createRestartOpsObj(clusterName, "restart-ops-"+randomStr) mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp, statelessComp) @@ -74,8 +83,8 @@ var _ = Describe("Restart OpsRequest", func() { Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) By("test restart action and reconcile function") - testapps.MockConsensusComponentStatefulSet(testCtx, clusterName, consensusComp) - testapps.MockStatelessComponentDeploy(testCtx, clusterName, statelessComp) + testapps.MockConsensusComponentStatefulSet(&testCtx, clusterName, consensusComp) + testapps.MockStatelessComponentDeploy(&testCtx, clusterName, statelessComp) rHandler := restartOpsHandler{} _ = rHandler.Action(reqCtx, k8sClient, opsRes) @@ -87,6 +96,21 @@ var _ = Describe("Restart OpsRequest", func() { Expect(len(h.GetRealAffectedComponentMap(opsRes.OpsRequest))).Should(Equal(2)) }) + It("expect failed when cluster is stopped", func() { + By("mock cluster is stopped") + Expect(testapps.ChangeObjStatus(&testCtx, cluster, func() { + cluster.Status.Phase = appsv1alpha1.StoppedClusterPhase + })).Should(Succeed()) + By("create Restart opsRequest") + opsRes.OpsRequest = createRestartOpsObj(clusterName, "restart-ops-"+randomStr) + _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) + Expect(err).Should(HaveOccurred()) + Expect(err.Error()).Should(ContainSubstring("OpsRequest.spec.type=Restart is forbidden when Cluster.status.phase=Stopped")) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(opsRes.OpsRequest), + func(g Gomega, fetched *appsv1alpha1.OpsRequest) { + g.Expect(fetched.Status.Phase).To(Equal(appsv1alpha1.OpsFailedPhase)) + })).Should(Succeed()) + }) }) }) diff --git a/controllers/apps/operations/start.go b/controllers/apps/operations/start.go index 36b0fdb2a..508a4cb3c 100644 --- a/controllers/apps/operations/start.go +++ b/controllers/apps/operations/start.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -61,7 +64,7 @@ func (start StartOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli client.Cl if replicasOfSnapshot == 0 { continue } - // only reset the component which replicas number is 0 + // only reset the component whose replicas number is 0 if v.Replicas == 0 { cluster.Spec.ComponentSpecs[i].Replicas = replicasOfSnapshot } @@ -90,7 +93,7 @@ func (start StartOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, int32, error) { return handleComponentProgressForScalingReplicas(reqCtx, cli, opsRes, pgRes, compStatus, getExpectReplicas) } - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) } // SaveLastConfiguration records last configuration to the OpsRequest.status.lastConfiguration diff --git a/controllers/apps/operations/start_test.go b/controllers/apps/operations/start_test.go index 1012703e9..3614e9cf8 100644 --- a/controllers/apps/operations/start_test.go +++ b/controllers/apps/operations/start_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -42,7 +45,7 @@ var _ = Describe("Start OpsRequest", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. diff --git a/controllers/apps/operations/stop.go b/controllers/apps/operations/stop.go index 00d027923..a8ef81b5d 100644 --- a/controllers/apps/operations/stop.go +++ b/controllers/apps/operations/stop.go @@ -1,25 +1,30 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( + "context" "encoding/json" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -84,9 +89,17 @@ func (stop StopOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli cl opsRes *OpsResource, pgRes progressResource, compStatus *appsv1alpha1.OpsRequestComponentStatus) (int32, int32, error) { - return handleComponentProgressForScalingReplicas(reqCtx, cli, opsRes, pgRes, compStatus, getExpectReplicas) + expectProgressCount, completedCount, err := handleComponentProgressForScalingReplicas(reqCtx, cli, opsRes, pgRes, compStatus, getExpectReplicas) + if err != nil { + return expectProgressCount, completedCount, err + } + // TODO: delete the configmaps of the cluster should be removed from the opsRequest after refactor. + if err := deleteConfigMaps(reqCtx.Ctx, cli, opsRes.Cluster); err != nil { + return expectProgressCount, completedCount, err + } + return expectProgressCount, completedCount, nil } - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "", handleComponentProgress) } // SaveLastConfiguration records last configuration to the OpsRequest.status.lastConfiguration @@ -125,3 +138,11 @@ func getCompMapFromLastConfiguration(opsRequest *appsv1alpha1.OpsRequest) realAf } return realChangedMap } + +func deleteConfigMaps(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster) error { + inNS := client.InNamespace(cluster.Namespace) + ml := client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.GetName(), + } + return cli.DeleteAllOf(ctx, &corev1.ConfigMap{}, inNS, ml) +} diff --git a/controllers/apps/operations/stop_test.go b/controllers/apps/operations/stop_test.go index 9a2fca1ea..8b1fbe589 100644 --- a/controllers/apps/operations/stop_test.go +++ b/controllers/apps/operations/stop_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -39,7 +42,7 @@ var _ = Describe("Stop OpsRequest", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. diff --git a/controllers/apps/operations/suite_test.go b/controllers/apps/operations/suite_test.go index 6edd7760a..54d9cc1f9 100644 --- a/controllers/apps/operations/suite_test.go +++ b/controllers/apps/operations/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -118,6 +121,8 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) + appsv1alpha1.RegisterWebhookManager(k8sManager) + testCtx = testutil.NewDefaultTestContext(ctx, k8sClient, testEnv) go func() { @@ -139,7 +144,7 @@ var _ = AfterSuite(func() { func initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName string) (*OpsResource, *appsv1alpha1.ClusterDefinition, *appsv1alpha1.Cluster) { - clusterDef, _, clusterObject := testapps.InitClusterWithHybridComps(testCtx, clusterDefinitionName, + clusterDef, _, clusterObject := testapps.InitClusterWithHybridComps(&testCtx, clusterDefinitionName, clusterVersionName, clusterName, statelessComp, statefulComp, consensusComp) opsRes := &OpsResource{ Cluster: clusterObject, @@ -166,7 +171,7 @@ func initOperationsResources(clusterDefinitionName, func initConsensusPods(ctx context.Context, cli client.Client, opsRes *OpsResource, clusterName string) []corev1.Pod { // mock the pods of consensusSet component - testapps.MockConsensusComponentPods(testCtx, nil, clusterName, consensusComp) + testapps.MockConsensusComponentPods(&testCtx, nil, clusterName, consensusComp) podList, err := util.GetComponentPodList(ctx, cli, *opsRes.Cluster, consensusComp) Expect(err).Should(Succeed()) // the opsRequest will use startTime to check some condition. diff --git a/controllers/apps/operations/type.go b/controllers/apps/operations/type.go index 2e5dab449..059116245 100644 --- a/controllers/apps/operations/type.go +++ b/controllers/apps/operations/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -28,27 +31,24 @@ import ( ) type OpsHandler interface { - // Action The action running time should be short. if it fails, it will be reconciled by the OpsRequest controller. - // Do not patch OpsRequest status in this function with k8s client, just modify the status variable of ops. - // The opsRequest controller will unify the patch it to the k8s apiServer. + // Action The action duration should be short. if it fails, it will be reconciled by the OpsRequest controller. + // Do not patch OpsRequest status in this function with k8s client, just modify the status of ops. + // The opsRequest controller will patch it to the k8s apiServer. Action(reqCtx intctrlutil.RequestCtx, cli client.Client, opsResource *OpsResource) error // ReconcileAction loops till the operation is completed. // return OpsRequest.status.phase and requeueAfter time. ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsResource *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) - // ActionStartedCondition append to OpsRequest.status.conditions when start performing Action function + // ActionStartedCondition appends to OpsRequest.status.conditions when start performing Action function ActionStartedCondition(opsRequest *appsv1alpha1.OpsRequest) *metav1.Condition // SaveLastConfiguration saves last configuration to the OpsRequest.status.lastConfiguration, - // and this method will be executed together when opsRequest to running. + // and this method will be executed together when opsRequest in running. SaveLastConfiguration(reqCtx intctrlutil.RequestCtx, cli client.Client, opsResource *OpsResource) error - // GetRealAffectedComponentMap returns a changed configuration componentName map by - // compared current configuration with the last configuration. - // we only changed the component status of cluster.status to the ToClusterPhase - // of OpsBehaviour, which component name is in the returned componentName map. - // Note: if the operation will not modify the Spec struct of the component workload, - // GetRealAffectedComponentMap function should return nil unless phase management of cluster and components - // is implemented at ReconcileAction function. + // GetRealAffectedComponentMap returns a component map that is actually affected by the opsRequest. + // when MaintainClusterPhaseBySelf of the opsBehaviour is true, + // will change the phase of the component to Updating after Action is done which exists in this map. + // Deprecated: will be removed soon. GetRealAffectedComponentMap(opsRequest *appsv1alpha1.OpsRequest) realAffectedComponentMap } @@ -61,15 +61,19 @@ type OpsBehaviour struct { ToClusterPhase appsv1alpha1.ClusterPhase // MaintainClusterPhaseBySelf indicates whether the operation will maintain cluster/component phase by itself. - // Generally, the cluster/component phase will be maintained by cluster controller, but if your operation will not update - // StatefulSet/Deployment by Cluster controller and make pod to rebuilt, you need to maintain the cluster/component phase yourself. + // Generally, the cluster/component phase will be maintained by cluster controller, but if the operation will not update + // StatefulSet/Deployment by Cluster controller and make pod rebuilt, maintain the cluster/component phase by self. MaintainClusterPhaseBySelf bool // ProcessingReasonInClusterCondition indicates the reason of the condition that type is "OpsRequestProcessed" in Cluster.Status.Conditions and // is only valid when ToClusterPhase is not empty. it will indicate what operation the cluster is doing and - // will be displayed of "kblic cluster list". + // will be displayed of "kbcli cluster list". ProcessingReasonInClusterCondition string + // CancelFunc this function defines the cancel action and does not patch/update the opsRequest by client-go in here. + // only update the opsRequest object, then opsRequest controller will update uniformly. + CancelFunc func(reqCtx intctrlutil.RequestCtx, cli client.Client, opsResource *OpsResource) error + OpsHandler OpsHandler } @@ -98,8 +102,6 @@ const ( ProcessingReasonHorizontalScaling = "HorizontalScaling" // ProcessingReasonVerticalScaling is the reason of the "OpsRequestProcessed" condition for the vertical scaling opsRequest processing in cluster. ProcessingReasonVerticalScaling = "VerticalScaling" - // ProcessingReasonVolumeExpanding is the reason of the "OpsRequestProcessed" condition for the volume expansion opsRequest processing in cluster. - ProcessingReasonVolumeExpanding = "VolumeExpanding" // ProcessingReasonStarting is the reason of the "OpsRequestProcessed" condition for the start opsRequest processing in cluster. ProcessingReasonStarting = "Starting" // ProcessingReasonStopping is the reason of the "OpsRequestProcessed" condition for the stop opsRequest processing in cluster. diff --git a/controllers/apps/operations/upgrade.go b/controllers/apps/operations/upgrade.go index e04fc914c..948bb0bb8 100644 --- a/controllers/apps/operations/upgrade.go +++ b/controllers/apps/operations/upgrade.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -60,7 +63,7 @@ func (u upgradeOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli client.Clie // ReconcileAction will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed. // the Reconcile function for upgrade opsRequest. func (u upgradeOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "upgrade", handleComponentStatusProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "upgrade", handleComponentStatusProgress) } // GetRealAffectedComponentMap gets the real affected component map for the operation diff --git a/controllers/apps/operations/upgrade_test.go b/controllers/apps/operations/upgrade_test.go index 871b56fb3..965faf403 100644 --- a/controllers/apps/operations/upgrade_test.go +++ b/controllers/apps/operations/upgrade_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -38,7 +41,7 @@ var _ = Describe("Upgrade OpsRequest", func() { ) const mysqlImageForUpdate = "docker.io/apecloud/apecloud-mysql-server:8.0.30" cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -67,9 +70,9 @@ var _ = Describe("Upgrade OpsRequest", func() { By("create Upgrade Ops") newClusterVersionName := "clusterversion-upgrade-" + randomStr _ = testapps.NewClusterVersionFactory(newClusterVersionName, clusterDefinitionName). - AddComponent(statelessComp).AddContainerShort(testapps.DefaultNginxContainerName, "nginx:1.14.2"). - AddComponent(consensusComp).AddContainerShort(testapps.DefaultMySQLContainerName, mysqlImageForUpdate). - AddComponent(statefulComp).AddContainerShort(testapps.DefaultMySQLContainerName, mysqlImageForUpdate). + AddComponentVersion(statelessComp).AddContainerShort(testapps.DefaultNginxContainerName, "nginx:1.14.2"). + AddComponentVersion(consensusComp).AddContainerShort(testapps.DefaultMySQLContainerName, mysqlImageForUpdate). + AddComponentVersion(statefulComp).AddContainerShort(testapps.DefaultMySQLContainerName, mysqlImageForUpdate). Create(&testCtx).GetObject() ops := testapps.NewOpsRequestObj("upgrade-ops-"+randomStr, testCtx.DefaultNamespace, clusterObject.Name, appsv1alpha1.UpgradeType) diff --git a/controllers/apps/operations/util/common_util.go b/controllers/apps/operations/util/common_util.go index 0db958277..34b983376 100644 --- a/controllers/apps/operations/util/common_util.go +++ b/controllers/apps/operations/util/common_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -69,12 +72,31 @@ func PatchOpsRequestReconcileAnnotation(ctx context.Context, cli client.Client, if opsRequest.Annotations == nil { opsRequest.Annotations = map[string]string{} } - // because many changes may be triggered within one second, if the accuracy is only seconds, the event may be lost. + // because many changes may be triggered within one second, if the accuracy is only in seconds, the event may be lost. // so use nanoseconds to record the time. opsRequest.Annotations[intctrlutil.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano) return cli.Patch(ctx, opsRequest, patch) } +//// PatchOpsRequestReconcileAnnotation2 patches the reconcile annotation to OpsRequest +// func PatchOpsRequestReconcileAnnotation2(ctx context.Context, cli client.Client, namespace string, opsRequestName string, dag *graph.DAG) error { +// opsRequest := &appsv1alpha1.OpsRequest{} +// if err := cli.Get(ctx, client.ObjectKey{Name: opsRequestName, Namespace: namespace}, opsRequest); err != nil { +// return err +// } +// +// opsRequestDeepCopy := opsRequest.DeepCopy() +// if opsRequest.Annotations == nil { +// opsRequest.Annotations = map[string]string{} +// } +// // because many changes may be triggered within one second, if the accuracy is only seconds, the event may be lost. +// // so use nanoseconds to record the time. +// opsRequest.Annotations[intctrlutil.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano) +// +// types.AddVertex4Patch(dag, opsRequest, opsRequestDeepCopy) +// return nil +// } + // GetOpsRequestSliceFromCluster gets OpsRequest slice from cluster annotations. // this records what OpsRequests are running in cluster func GetOpsRequestSliceFromCluster(cluster *appsv1alpha1.Cluster) ([]appsv1alpha1.OpsRecorder, error) { @@ -125,6 +147,35 @@ func MarkRunningOpsRequestAnnotation(ctx context.Context, cli client.Client, clu return nil } +//// MarkRunningOpsRequestAnnotation2 marks reconcile annotation to the OpsRequest which is running in the cluster. +//// then the related OpsRequest can reconcile. +//// Note: if the client-go fetches the Cluster resources from cache, +//// it should record the Cluster.ResourceVersion to check if the Cluster object from client-go is the latest in OpsRequest controller. +//// @return could return ErrNoOps +// func MarkRunningOpsRequestAnnotation2(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, dag *graph.DAG) error { +// var ( +// opsRequestSlice []appsv1alpha1.OpsRecorder +// err error +// ) +// if opsRequestSlice, err = GetOpsRequestSliceFromCluster(cluster); err != nil { +// return err +// } +// // mark annotation for operations +// var notExistOps = map[string]struct{}{} +// for _, v := range opsRequestSlice { +// if err = PatchOpsRequestReconcileAnnotation2(ctx, cli, cluster.Namespace, v.Name, dag); err != nil && !apierrors.IsNotFound(err) { +// return err +// } +// if apierrors.IsNotFound(err) { +// notExistOps[v.Name] = struct{}{} +// } +// } +// if len(notExistOps) != 0 { +// return RemoveClusterInvalidOpsRequestAnnotation2(ctx, cli, cluster, opsRequestSlice, notExistOps) +// } +// return nil +// } + // RemoveClusterInvalidOpsRequestAnnotation deletes the OpsRequest annotation in cluster when the OpsRequest not existing. func RemoveClusterInvalidOpsRequestAnnotation( ctx context.Context, @@ -142,3 +193,21 @@ func RemoveClusterInvalidOpsRequestAnnotation( } return PatchClusterOpsAnnotations(ctx, cli, cluster, newOpsRequestSlice) } + +//// RemoveClusterInvalidOpsRequestAnnotation2 deletes the OpsRequest annotation in cluster when the OpsRequest not existing. +// func RemoveClusterInvalidOpsRequestAnnotation2(ctx context.Context, +// cli client.Client, +// cluster *appsv1alpha1.Cluster, +// opsRequestSlice []appsv1alpha1.OpsRecorder, +// notExistOps map[string]struct{}) error { +// // delete the OpsRequest annotation in cluster when the OpsRequest not existing. +// newOpsRequestSlice := make([]appsv1alpha1.OpsRecorder, 0, len(opsRequestSlice)) +// for _, v := range opsRequestSlice { +// if _, ok := notExistOps[v.Name]; ok { +// continue +// } +// newOpsRequestSlice = append(newOpsRequestSlice, v) +// } +// setOpsRequestToCluster(cluster, opsRequestSlice) +// return nil +// } diff --git a/controllers/apps/operations/util/common_util_test.go b/controllers/apps/operations/util/common_util_test.go index 93333ef87..a93c80227 100644 --- a/controllers/apps/operations/util/common_util_test.go +++ b/controllers/apps/operations/util/common_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -55,7 +58,7 @@ var _ = Describe("OpsRequest Controller", func() { Context("Test OpsRequest", func() { It("Should Test all OpsRequest", func() { - cluster := testapps.CreateConsensusMysqlCluster(testCtx, clusterDefinitionName, + cluster := testapps.CreateConsensusMysqlCluster(&testCtx, clusterDefinitionName, clusterVersionName, clusterName, "consensus", consensusCompName) By("init restart OpsRequest") testOpsName := "restart-" + randomStr diff --git a/controllers/apps/operations/util/suite_test.go b/controllers/apps/operations/util/suite_test.go index 7315aca9e..1789ac7e8 100644 --- a/controllers/apps/operations/util/suite_test.go +++ b/controllers/apps/operations/util/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/controllers/apps/operations/vertical_scaling.go b/controllers/apps/operations/vertical_scaling.go index 1913d98b5..5561de1c7 100644 --- a/controllers/apps/operations/vertical_scaling.go +++ b/controllers/apps/operations/vertical_scaling.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -20,6 +23,7 @@ import ( "reflect" "time" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" @@ -32,13 +36,15 @@ type verticalScalingHandler struct{} var _ OpsHandler = verticalScalingHandler{} func init() { + vsHandler := verticalScalingHandler{} verticalScalingBehaviour := OpsBehaviour{ // if cluster is Abnormal or Failed, new opsRequest may can repair it. // TODO: we should add "force" flag for these opsRequest. FromClusterPhases: appsv1alpha1.GetClusterUpRunningPhases(), ToClusterPhase: appsv1alpha1.SpecReconcilingClusterPhase, - OpsHandler: verticalScalingHandler{}, + OpsHandler: vsHandler, ProcessingReasonInClusterCondition: ProcessingReasonVerticalScaling, + CancelFunc: vsHandler.Cancel, } opsMgr := GetOpsManager() @@ -55,10 +61,19 @@ func (vs verticalScalingHandler) ActionStartedCondition(opsRequest *appsv1alpha1 func (vs verticalScalingHandler) Action(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error { verticalScalingMap := opsRes.OpsRequest.Spec.ToVerticalScalingListToMap() for index, component := range opsRes.Cluster.Spec.ComponentSpecs { - if verticalScaling, ok := verticalScalingMap[component.Name]; ok { + verticalScaling, ok := verticalScalingMap[component.Name] + if !ok { + continue + } + if verticalScaling.Class != "" { + component.ClassDefRef = &appsv1alpha1.ClassDefRef{Class: verticalScaling.Class} + component.Resources = corev1.ResourceRequirements{} + } else { + // clear old class ref + component.ClassDefRef = &appsv1alpha1.ClassDefRef{} component.Resources = verticalScaling.ResourceRequirements - opsRes.Cluster.Spec.ComponentSpecs[index] = component } + opsRes.Cluster.Spec.ComponentSpecs[index] = component } return cli.Update(reqCtx.Ctx, opsRes.Cluster) } @@ -66,7 +81,7 @@ func (vs verticalScalingHandler) Action(reqCtx intctrlutil.RequestCtx, cli clien // ReconcileAction will be performed when action is done and loops till OpsRequest.status.phase is Succeed/Failed. // the Reconcile function for vertical scaling opsRequest. func (vs verticalScalingHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { - return ReconcileActionWithComponentOps(reqCtx, cli, opsRes, "vertical scale", handleComponentStatusProgress) + return reconcileActionWithComponentOps(reqCtx, cli, opsRes, "vertical scale", handleComponentStatusProgress) } // SaveLastConfiguration records last configuration to the OpsRequest.status.lastConfiguration @@ -77,9 +92,13 @@ func (vs verticalScalingHandler) SaveLastConfiguration(reqCtx intctrlutil.Reques if _, ok := componentNameSet[v.Name]; !ok { continue } - lastComponentInfo[v.Name] = appsv1alpha1.LastComponentConfiguration{ + lastConfiguration := appsv1alpha1.LastComponentConfiguration{ ResourceRequirements: v.Resources, } + if v.ClassDefRef != nil { + lastConfiguration.Class = v.ClassDefRef.Class + } + lastComponentInfo[v.Name] = lastConfiguration } opsRes.OpsRequest.Status.LastConfiguration.Components = lastComponentInfo return nil @@ -94,9 +113,18 @@ func (vs verticalScalingHandler) GetRealAffectedComponentMap(opsRequest *appsv1a if !ok { continue } - if !reflect.DeepEqual(currVs.ResourceRequirements, v.ResourceRequirements) { + if !reflect.DeepEqual(currVs.ResourceRequirements, v.ResourceRequirements) || currVs.Class != v.Class { realChangedMap[k] = struct{}{} } } return realChangedMap } + +// Cancel this function defines the cancel verticalScaling action. +func (vs verticalScalingHandler) Cancel(reqCxt intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) error { + return cancelComponentOps(reqCxt.Ctx, cli, opsRes, func(lastConfig *appsv1alpha1.LastComponentConfiguration, comp *appsv1alpha1.ClusterComponentSpec) error { + comp.ClassDefRef = &appsv1alpha1.ClassDefRef{Class: lastConfig.Class} + comp.Resources = lastConfig.ResourceRequirements + return nil + }) +} diff --git a/controllers/apps/operations/vertical_scaling_test.go b/controllers/apps/operations/vertical_scaling_test.go index 393e6e475..713529b1e 100644 --- a/controllers/apps/operations/vertical_scaling_test.go +++ b/controllers/apps/operations/vertical_scaling_test.go @@ -1,33 +1,41 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( + "time" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + testk8s "github.com/apecloud/kubeblocks/internal/testutil/k8s" ) var _ = Describe("VerticalScaling OpsRequest", func() { @@ -39,7 +47,7 @@ var _ = Describe("VerticalScaling OpsRequest", func() { clusterName = "cluster-for-ops-" + randomStr ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -60,16 +68,35 @@ var _ = Describe("VerticalScaling OpsRequest", func() { AfterEach(cleanEnv) Context("Test OpsRequest", func() { - It("Test verticalScaling OpsRequest", func() { + testVerticalScaling := func(verticalScaling []appsv1alpha1.VerticalScaling) { By("init operations resources ") reqCtx := intctrlutil.RequestCtx{Ctx: ctx} opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) By("create VerticalScaling ops") - ops := testapps.NewOpsRequestObj("verticalscaling-ops-"+randomStr, testCtx.DefaultNamespace, + ops := testapps.NewOpsRequestObj("vertical-scaling-ops-"+randomStr, testCtx.DefaultNamespace, clusterName, appsv1alpha1.VerticalScalingType) - ops.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{ + + ops.Spec.VerticalScalingList = verticalScaling + opsRes.OpsRequest = testapps.CreateOpsRequest(ctx, testCtx, ops) + By("test save last configuration and OpsRequest phase is Running") + _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) + Expect(err).ShouldNot(HaveOccurred()) + Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(ops))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) + + By("test vertical scale action function") + vsHandler := verticalScalingHandler{} + Expect(vsHandler.Action(reqCtx, k8sClient, opsRes)).Should(Succeed()) + _, _, err = vsHandler.ReconcileAction(reqCtx, k8sClient, opsRes) + Expect(err).ShouldNot(HaveOccurred()) + + By("test GetRealAffectedComponentMap function") + Expect(len(vsHandler.GetRealAffectedComponentMap(opsRes.OpsRequest))).Should(Equal(1)) + } + + It("vertical scaling by resource", func() { + verticalScaling := []appsv1alpha1.VerticalScaling{ { ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp}, ResourceRequirements: corev1.ResourceRequirements{ @@ -84,20 +111,88 @@ var _ = Describe("VerticalScaling OpsRequest", func() { }, }, } + testVerticalScaling(verticalScaling) + }) + + It("vertical scaling by class", func() { + verticalScaling := []appsv1alpha1.VerticalScaling{ + { + ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp}, + Class: testapps.Class1c1gName, + }, + } + testVerticalScaling(verticalScaling) + }) + + It("cancel vertical scaling opsRequest", func() { + By("init operations resources with CLusterDefinition/ClusterVersion/Hybrid components Cluster/consensus Pods") + reqCtx := intctrlutil.RequestCtx{Ctx: ctx} + opsRes, _, _ := initOperationsResources(clusterDefinitionName, clusterVersionName, clusterName) + podList := initConsensusPods(ctx, k8sClient, opsRes, clusterName) + + By("create VerticalScaling ops") + ops := testapps.NewOpsRequestObj("vertical-scaling-ops-"+randomStr, testCtx.DefaultNamespace, + clusterName, appsv1alpha1.VerticalScalingType) + ops.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{ + { + ComponentOps: appsv1alpha1.ComponentOps{ComponentName: consensusComp}, + ResourceRequirements: corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("400m"), + corev1.ResourceMemory: resource.MustParse("300Mi"), + }, + }, + }, + } opsRes.OpsRequest = testapps.CreateOpsRequest(ctx, testCtx, ops) - By("test save last configuration and OpsRequest phase is Running") - _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) + + By("mock opsRequest is Running") + mockComponentIsOperating(opsRes.Cluster, appsv1alpha1.SpecReconcilingClusterCompPhase, consensusComp) + Expect(testapps.ChangeObjStatus(&testCtx, opsRes.OpsRequest, func() { + opsRes.OpsRequest.Status.Phase = appsv1alpha1.OpsRunningPhase + opsRes.OpsRequest.Status.StartTimestamp = metav1.Time{Time: time.Now()} + })).ShouldNot(HaveOccurred()) + // wait 1 second for checking progress + time.Sleep(time.Second) + reCreatePod := func(pod *corev1.Pod) { + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + testk8s.RemovePodFinalizer(ctx, testCtx, pod) + testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterName, consensusComp, pod.Name, "leader", "ReadWrite") + } + + By("mock podList[0] rolling update successfully by re-creating it") + reCreatePod(&podList[0]) + + By("reconcile opsRequest status") + _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) Expect(err).ShouldNot(HaveOccurred()) - Eventually(testapps.GetOpsRequestPhase(&testCtx, client.ObjectKeyFromObject(ops))).Should(Equal(appsv1alpha1.OpsCreatingPhase)) - By("test vertical scale action function") - vsHandler := verticalScalingHandler{} - Expect(vsHandler.Action(reqCtx, k8sClient, opsRes)).Should(Succeed()) - _, _, err = vsHandler.ReconcileAction(reqCtx, k8sClient, opsRes) - Expect(err == nil).Should(BeTrue()) + By("the progress status of pod[0] should be Succeed ") + progressDetails := opsRes.OpsRequest.Status.Components[consensusComp].ProgressDetails + progressDetail := findStatusProgressDetail(progressDetails, getProgressObjectKey("", podList[0].Name)) + Expect(progressDetail.Status).Should(Equal(appsv1alpha1.SucceedProgressStatus)) - By("test GetRealAffectedComponentMap function") - Expect(len(vsHandler.GetRealAffectedComponentMap(opsRes.OpsRequest))).Should(Equal(1)) + By("cancel verticalScaling opsRequest") + cancelOpsRequest(reqCtx, opsRes, opsRes.OpsRequest.Status.StartTimestamp.Time) + + By("mock podList[0] rolled back successfully by re-creating it") + reCreatePod(&podList[0]) + + By("reconcile opsRequest status after canceling opsRequest and component is Running after rolling update") + mockConsensusCompToRunning(opsRes) + _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) + Expect(err).ShouldNot(HaveOccurred()) + + By("expect for cancelling opsRequest successfully") + opsRequest := opsRes.OpsRequest + Expect(opsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancelledPhase)) + Expect(opsRequest.Status.Progress).Should(Equal("1/1")) + progressDetails = opsRequest.Status.Components[consensusComp].ProgressDetails + Expect(len(progressDetails)).Should(Equal(1)) + progressDetail = findStatusProgressDetail(progressDetails, getProgressObjectKey("", podList[0].Name)) + Expect(progressDetail.Status).Should(Equal(appsv1alpha1.SucceedProgressStatus)) + Expect(progressDetail.Message).Should(ContainSubstring("with rollback")) }) }) }) diff --git a/controllers/apps/operations/volume_expansion.go b/controllers/apps/operations/volume_expansion.go index 4e7f079ac..915145ad8 100644 --- a/controllers/apps/operations/volume_expansion.go +++ b/controllers/apps/operations/volume_expansion.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations @@ -19,10 +22,11 @@ package operations import ( "fmt" "reflect" + "regexp" + "strconv" "time" "github.com/pkg/errors" - "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -37,19 +41,17 @@ type volumeExpansionOpsHandler struct{} var _ OpsHandler = volumeExpansionOpsHandler{} +var pvcNameRegex = regexp.MustCompile("(.*)-([0-9]+)$") + const ( // VolumeExpansionTimeOut volume expansion timeout. VolumeExpansionTimeOut = 30 * time.Minute ) func init() { - // the volume expansion operation only support online expanding now, so this operation not affect the cluster availability. + // the volume expansion operation only supports online expansion now volumeExpansionBehaviour := OpsBehaviour{ - FromClusterPhases: appsv1alpha1.GetClusterUpRunningPhases(), - ToClusterPhase: appsv1alpha1.SpecReconcilingClusterPhase, - MaintainClusterPhaseBySelf: true, - OpsHandler: volumeExpansionOpsHandler{}, - ProcessingReasonInClusterCondition: ProcessingReasonVolumeExpanding, + OpsHandler: volumeExpansionOpsHandler{}, } opsMgr := GetOpsManager() @@ -90,20 +92,17 @@ func (ve volumeExpansionOpsHandler) Action(reqCtx intctrlutil.RequestCtx, cli cl // the Reconcile function for volume expansion opsRequest. func (ve volumeExpansionOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource) (appsv1alpha1.OpsPhase, time.Duration, error) { var ( - opsRequest = opsRes.OpsRequest - // decide whether all pvcs of volumeClaimTemplate are Failed or Succeed - allVCTCompleted = true - requeueAfter time.Duration - err error - opsRequestPhase = appsv1alpha1.OpsRunningPhase - oldOpsRequestStatus = opsRequest.Status.DeepCopy() - oldClusterStatus = opsRes.Cluster.Status.DeepCopy() - expectProgressCount int - succeedProgressCount int + opsRequest = opsRes.OpsRequest + requeueAfter time.Duration + err error + opsRequestPhase = appsv1alpha1.OpsRunningPhase + oldOpsRequestStatus = opsRequest.Status.DeepCopy() + expectProgressCount int + succeedProgressCount int + completedProgressCount int ) patch := client.MergeFrom(opsRequest.DeepCopy()) - clusterPatch := client.MergeFrom(opsRes.Cluster.DeepCopy()) if opsRequest.Status.Components == nil { ve.initComponentStatus(opsRequest) } @@ -112,55 +111,44 @@ func (ve volumeExpansionOpsHandler) ReconcileAction(reqCtx intctrlutil.RequestCt // sync the volumeClaimTemplate status and component phase On the OpsRequest and Cluster. for _, v := range opsRequest.Spec.VolumeExpansionList { compStatus := opsRequest.Status.Components[v.ComponentName] - completedOnComponent := true for _, vct := range v.VolumeClaimTemplates { - succeedCount, expectCount, isCompleted, err := ve.handleVCTExpansionProgress(reqCtx, cli, opsRes, + succeedCount, expectCount, completedCount, err := ve.handleVCTExpansionProgress(reqCtx, cli, opsRes, &compStatus, storageMap, v.ComponentName, vct.Name) if err != nil { return "", requeueAfter, err } expectProgressCount += expectCount succeedProgressCount += succeedCount - if !isCompleted { - requeueAfter = time.Minute - allVCTCompleted = false - completedOnComponent = false - } + completedProgressCount += completedCount } - // when component expand volume completed, do it. - ve.setComponentPhaseForClusterAndOpsRequest(&compStatus, opsRes.Cluster, v.ComponentName, completedOnComponent) opsRequest.Status.Components[v.ComponentName] = compStatus } - opsRequest.Status.Progress = fmt.Sprintf("%d/%d", succeedProgressCount, expectProgressCount) - + if completedProgressCount != expectProgressCount { + requeueAfter = time.Minute + } + opsRequest.Status.Progress = fmt.Sprintf("%d/%d", completedProgressCount, expectProgressCount) // patch OpsRequest.status.components - if !reflect.DeepEqual(oldOpsRequestStatus, opsRequest.Status) { + if !reflect.DeepEqual(*oldOpsRequestStatus, opsRequest.Status) { if err = cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil { return opsRequestPhase, requeueAfter, err } } - // check all pvcs of volumeClaimTemplate are successful - allVCTSucceed := expectProgressCount == succeedProgressCount - if allVCTSucceed { - opsRequestPhase = appsv1alpha1.OpsSucceedPhase - } else if allVCTCompleted { - // all volume claim template volume expansion completed, but allVCTSucceed is false. - // decide the OpsRequest is failed. - opsRequestPhase = appsv1alpha1.OpsFailedPhase - } - - if ve.checkIsTimeOut(opsRequest, allVCTSucceed) { - // if volume expansion timed out, do it - opsRequestPhase = appsv1alpha1.OpsFailedPhase - err = errors.New(fmt.Sprintf("Timed out waiting for volume expansion completed, the timeout is %g minutes", VolumeExpansionTimeOut.Minutes())) - } - - // when opsRequest completed or cluster status is changed, do it - if patchErr := ve.patchClusterStatus(reqCtx, cli, opsRes, opsRequestPhase, oldClusterStatus, clusterPatch); patchErr != nil { - return "", requeueAfter, patchErr + // check all PVCs of volumeClaimTemplate are successful + if expectProgressCount == completedProgressCount { + if expectProgressCount == succeedProgressCount { + opsRequestPhase = appsv1alpha1.OpsSucceedPhase + } else { + opsRequestPhase = appsv1alpha1.OpsFailedPhase + } + } else { + // check whether the volume expansion operation has timed out + if time.Now().After(opsRequest.Status.StartTimestamp.Add(VolumeExpansionTimeOut)) { + // if volume expansion timed out + opsRequestPhase = appsv1alpha1.OpsFailedPhase + err = errors.New(fmt.Sprintf("Timed out waiting for volume expansion to complete, the timeout value is %g minutes", VolumeExpansionTimeOut.Minutes())) + } } - return opsRequestPhase, requeueAfter, err } @@ -198,56 +186,6 @@ func (ve volumeExpansionOpsHandler) SaveLastConfiguration(reqCtx intctrlutil.Req return nil } -// checkIsTimeOut check whether the volume expansion operation has timed out -func (ve volumeExpansionOpsHandler) checkIsTimeOut(opsRequest *appsv1alpha1.OpsRequest, allVCTSucceed bool) bool { - return !allVCTSucceed && time.Now().After(opsRequest.Status.StartTimestamp.Add(VolumeExpansionTimeOut)) -} - -// setClusterComponentPhaseToRunning when component expand volume completed, check whether change the component status. -func (ve volumeExpansionOpsHandler) setComponentPhaseForClusterAndOpsRequest(component *appsv1alpha1.OpsRequestComponentStatus, - cluster *appsv1alpha1.Cluster, - componentName string, - completedOnComponent bool) { - if !completedOnComponent { - return - } - c, ok := cluster.Status.Components[componentName] - if !ok { - return - } - p := c.Phase - if p == appsv1alpha1.SpecReconcilingClusterCompPhase { - p = appsv1alpha1.RunningClusterCompPhase - } - c.Phase = p - cluster.Status.SetComponentStatus(componentName, c) - component.Phase = p -} - -// isExpansionCompleted check the expansion is completed -func (ve volumeExpansionOpsHandler) isExpansionCompleted(phase appsv1alpha1.ProgressStatus) bool { - return slices.Contains([]appsv1alpha1.ProgressStatus{appsv1alpha1.FailedProgressStatus, - appsv1alpha1.SucceedProgressStatus}, phase) -} - -// patchClusterStatus patch cluster status -func (ve volumeExpansionOpsHandler) patchClusterStatus(reqCtx intctrlutil.RequestCtx, - cli client.Client, - opsRes *OpsResource, - opsRequestPhase appsv1alpha1.OpsPhase, - oldClusterStatus *appsv1alpha1.ClusterStatus, - clusterPatch client.Patch) error { - // when the OpsRequest.status.phase is Succeed or Failed, do it - if opsRequestIsCompleted(opsRequestPhase) && opsRes.Cluster.Status.Phase == appsv1alpha1.SpecReconcilingClusterPhase { - opsRes.Cluster.Status.Phase = appsv1alpha1.RunningClusterPhase - } - // if cluster status changed, patch it - if !reflect.DeepEqual(oldClusterStatus, opsRes.Cluster.Status) { - return cli.Status().Patch(reqCtx.Ctx, opsRes.Cluster, clusterPatch) - } - return nil -} - // pvcIsResizing when pvc start resizing, it will set conditions type to Resizing/FileSystemResizePending func (ve volumeExpansionOpsHandler) pvcIsResizing(pvc *corev1.PersistentVolumeClaim) bool { var isResizing bool @@ -271,45 +209,73 @@ func (ve volumeExpansionOpsHandler) getRequestStorageMap(opsRequest *appsv1alpha return storageMap } -// initComponentStatus init status.components for the VolumeExpansion OpsRequest +// initComponentStatus inits status.components for the VolumeExpansion OpsRequest func (ve volumeExpansionOpsHandler) initComponentStatus(opsRequest *appsv1alpha1.OpsRequest) { opsRequest.Status.Components = map[string]appsv1alpha1.OpsRequestComponentStatus{} for _, v := range opsRequest.Spec.VolumeExpansionList { - opsRequest.Status.Components[v.ComponentName] = appsv1alpha1.OpsRequestComponentStatus{ - Phase: appsv1alpha1.SpecReconcilingClusterCompPhase, - } + opsRequest.Status.Components[v.ComponentName] = appsv1alpha1.OpsRequestComponentStatus{} } } -// handleVCTExpansionProgress check whether the pvc of the volume claim template is resizing/expansion succeeded/expansion completed. +// handleVCTExpansionProgress checks whether the pvc of the volume claim template is in (resizing, expansion succeeded, expansion completed). func (ve volumeExpansionOpsHandler) handleVCTExpansionProgress(reqCtx intctrlutil.RequestCtx, cli client.Client, opsRes *OpsResource, compStatus *appsv1alpha1.OpsRequestComponentStatus, storageMap map[string]resource.Quantity, - componentName, vctName string) (succeedCount int, expectCount int, isCompleted bool, err error) { + componentName, vctName string) (int, int, int, error) { + var ( + succeedCount int + expectCount int + completedCount int + err error + ) pvcList := &corev1.PersistentVolumeClaimList{} if err = cli.List(reqCtx.Ctx, pvcList, client.MatchingLabels{ constant.AppInstanceLabelKey: opsRes.Cluster.Name, constant.KBAppComponentLabelKey: componentName, constant.VolumeClaimTemplateNameLabelKey: vctName, }, client.InNamespace(opsRes.Cluster.Namespace)); err != nil { - return + return 0, 0, 0, err } + comp := opsRes.Cluster.Spec.GetComponentByName(componentName) + if comp == nil { + err = fmt.Errorf("comp %s of cluster %s not found", componentName, opsRes.Cluster.Name) + return 0, 0, 0, err + } + expectCount = int(comp.Replicas) vctKey := getComponentVCTKey(componentName, vctName) requestStorage := storageMap[vctKey] - expectCount = len(pvcList.Items) - var completedCount int + var ordinal int for _, v := range pvcList.Items { + // filter PVC(s) with ordinal no larger than comp.Replicas - 1, which left by scale-in + ordinal, err = getPVCOrdinal(v.Name) + if err != nil { + return 0, 0, 0, err + } + if ordinal > expectCount-1 { + continue + } objectKey := getPVCProgressObjectKey(v.Name) - progressDetail := appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey, Group: vctName} - // if the volume expand succeed - if v.Status.Capacity.Storage().Cmp(requestStorage) >= 0 { + progressDetail := findStatusProgressDetail(compStatus.ProgressDetails, objectKey) + if progressDetail == nil { + progressDetail = &appsv1alpha1.ProgressStatusDetail{ObjectKey: objectKey, Group: vctName} + } + if progressDetail.Status == appsv1alpha1.FailedProgressStatus { + completedCount += 1 + continue + } + currStorageSize := v.Status.Capacity.Storage() + // should check if the spec.resources.requests.storage equals to the requested storage + // and pvc is bound if the pvc is re-created for recovery. + if currStorageSize.Cmp(requestStorage) == 0 && + v.Spec.Resources.Requests.Storage().Cmp(requestStorage) == 0 && + v.Status.Phase == corev1.ClaimBound { succeedCount += 1 completedCount += 1 - message := fmt.Sprintf("Successfully expand volume: %s in Component: %s ", objectKey, componentName) + message := fmt.Sprintf("Successfully expand volume: %s in Component: %s", objectKey, componentName) progressDetail.SetStatusAndMessage(appsv1alpha1.SucceedProgressStatus, message) - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, *progressDetail) continue } if ve.pvcIsResizing(&v) { @@ -319,13 +285,9 @@ func (ve volumeExpansionOpsHandler) handleVCTExpansionProgress(reqCtx intctrluti message := fmt.Sprintf("Waiting for an external controller to process the pvc: %s in Component: %s ", objectKey, componentName) progressDetail.SetStatusAndMessage(appsv1alpha1.PendingProgressStatus, message) } - SetComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, progressDetail) - if ve.isExpansionCompleted(progressDetail.Status) { - completedCount += 1 - } + setComponentStatusProgressDetail(opsRes.Recorder, opsRes.OpsRequest, &compStatus.ProgressDetails, *progressDetail) } - isCompleted = completedCount == len(pvcList.Items) - return succeedCount, expectCount, isCompleted, nil + return succeedCount, expectCount, completedCount, nil } func getComponentVCTKey(componentName, vctName string) string { @@ -335,3 +297,11 @@ func getComponentVCTKey(componentName, vctName string) string { func getPVCProgressObjectKey(pvcName string) string { return fmt.Sprintf("PVC/%s", pvcName) } + +func getPVCOrdinal(pvcName string) (int, error) { + subMatches := pvcNameRegex.FindStringSubmatch(pvcName) + if len(subMatches) < 3 { + return 0, fmt.Errorf("wrong pvc name: %s", pvcName) + } + return strconv.Atoi(subMatches[2]) +} diff --git a/controllers/apps/operations/volume_expansion_test.go b/controllers/apps/operations/volume_expansion_test.go index 4ed9f598e..76c840ca2 100644 --- a/controllers/apps/operations/volume_expansion_test.go +++ b/controllers/apps/operations/volume_expansion_test.go @@ -1,29 +1,32 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( - "fmt" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + storagev1 "k8s.io/api/storage/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -54,7 +57,7 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -79,23 +82,21 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { createPVC := func(clusterName, scName, vctName, pvcName string) { // Note: in real k8s cluster, it maybe fails when pvc created by k8s controller. testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterName, - consensusCompName, "data").SetStorage("2Gi").SetStorageClass(storageClassName).Create(&testCtx) + consensusCompName, testapps.DataVolumeName).AddLabels(constant.AppInstanceLabelKey, clusterName, + constant.VolumeClaimTemplateNameLabelKey, testapps.DataVolumeName, + constant.KBAppComponentLabelKey, consensusCompName).SetStorage("2Gi").SetStorageClass(storageClassName).CheckedCreate(&testCtx) } - mockDoOperationOnCluster := func(cluster *appsv1alpha1.Cluster, opsRequestName string, opsType appsv1alpha1.OpsType) { - Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(cluster), func(tmpCluster *appsv1alpha1.Cluster) { - if tmpCluster.Annotations == nil { - tmpCluster.Annotations = map[string]string{} - } - tmpCluster.Annotations[constant.OpsRequestAnnotationKey] = fmt.Sprintf(`[{"type": "%s", "name":"%s"}]`, opsType, opsRequestName) - })()).ShouldNot(HaveOccurred()) - - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(cluster), func(g Gomega, myCluster *appsv1alpha1.Cluster) { - g.Expect(getOpsRequestNameFromAnnotation(myCluster, appsv1alpha1.VolumeExpansionType)).ShouldNot(BeNil()) - })).Should(Succeed()) - } + initResourcesForVolumeExpansion := func(clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource, storage string, replicas int) (*appsv1alpha1.OpsRequest, []string) { + pvcNames := opsRes.Cluster.GetVolumeClaimNames(consensusCompName) + for _, pvcName := range pvcNames { + createPVC(clusterObject.Name, storageClassName, vctName, pvcName) + // mock pvc is Bound + Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace}, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Phase = corev1.ClaimBound + })()).ShouldNot(HaveOccurred()) - initResourcesForVolumeExpansion := func(clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource, index int) (*appsv1alpha1.OpsRequest, string) { + } currRandomStr := testCtx.GetRandomStr() ops := testapps.NewOpsRequestObj("volumeexpansion-ops-"+currRandomStr, testCtx.DefaultNamespace, clusterObject.Name, appsv1alpha1.VolumeExpansionType) @@ -105,7 +106,7 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { VolumeClaimTemplates: []appsv1alpha1.OpsRequestVolumeClaimTemplate{ { Name: vctName, - Storage: resource.MustParse("3Gi"), + Storage: resource.MustParse(storage), }, }, }, @@ -114,42 +115,44 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { // create opsRequest ops = testapps.CreateOpsRequest(ctx, testCtx, ops) + return ops, pvcNames + } - By("mock do operation on cluster") - mockDoOperationOnCluster(clusterObject, ops.Name, appsv1alpha1.VolumeExpansionType) + mockVolumeExpansionActionAndReconcile := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource, newOps *appsv1alpha1.OpsRequest, pvcNames []string) { + // first step, validate ops and update phase to Creating + _, err := GetOpsManager().Do(reqCtx, k8sClient, opsRes) + Expect(err).Should(BeNil()) - // create-pvc - pvcName := fmt.Sprintf("%s-%s-%s-%d", vctName, clusterObject.Name, consensusCompName, index) - createPVC(clusterObject.Name, storageClassName, vctName, pvcName) - // waiting pvc controller mark annotation to OpsRequest - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, tmpOps *appsv1alpha1.OpsRequest) { - g.Expect(tmpOps.Annotations).ShouldNot(BeNil()) - g.Expect(tmpOps.Annotations[constant.ReconcileAnnotationKey]).ShouldNot(BeEmpty()) - })).Should(Succeed()) - return ops, pvcName - } + // next step, do volume-expand action + _, err = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + Expect(err).Should(BeNil()) + + By("mock pvc.spec.resources.request.storage has applied by cluster controller") + for _, pvcName := range pvcNames { + Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace}, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Spec.Resources.Requests[corev1.ResourceStorage] = newOps.Spec.VolumeExpansionList[0].VolumeClaimTemplates[0].Storage + })()).ShouldNot(HaveOccurred()) + } - mockVolumeExpansionActionAndReconcile := func(reqCtx intctrlutil.RequestCtx, opsRes *OpsResource, newOps *appsv1alpha1.OpsRequest) { + By("mock opsRequest is Running") Expect(testapps.ChangeObjStatus(&testCtx, newOps, func() { - _, _ = GetOpsManager().Do(reqCtx, k8sClient, opsRes) newOps.Status.Phase = appsv1alpha1.OpsRunningPhase newOps.Status.StartTimestamp = metav1.Time{Time: time.Now()} })).ShouldNot(HaveOccurred()) - // do volume-expand action - _, _ = GetOpsManager().Do(reqCtx, k8sClient, opsRes) + // reconcile ops status opsRes.OpsRequest = newOps - _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(err == nil).Should(BeTrue()) - Eventually(testapps.GetOpsRequestCompPhase(ctx, testCtx, newOps.Name, consensusCompName)).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + _, err = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) + Expect(err).Should(BeNil()) } testWarningEventOnPVC := func(reqCtx intctrlutil.RequestCtx, clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource) { // init resources for volume expansion - newOps, pvcName := initResourcesForVolumeExpansion(clusterObject, opsRes, 1) + comp := opsRes.Cluster.Spec.GetComponentByName(consensusCompName) + newOps, pvcNames := initResourcesForVolumeExpansion(clusterObject, opsRes, "4Gi", int(comp.Replicas)) By("mock run volumeExpansion action and reconcileAction") - mockVolumeExpansionActionAndReconcile(reqCtx, opsRes, newOps) + mockVolumeExpansionActionAndReconcile(reqCtx, opsRes, newOps, pvcNames) By("test warning event and volumeExpansion failed") // test when the event does not reach the conditions @@ -160,16 +163,15 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { Message: "You've reached the maximum modification rate per volume limit. Wait at least 6 hours between modifications per EBS volume.", } stsInvolvedObject := corev1.ObjectReference{ - Name: pvcName, + Name: pvcNames[0], Kind: constant.PersistentVolumeClaimKind, Namespace: "default", } event.InvolvedObject = stsInvolvedObject pvcEventHandler := PersistentVolumeClaimEventHandler{} Expect(pvcEventHandler.Handle(k8sClient, reqCtx, eventRecorder, event)).Should(Succeed()) - Eventually(testapps.GetOpsRequestCompPhase(ctx, testCtx, newOps.Name, consensusCompName)).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) - // test when the event reach the conditions + // test when the event reaches the conditions event.Count = 5 event.FirstTimestamp = metav1.Time{Time: time.Now()} event.LastTimestamp = metav1.Time{Time: time.Now().Add(61 * time.Second)} @@ -177,7 +179,7 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(newOps), func(g Gomega, tmpOps *appsv1alpha1.OpsRequest) { progressDetails := tmpOps.Status.Components[consensusCompName].ProgressDetails g.Expect(len(progressDetails) > 0).Should(BeTrue()) - progressDetail := FindStatusProgressDetail(progressDetails, getPVCProgressObjectKey(pvcName)) + progressDetail := findStatusProgressDetail(progressDetails, getPVCProgressObjectKey(pvcNames[0])) g.Expect(progressDetail.Status == appsv1alpha1.FailedProgressStatus).Should(BeTrue()) })).Should(Succeed()) } @@ -189,59 +191,58 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { })).ShouldNot(HaveOccurred()) // init resources for volume expansion - newOps, pvcName := initResourcesForVolumeExpansion(clusterObject, opsRes, 0) + comp := clusterObject.Spec.GetComponentByName(consensusCompName) + newOps, pvcNames := initResourcesForVolumeExpansion(clusterObject, opsRes, "3Gi", int(comp.Replicas)) By("mock run volumeExpansion action and reconcileAction") - mockVolumeExpansionActionAndReconcile(reqCtx, opsRes, newOps) + mockVolumeExpansionActionAndReconcile(reqCtx, opsRes, newOps, pvcNames) By("mock pvc is resizing") - pvcKey := client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace} - Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { - pvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{{ - Type: corev1.PersistentVolumeClaimResizing, - Status: corev1.ConditionTrue, - LastTransitionTime: metav1.Now(), - }, - } - })()).ShouldNot(HaveOccurred()) - - Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { - conditions := tmpPVC.Status.Conditions - g.Expect(len(conditions) > 0 && conditions[0].Type == corev1.PersistentVolumeClaimResizing).Should(BeTrue()) - })).Should(Succeed()) - - // waiting OpsRequest.status.components["consensus"].vct["data"] is running - _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(newOps), func(g Gomega, tmpOps *appsv1alpha1.OpsRequest) { - progressDetails := tmpOps.Status.Components[consensusCompName].ProgressDetails - progressDetail := FindStatusProgressDetail(progressDetails, getPVCProgressObjectKey(pvcName)) - g.Expect(progressDetail != nil && progressDetail.Status == appsv1alpha1.ProcessingProgressStatus).Should(BeTrue()) - })).Should(Succeed()) - - By("mock pvc resizing succeed") - // mock pvc volumeExpansion succeed - Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { - pvc.Status.Capacity = corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("3Gi")} - })()).ShouldNot(HaveOccurred()) - - Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { - g.Expect(tmpPVC.Status.Capacity[corev1.ResourceStorage] == resource.MustParse("3Gi")).Should(BeTrue()) - })).Should(Succeed()) + for _, pvcName := range pvcNames { + pvcKey := client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace} + Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Conditions = []corev1.PersistentVolumeClaimCondition{{ + Type: corev1.PersistentVolumeClaimResizing, + Status: corev1.ConditionTrue, + LastTransitionTime: metav1.Now(), + }, + } + pvc.Status.Phase = corev1.ClaimBound + })()).ShouldNot(HaveOccurred()) + + Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { + conditions := tmpPVC.Status.Conditions + g.Expect(len(conditions) > 0 && conditions[0].Type == corev1.PersistentVolumeClaimResizing).Should(BeTrue()) + })).Should(Succeed()) + + // waiting OpsRequest.status.components["consensus"].vct["data"] is running + _, _ = GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(newOps), func(g Gomega, tmpOps *appsv1alpha1.OpsRequest) { + progressDetails := tmpOps.Status.Components[consensusCompName].ProgressDetails + progressDetail := findStatusProgressDetail(progressDetails, getPVCProgressObjectKey(pvcName)) + g.Expect(progressDetail != nil && progressDetail.Status == appsv1alpha1.ProcessingProgressStatus).Should(BeTrue()) + })).Should(Succeed()) + + By("mock pvc resizing succeed") + // mock pvc volumeExpansion succeed + Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Capacity = corev1.ResourceList{corev1.ResourceStorage: resource.MustParse("3Gi")} + })()).ShouldNot(HaveOccurred()) + + Eventually(testapps.CheckObj(&testCtx, pvcKey, func(g Gomega, tmpPVC *corev1.PersistentVolumeClaim) { + g.Expect(tmpPVC.Status.Capacity[corev1.ResourceStorage] == resource.MustParse("3Gi")).Should(BeTrue()) + })).Should(Succeed()) + } - // waiting OpsRequest.status.phase is succeed + // waiting for OpsRequest.status.phase is succeed _, err := GetOpsManager().Reconcile(reqCtx, k8sClient, opsRes) - Expect(err == nil).Should(BeTrue()) - Expect(opsRes.OpsRequest.Status.Phase == appsv1alpha1.OpsSucceedPhase).Should(BeTrue()) - - testWarningEventOnPVC(reqCtx, clusterObject, opsRes) + Expect(err).Should(BeNil()) + Expect(opsRes.OpsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsSucceedPhase)) } testDeleteRunningVolumeExpansion := func(clusterObject *appsv1alpha1.Cluster, opsRes *OpsResource) { // init resources for volume expansion - newOps, pvcName := initResourcesForVolumeExpansion(clusterObject, opsRes, 2) - Expect(testapps.ChangeObjStatus(&testCtx, clusterObject, func() { - clusterObject.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase - })).ShouldNot(HaveOccurred()) + newOps, pvcNames := initResourcesForVolumeExpansion(clusterObject, opsRes, "5Gi", 1) Expect(k8sClient.Delete(ctx, newOps)).Should(Succeed()) Eventually(func() error { return k8sClient.Get(ctx, client.ObjectKey{Name: newOps.Name, Namespace: testCtx.DefaultNamespace}, &appsv1alpha1.OpsRequest{}) @@ -249,7 +250,7 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { By("test handle the invalid volumeExpansion OpsRequest") pvc := &corev1.PersistentVolumeClaim{} - Expect(k8sClient.Get(ctx, client.ObjectKey{Name: pvcName, Namespace: testCtx.DefaultNamespace}, pvc)).Should(Succeed()) + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: pvcNames[0], Namespace: testCtx.DefaultNamespace}, pvc)).Should(Succeed()) Expect(handleVolumeExpansionWithPVC(intctrlutil.RequestCtx{Ctx: ctx}, k8sClient, pvc)).Should(Succeed()) Eventually(testapps.GetClusterPhase(&testCtx, client.ObjectKeyFromObject(clusterObject))).Should(Equal(appsv1alpha1.RunningClusterPhase)) @@ -258,12 +259,12 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { Context("Test VolumeExpansion", func() { It("VolumeExpansion should work", func() { reqCtx := intctrlutil.RequestCtx{Ctx: ctx} - _, _, clusterObject := testapps.InitConsensusMysql(testCtx, clusterDefinitionName, + _, _, clusterObject := testapps.InitConsensusMysql(&testCtx, clusterDefinitionName, clusterVersionName, clusterName, "consensus", consensusCompName) // init storageClass - sc := testapps.CreateStorageClass(testCtx, storageClassName, true) - Expect(testapps.ChangeObj(&testCtx, sc, func() { - sc.Annotations = map[string]string{storage.IsDefaultStorageClassAnnotation: "true"} + sc := testapps.CreateStorageClass(&testCtx, storageClassName, true) + Expect(testapps.ChangeObj(&testCtx, sc, func(lsc *storagev1.StorageClass) { + lsc.Annotations = map[string]string{storage.IsDefaultStorageClassAnnotation: "true"} })).ShouldNot(HaveOccurred()) opsRes := &OpsResource{ @@ -284,6 +285,9 @@ var _ = Describe("OpsRequest Controller Volume Expansion Handler", func() { By("Test VolumeExpansion") testVolumeExpansion(reqCtx, clusterObject, opsRes, randomStr) + By("Test Warning Event occurs during volume expanding") + testWarningEventOnPVC(reqCtx, clusterObject, opsRes) + By("Test delete the Running VolumeExpansion OpsRequest") testDeleteRunningVolumeExpansion(clusterObject, opsRes) }) diff --git a/controllers/apps/operations/volume_expansion_updater.go b/controllers/apps/operations/volume_expansion_updater.go index 7b873afd2..2273a6e77 100644 --- a/controllers/apps/operations/volume_expansion_updater.go +++ b/controllers/apps/operations/volume_expansion_updater.go @@ -1,28 +1,29 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package operations import ( - "context" "time" "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" @@ -54,54 +55,24 @@ func init() { // handleVolumeExpansionOperation handles the pvc for the volume expansion OpsRequest. // it will be triggered when the PersistentVolumeClaim has changed. func handleVolumeExpansionWithPVC(reqCtx intctrlutil.RequestCtx, cli client.Client, pvc *corev1.PersistentVolumeClaim) error { - clusterName := pvc.Labels[constant.AppInstanceLabelKey] - cluster := &appsv1alpha1.Cluster{} - if err := cli.Get(reqCtx.Ctx, client.ObjectKey{Name: clusterName, Namespace: pvc.Namespace}, cluster); err != nil { + opsRequestList, err := appsv1alpha1.GetRunningOpsByOpsType(reqCtx.Ctx, cli, + pvc.Labels[constant.AppInstanceLabelKey], pvc.Namespace, string(appsv1alpha1.VolumeExpansionType)) + if err != nil { return err } - // check whether the cluster is expanding volume - opsRequestName := getOpsRequestNameFromAnnotation(cluster, appsv1alpha1.VolumeExpansionType) - if opsRequestName == nil { + if len(opsRequestList) == 0 { return nil } // notice the OpsRequest to reconcile - err := opsutil.PatchOpsRequestReconcileAnnotation(reqCtx.Ctx, cli, cluster.Namespace, *opsRequestName) - // if the OpsRequest is not found, means it is deleted by user. - // we should delete the invalid OpsRequest annotation in the cluster and reconcile the cluster phase. - if apierrors.IsNotFound(err) { - opsRequestSlice, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) - notExistOps := map[string]struct{}{ - *opsRequestName: {}, - } - if err = opsutil.RemoveClusterInvalidOpsRequestAnnotation(reqCtx.Ctx, cli, cluster, - opsRequestSlice, notExistOps); err != nil { + for _, ops := range opsRequestList { + if err = opsutil.PatchOpsRequestReconcileAnnotation(reqCtx.Ctx, cli, pvc.Namespace, ops.Name); err != nil { return err } - return handleClusterVolumeExpandingPhase(reqCtx.Ctx, cli, cluster) - } - return err -} - -// handleClusterVolumeExpandingPhase this function will reconcile the cluster status phase when the OpsRequest is deleted. -func handleClusterVolumeExpandingPhase(ctx context.Context, - cli client.Client, - cluster *appsv1alpha1.Cluster) error { - if cluster.Status.Phase != appsv1alpha1.SpecReconcilingClusterPhase { - return nil - } - patch := client.MergeFrom(cluster.DeepCopy()) - for k, v := range cluster.Status.Components { - if v.Phase == appsv1alpha1.SpecReconcilingClusterCompPhase { - v.Phase = appsv1alpha1.RunningClusterCompPhase - cluster.Status.SetComponentStatus(k, v) - } } - // REVIEW: a single component status affect cluser level status? - cluster.Status.Phase = appsv1alpha1.RunningClusterPhase - return cli.Status().Patch(ctx, cluster, patch) + return nil } -// Handle the warning events on pvcs. if the events are resize failed events, update the OpsRequest.status. +// Handle the warning events of PVCs. if the events are resize-failed events, update the OpsRequest.status. func (pvcEventHandler PersistentVolumeClaimEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, @@ -129,19 +100,19 @@ func (pvcEventHandler PersistentVolumeClaimEventHandler) Handle(cli client.Clien return nil } - // here, if the volume expansion ops is running. we will change the pvc status to Failed on the OpsRequest. - return pvcEventHandler.handlePVCFailedStatusOnOpsRequest(cli, reqCtx, recorder, event, pvc) + // here, if the volume expansion ops is running, change the pvc status to Failed on the OpsRequest. + return pvcEventHandler.handlePVCFailedStatusOnRunningOpsRequests(cli, reqCtx, recorder, event, pvc) } -// isTargetResizeFailedEvents checks the event is the resize failed events. +// isTargetResizeFailedEvents checks the event is the resize-failed events. func (pvcEventHandler PersistentVolumeClaimEventHandler) isTargetResizeFailedEvents(event *corev1.Event) bool { - // ignores ExternalExpanding event, this event is always exists when using csi driver. + // ignores ExternalExpanding event, this event always exists when using csi driver. return event.Type == corev1.EventTypeWarning && event.InvolvedObject.Kind == constant.PersistentVolumeClaimKind && slices.Index([]string{VolumeResizeFailed, FileSystemResizeFailed}, event.Reason) != -1 } -// handlePVCFailedStatusOnOpsRequest if the volume expansion ops is running. we will change the pvc status to Failed on the OpsRequest, -func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatusOnOpsRequest(cli client.Client, +// handlePVCFailedStatusOnOpsRequest if the volume expansion ops is running, changes the pvc status to Failed on the OpsRequest, +func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatusOnRunningOpsRequests(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event, @@ -157,15 +128,28 @@ func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatusOn }, cluster); err != nil { return err } - // get the volume expansion ops which is running on cluster. - opsRequestName := getOpsRequestNameFromAnnotation(cluster, appsv1alpha1.VolumeExpansionType) - if opsRequestName == nil { + opsRequestList, err := appsv1alpha1.GetRunningOpsByOpsType(reqCtx.Ctx, cli, + pvc.Labels[constant.AppInstanceLabelKey], pvc.Namespace, string(appsv1alpha1.VolumeExpansionType)) + if err != nil { + return err + } + if len(opsRequestList) == 0 { return nil } - opsRequest := &appsv1alpha1.OpsRequest{} - if err = cli.Get(reqCtx.Ctx, client.ObjectKey{Name: *opsRequestName, Namespace: pvc.Namespace}, opsRequest); err != nil { - return err + for _, ops := range opsRequestList { + if err = pvcEventHandler.handlePVCFailedStatus(cli, reqCtx, recorder, event, pvc, &ops); err != nil { + return err + } } + return nil +} + +func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatus(cli client.Client, + reqCtx intctrlutil.RequestCtx, + recorder record.EventRecorder, + event *corev1.Event, + pvc *corev1.PersistentVolumeClaim, + opsRequest *appsv1alpha1.OpsRequest) error { compsStatus := opsRequest.Status.Components if compsStatus == nil { return nil @@ -181,7 +165,7 @@ func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatusOn } // save the failed message to the progressDetail. objectKey := getPVCProgressObjectKey(pvc.Name) - progressDetail := FindStatusProgressDetail(component.ProgressDetails, objectKey) + progressDetail := findStatusProgressDetail(component.ProgressDetails, objectKey) if progressDetail == nil || progressDetail.Message != event.Message { isChanged = true } @@ -192,14 +176,14 @@ func (pvcEventHandler PersistentVolumeClaimEventHandler) handlePVCFailedStatusOn Message: event.Message, } - SetComponentStatusProgressDetail(recorder, opsRequest, &component.ProgressDetails, *progressDetail) + setComponentStatusProgressDetail(recorder, opsRequest, &component.ProgressDetails, *progressDetail) compsStatus[cName] = component break } if !isChanged { return nil } - if err = cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil { + if err := cli.Status().Patch(reqCtx.Ctx, opsRequest, patch); err != nil { return err } recorder.Event(opsRequest, corev1.EventTypeWarning, event.Reason, event.Message) diff --git a/controllers/apps/opsrequest_controller.go b/controllers/apps/opsrequest_controller.go index 2ecc597c7..535ff8d55 100644 --- a/controllers/apps/opsrequest_controller.go +++ b/controllers/apps/opsrequest_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -24,6 +27,7 @@ import ( "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" @@ -63,9 +67,10 @@ func (r *OpsRequestReconciler) Reconcile(ctx context.Context, req ctrl.Request) opsCtrlHandler := &opsControllerHandler{} return opsCtrlHandler.Handle(reqCtx, &operations.OpsResource{Recorder: r.Recorder}, r.fetchOpsRequest, - r.handleDeleteEvent, + r.handleDeletion, r.fetchCluster, r.addClusterLabelAndSetOwnerReference, + r.handleCancelSignal, r.handleOpsRequestByPhase, ) } @@ -85,7 +90,7 @@ func (r *OpsRequestReconciler) fetchOpsRequest(reqCtx intctrlutil.RequestCtx, op return intctrlutil.ResultToP(intctrlutil.RequeueWithError(err, reqCtx.Log, "")) } // if the opsRequest is not found, we need to check if this opsRequest is deleted abnormally - if err = r.handleOpsDeletedDuringRunning(reqCtx); err != nil { + if err = r.handleOpsReqDeletedDuringRunning(reqCtx); err != nil { return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) } return intctrlutil.ResultToP(intctrlutil.Reconciled()) @@ -94,8 +99,8 @@ func (r *OpsRequestReconciler) fetchOpsRequest(reqCtx intctrlutil.RequestCtx, op return nil, nil } -// handleDeleteEvent handles the delete event of the OpsRequest. -func (r *OpsRequestReconciler) handleDeleteEvent(reqCtx intctrlutil.RequestCtx, opsRes *operations.OpsResource) (*ctrl.Result, error) { +// handleDeletion handles the delete event of the OpsRequest. +func (r *OpsRequestReconciler) handleDeletion(reqCtx intctrlutil.RequestCtx, opsRes *operations.OpsResource) (*ctrl.Result, error) { if opsRes.OpsRequest.Status.Phase == appsv1alpha1.OpsRunningPhase { return nil, nil } @@ -134,16 +139,44 @@ func (r *OpsRequestReconciler) handleOpsRequestByPhase(reqCtx intctrlutil.Reques return intctrlutil.ResultToP(intctrlutil.Reconciled()) case appsv1alpha1.OpsPendingPhase, appsv1alpha1.OpsCreatingPhase: return r.doOpsRequestAction(reqCtx, opsRes) - case appsv1alpha1.OpsRunningPhase: - return r.reconcileStatusDuringRunning(reqCtx, opsRes) + case appsv1alpha1.OpsRunningPhase, appsv1alpha1.OpsCancellingPhase: + return r.reconcileStatusDuringRunningOrCanceling(reqCtx, opsRes) case appsv1alpha1.OpsSucceedPhase: return r.handleSucceedOpsRequest(reqCtx, opsRes.OpsRequest) - case appsv1alpha1.OpsFailedPhase: + case appsv1alpha1.OpsFailedPhase, appsv1alpha1.OpsCancelledPhase: return intctrlutil.ResultToP(intctrlutil.Reconciled()) } return intctrlutil.ResultToP(intctrlutil.Reconciled()) } +// handleCancelSignal handles the cancel signal for opsRequest. +func (r *OpsRequestReconciler) handleCancelSignal(reqCtx intctrlutil.RequestCtx, opsRes *operations.OpsResource) (*ctrl.Result, error) { + opsRequest := opsRes.OpsRequest + if !opsRequest.Spec.Cancel { + return nil, nil + } + if opsRequest.IsComplete() || opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + return nil, nil + } + opsBehaviour := operations.GetOpsManager().OpsMap[opsRequest.Spec.Type] + if opsBehaviour.CancelFunc == nil { + r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, reasonOpsCancelActionNotSupported, + "Type: %s does not support cancel action.", opsRequest.Spec.Type) + return nil, nil + } + deepCopyOps := opsRequest.DeepCopy() + if err := opsBehaviour.CancelFunc(reqCtx, r.Client, opsRes); err != nil { + r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, reasonOpsCancelActionFailed, err.Error()) + return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) + } + opsRequest.Status.CancelTimestamp = metav1.Time{Time: time.Now()} + if err := operations.PatchOpsStatusWithOpsDeepCopy(reqCtx.Ctx, r.Client, opsRes, deepCopyOps, + appsv1alpha1.OpsCancellingPhase, appsv1alpha1.NewCancelingCondition(opsRes.OpsRequest)); err != nil { + return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) + } + return intctrlutil.ResultToP(intctrlutil.Reconciled()) +} + // handleSucceedOpsRequest the opsRequest will be deleted after one hour when status.phase is Succeed func (r *OpsRequestReconciler) handleSucceedOpsRequest(reqCtx intctrlutil.RequestCtx, opsRequest *appsv1alpha1.OpsRequest) (*ctrl.Result, error) { if opsRequest.Status.CompletionTimestamp.IsZero() || opsRequest.Spec.TTLSecondsAfterSucceed == 0 { @@ -160,12 +193,12 @@ func (r *OpsRequestReconciler) handleSucceedOpsRequest(reqCtx intctrlutil.Reques return intctrlutil.ResultToP(intctrlutil.Reconciled()) } -// reconcileStatusDuringRunning reconciles the status of OpsRequest when it is running. -func (r *OpsRequestReconciler) reconcileStatusDuringRunning(reqCtx intctrlutil.RequestCtx, opsRes *operations.OpsResource) (*ctrl.Result, error) { +// reconcileStatusDuringRunningOrCanceling reconciles the status of OpsRequest when it is running or canceling. +func (r *OpsRequestReconciler) reconcileStatusDuringRunningOrCanceling(reqCtx intctrlutil.RequestCtx, opsRes *operations.OpsResource) (*ctrl.Result, error) { opsRequest := opsRes.OpsRequest // wait for OpsRequest.status.phase to Succeed if requeueAfter, err := operations.GetOpsManager().Reconcile(reqCtx, r.Client, opsRes); err != nil { - r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, "ReconcileStatusFailed", "Failed to reconcile the status of OpsRequest: %s", err.Error()) + r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, reasonOpsReconcileStatusFailed, "Failed to reconcile the status of OpsRequest: %s", err.Error()) return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) } else if requeueAfter != 0 { // if the reconcileAction need requeue, do it @@ -179,7 +212,8 @@ func (r *OpsRequestReconciler) addClusterLabelAndSetOwnerReference(reqCtx intctr // add label of clusterRef opsRequest := opsRes.OpsRequest clusterName := opsRequest.Labels[constant.AppInstanceLabelKey] - if clusterName == opsRequest.Spec.ClusterRef { + opsType := opsRequest.Labels[constant.OpsRequestTypeLabelKey] + if clusterName == opsRequest.Spec.ClusterRef && opsType == string(opsRequest.Spec.Type) { return nil, nil } patch := client.MergeFrom(opsRequest.DeepCopy()) @@ -187,6 +221,7 @@ func (r *OpsRequestReconciler) addClusterLabelAndSetOwnerReference(reqCtx intctr opsRequest.Labels = map[string]string{} } opsRequest.Labels[constant.AppInstanceLabelKey] = opsRequest.Spec.ClusterRef + opsRequest.Labels[constant.OpsRequestTypeLabelKey] = string(opsRequest.Spec.Type) scheme, _ := appsv1alpha1.SchemeBuilder.Build() if err := controllerutil.SetOwnerReference(opsRes.Cluster, opsRequest, scheme); err != nil { return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) @@ -204,7 +239,7 @@ func (r *OpsRequestReconciler) doOpsRequestAction(reqCtx intctrlutil.RequestCtx, opsDeepCopy := opsRequest.DeepCopy() res, err := operations.GetOpsManager().Do(reqCtx, r.Client, opsRes) if err != nil { - r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, "DoActionFailed", "Failed to process the operation of OpsRequest: %s", err.Error()) + r.Recorder.Eventf(opsRequest, corev1.EventTypeWarning, reasonOpsDoActionFailed, "Failed to process the operation of OpsRequest: %s", err.Error()) if !reflect.DeepEqual(opsRequest.Status, opsDeepCopy.Status) { if patchErr := r.Client.Status().Patch(reqCtx.Ctx, opsRequest, client.MergeFrom(opsDeepCopy)); patchErr != nil { return intctrlutil.ResultToP(intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "")) @@ -223,8 +258,8 @@ func (r *OpsRequestReconciler) doOpsRequestAction(reqCtx intctrlutil.RequestCtx, return intctrlutil.ResultToP(intctrlutil.Reconciled()) } -// handleOpsDeletedDuringRunning handles the cluster annotation if the OpsRequest is deleted during running. -func (r *OpsRequestReconciler) handleOpsDeletedDuringRunning(reqCtx intctrlutil.RequestCtx) error { +// handleOpsReqDeletedDuringRunning handles the cluster annotation if the OpsRequest is deleted during running. +func (r *OpsRequestReconciler) handleOpsReqDeletedDuringRunning(reqCtx intctrlutil.RequestCtx) error { clusterList := &appsv1alpha1.ClusterList{} if err := r.Client.List(reqCtx.Ctx, clusterList, client.InNamespace(reqCtx.Req.Namespace)); err != nil { return err @@ -235,7 +270,7 @@ func (r *OpsRequestReconciler) handleOpsDeletedDuringRunning(reqCtx intctrlutil. if index == -1 { continue } - // if the OpsRequest is abnormal end, we should clear the OpsRequest annotation in reference cluster. + // if the OpsRequest is abnormal, we should clear the OpsRequest annotation in referencing cluster. opsRequestSlice = slices.Delete(opsRequestSlice, index, index+1) return opsutil.PatchClusterOpsAnnotations(reqCtx.Ctx, r.Client, &cluster, opsRequestSlice) } diff --git a/controllers/apps/opsrequest_controller_test.go b/controllers/apps/opsrequest_controller_test.go index af7999a45..535e50f90 100644 --- a/controllers/apps/opsrequest_controller_test.go +++ b/controllers/apps/opsrequest_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -22,11 +25,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -44,13 +48,12 @@ var _ = Describe("OpsRequest Controller", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" const clusterNamePrefix = "test-cluster" - - const mysqlCompType = "consensus" + const mysqlCompDefName = "mysql" const mysqlCompName = "mysql" const defaultMinReadySeconds = 10 cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -58,16 +61,23 @@ var _ = Describe("OpsRequest Controller", func() { inNS := client.InNamespace(testCtx.DefaultNamespace) ml := client.HasLabels{testCtx.TestObjLabelKey} - testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml) + + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PodSignature, true, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.OpsRequestSignature, true, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.VolumeSnapshotSignature, true, inNS) // delete cluster(and all dependent sub-resources), clusterversion and clusterdef testapps.ClearClusterResources(&testCtx) testapps.ClearResources(&testCtx, intctrlutil.StorageClassSignature, ml) + + // non-namespaced + testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, ml) + testapps.ClearResources(&testCtx, intctrlutil.ComponentResourceConstraintSignature, ml) + testapps.ClearResources(&testCtx, intctrlutil.ComponentClassDefinitionSignature, ml) } BeforeEach(func() { cleanEnv() - }) AfterEach(func() { @@ -83,28 +93,28 @@ var _ = Describe("OpsRequest Controller", func() { // Testcases - checkLatestOpsIsProcessing := func(clusterKey client.ObjectKey, opsType appsv1alpha1.OpsType) { - Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { - con := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) - g.Expect(con).ShouldNot(BeNil()) - g.Expect(con.Status).Should(Equal(metav1.ConditionFalse)) - g.Expect(con.Reason).Should(Equal(appsv1alpha1.OpsRequestBehaviourMapper[opsType].ProcessingReasonInClusterCondition)) - })).Should(Succeed()) - } - - checkLatestOpsHasProcessed := func(clusterKey client.ObjectKey) { - Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { - con := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) - g.Expect(con).ShouldNot(BeNil()) - g.Expect(con.Status).Should(Equal(metav1.ConditionTrue)) - g.Expect(con.Reason).Should(Equal(lifecycle.ReasonOpsRequestProcessed)) - })).Should(Succeed()) - } + // checkLatestOpsIsProcessing := func(clusterKey client.ObjectKey, opsType appsv1alpha1.OpsType) { + // Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { + // con := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) + // g.Expect(con).ShouldNot(BeNil()) + // g.Expect(con.Status).Should(Equal(metav1.ConditionFalse)) + // g.Expect(con.Reason).Should(Equal(appsv1alpha1.OpsRequestBehaviourMapper[opsType].ProcessingReasonInClusterCondition)) + // })).Should(Succeed()) + // } + // + // checkLatestOpsHasProcessed := func(clusterKey client.ObjectKey) { + // Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { + // con := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) + // g.Expect(con).ShouldNot(BeNil()) + // g.Expect(con.Status).Should(Equal(metav1.ConditionTrue)) + // g.Expect(con.Reason).Should(Equal(lifecycle.ReasonOpsRequestProcessed)) + // })).Should(Succeed()) + // } mockSetClusterStatusPhaseToRunning := func(namespacedName types.NamespacedName) { Expect(testapps.GetAndChangeObjStatus(&testCtx, namespacedName, func(fetched *appsv1alpha1.Cluster) { - // TODO: whould be better to have hint for cluster.status.phase is mocked, + // TODO: would be better to have hint for cluster.status.phase is mocked, // i.e., add annotation info for the mocked context fetched.Status.Phase = appsv1alpha1.RunningClusterPhase if len(fetched.Status.Components) == 0 { @@ -123,36 +133,51 @@ var _ = Describe("OpsRequest Controller", func() { })()).ShouldNot(HaveOccurred()) } - testVerticalScaleCPUAndMemory := func(workloadType testapps.ComponentTplType) { + type resourceContext struct { + class *appsv1alpha1.ComponentClass + resource corev1.ResourceRequirements + } + + type verticalScalingContext struct { + source resourceContext + target resourceContext + } + + testVerticalScaleCPUAndMemory := func(workloadType testapps.ComponentDefTplType, scalingCtx verticalScalingContext) { const opsName = "mysql-verticalscaling" + By("Create class related objects") + constraint := testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName). + AddConstraints(testapps.GeneralResourceConstraint). + Create(&testCtx).GetObject() + + testapps.NewComponentClassDefinitionFactory("custom", clusterDefObj.Name, mysqlCompDefName). + AddClasses(constraint.Name, []string{testapps.Class1c1gName, testapps.Class2c4gName}). + Create(&testCtx) + By("Create a cluster obj") - resources := corev1.ResourceRequirements{ - Limits: corev1.ResourceList{ - "cpu": resource.MustParse("800m"), - "memory": resource.MustParse("512Mi"), - }, - Requests: corev1.ResourceList{ - "cpu": resource.MustParse("500m"), - "memory": resource.MustParse("256Mi"), - }, - } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, + clusterFactory := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). - SetReplicas(1). - SetResources(resources). - Create(&testCtx).GetObject() + AddComponent(mysqlCompName, mysqlCompDefName). + SetReplicas(1) + if scalingCtx.source.class != nil { + clusterFactory.SetClassDefRef(&appsv1alpha1.ClassDefRef{Class: scalingCtx.source.class.Name}) + } else { + clusterFactory.SetResources(scalingCtx.source.resource) + } + clusterObj = clusterFactory.Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) - By("Waiting for the cluster enter running phase") - Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + By("Waiting for the cluster enters creating phase") Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) By("mock pod/sts are available and wait for cluster enter running phase") podName := fmt.Sprintf("%s-%s-0", clusterObj.Name, mysqlCompName) - pod := testapps.MockConsensusComponentStsPod(testCtx, nil, clusterObj.Name, mysqlCompName, + pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterObj.Name, mysqlCompName, podName, "leader", "ReadWrite") + // the opsRequest will use startTime to check some condition. + // if there is no sleep for 1 second, unstable error may occur. + time.Sleep(time.Second) if workloadType == testapps.StatefulMySQLComponent { lastTransTime := metav1.NewTime(time.Now().Add(-1 * (defaultMinReadySeconds + 1) * time.Second)) Expect(testapps.ChangeObjStatus(&testCtx, pod, func() { @@ -170,59 +195,72 @@ var _ = Describe("OpsRequest Controller", func() { opsKey := types.NamespacedName{Name: opsName, Namespace: testCtx.DefaultNamespace} verticalScalingOpsRequest := testapps.NewOpsRequestObj(opsKey.Name, opsKey.Namespace, clusterObj.Name, appsv1alpha1.VerticalScalingType) - verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{ - { - ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName}, - ResourceRequirements: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - "cpu": resource.MustParse("400m"), - "memory": resource.MustParse("300Mi"), - }, + if scalingCtx.target.class != nil { + verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{ + { + ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName}, + Class: scalingCtx.target.class.Name, }, - }, + } + } else { + verticalScalingOpsRequest.Spec.VerticalScalingList = []appsv1alpha1.VerticalScaling{ + { + ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName}, + ResourceRequirements: scalingCtx.target.resource, + }, + } } Expect(testCtx.CreateObj(testCtx.Ctx, verticalScalingOpsRequest)).Should(Succeed()) - By("check VerticalScalingOpsRequest running") + By("wait for VerticalScalingOpsRequest is running") Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterObj.Name, mysqlCompName)).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) - checkLatestOpsIsProcessing(clusterKey, verticalScalingOpsRequest.Spec.Type) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + // TODO(refactor): try to check some ephemeral states? + // checkLatestOpsIsProcessing(clusterKey, verticalScalingOpsRequest.Spec.Type) - By("check Cluster and changed component phase is VerticalScaling") - Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { - g.Expect(cluster.Status.Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) // VerticalScalingPhase - g.Expect(cluster.Status.Components[mysqlCompName].Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) // VerticalScalingPhase - })).Should(Succeed()) + // By("check Cluster and changed component phase is VerticalScaling") + // Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + // g.Expect(cluster.Status.Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + // g.Expect(cluster.Status.Components[mysqlCompName].Phase).To(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + // })).Should(Succeed()) By("mock bring Cluster and changed component back to running status") Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(&mysqlSts), func(tmpSts *appsv1.StatefulSet) { testk8s.MockStatefulSetReady(tmpSts) })()).ShouldNot(HaveOccurred()) - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterObj.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) - checkLatestOpsHasProcessed(clusterKey) + // checkLatestOpsHasProcessed(clusterKey) - By("patch opsrequest controller to run") - Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func() { - if verticalScalingOpsRequest.Annotations == nil { - verticalScalingOpsRequest.Annotations = map[string]string{} + By("notice opsrequest controller to run") + Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) { + if lopsReq.Annotations == nil { + lopsReq.Annotations = map[string]string{} } - verticalScalingOpsRequest.Annotations[constant.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano) + lopsReq.Annotations[constant.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano) })).ShouldNot(HaveOccurred()) By("check VerticalScalingOpsRequest succeed") Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsSucceedPhase)) By("check cluster resource requirements changed") + var targetRequests corev1.ResourceList + if scalingCtx.target.class != nil { + targetRequests = corev1.ResourceList{ + corev1.ResourceCPU: scalingCtx.target.class.CPU, + corev1.ResourceMemory: scalingCtx.target.class.Memory, + } + } else { + targetRequests = scalingCtx.target.resource.Requests + } Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { - g.Expect(fetched.Spec.ComponentSpecs[0].Resources.Requests).To(Equal( - verticalScalingOpsRequest.Spec.VerticalScalingList[0].Requests)) + g.Expect(fetched.Spec.ComponentSpecs[0].Resources.Requests).To(Equal(targetRequests)) })).Should(Succeed()) By("check OpsRequest reclaimed after ttl") - Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func() { - verticalScalingOpsRequest.Spec.TTLSecondsAfterSucceed = 1 + Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) { + lopsReq.Spec.TTLSecondsAfterSucceed = 1 })).ShouldNot(HaveOccurred()) Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(verticalScalingOpsRequest), verticalScalingOpsRequest, false)).Should(Succeed()) @@ -230,21 +268,50 @@ var _ = Describe("OpsRequest Controller", func() { // Scenarios + // TODO: should focus on OpsRequest control actions, and iterator through all component workload types. Context("with Cluster which has MySQL StatefulSet", func() { BeforeEach(func() { By("Create a clusterDefinition obj") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) - It("issue an VerticalScalingOpsRequest should change Cluster's resource requirements successfully", func() { - testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent) + It("create cluster by class, vertical scaling by resource", func() { + ctx := verticalScalingContext{ + source: resourceContext{class: &testapps.Class1c1g}, + target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()}, + } + testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx) + }) + + It("create cluster by class, vertical scaling by class", func() { + ctx := verticalScalingContext{ + source: resourceContext{class: &testapps.Class1c1g}, + target: resourceContext{class: &testapps.Class2c4g}, + } + testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx) + }) + + It("create cluster by resource, vertical scaling by class", func() { + ctx := verticalScalingContext{ + source: resourceContext{resource: testapps.Class1c1g.ToResourceRequirements()}, + target: resourceContext{class: &testapps.Class2c4g}, + } + testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx) + }) + + It("create cluster by resource, vertical scaling by resource", func() { + ctx := verticalScalingContext{ + source: resourceContext{resource: testapps.Class1c1g.ToResourceRequirements()}, + target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()}, + } + testVerticalScaleCPUAndMemory(testapps.StatefulMySQLComponent, ctx) }) }) @@ -252,26 +319,43 @@ var _ = Describe("OpsRequest Controller", func() { BeforeEach(func() { By("Create a clusterDefinition obj") clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). AddHorizontalScalePolicy(appsv1alpha1.HorizontalScalePolicy{ - Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, + Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, + BackupPolicyTemplateName: backupPolicyTPLName, }).Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) - It("issue an VerticalScalingOpsRequest should change Cluster's resource requirements successfully", func() { - testVerticalScaleCPUAndMemory(testapps.ConsensusMySQLComponent) - }) + componentWorkload := func() *appsv1.StatefulSet { + stsList := testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, clusterKey, mysqlCompName) + return &stsList.Items[0] + } - It("HorizontalScaling when not support snapshot", func() { - By("init backup policy template") - viper.Set("VOLUMESNAPSHOT", false) + mockCompRunning := func(replicas int32) { + sts := componentWorkload() + Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { + testk8s.MockStatefulSetReady(sts) + })).ShouldNot(HaveOccurred()) + for i := 0; i < int(replicas); i++ { + podName := fmt.Sprintf("%s-%s-%d", clusterObj.Name, mysqlCompName, i) + podRole := "follower" + accessMode := "Readonly" + if i == 0 { + podRole = "leader" + accessMode = "ReadWrite" + } + testapps.MockConsensusComponentStsPod(&testCtx, sts, clusterObj.Name, mysqlCompName, podName, podRole, accessMode) + } + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + } + + createMysqlCluster := func(replicas int32) { createBackupPolicyTpl(clusterDefObj) - replicas := int32(3) By("set component to horizontal with snapshot policy and create a cluster") Expect(testapps.GetAndChangeObj(&testCtx, client.ObjectKeyFromObject(clusterDefObj), @@ -282,27 +366,20 @@ var _ = Describe("OpsRequest Controller", func() { pvcSpec := testapps.NewPVCSpec("1Gi") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetReplicas(replicas). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) By("mock component is Running") - stsList := testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, clusterKey, mysqlCompName) - sts := &stsList.Items[0] - Expect(int(*sts.Spec.Replicas)).To(BeEquivalentTo(replicas)) - Expect(testapps.ChangeObjStatus(&testCtx, sts, func() { - testk8s.MockStatefulSetReady(sts) - })).ShouldNot(HaveOccurred()) - testapps.MockConsensusComponentPods(testCtx, sts, clusterKey.Name, mysqlCompName) - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterKey.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + mockCompRunning(replicas) By("mock pvc created") for i := 0; i < int(replicas); i++ { pvcName := fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, mysqlCompName, i) pvc := testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterKey.Name, - mysqlCompName, "data").SetStorage("1Gi").Create(&testCtx).GetObject() + mysqlCompName, testapps.DataVolumeName).SetStorage("1Gi").Create(&testCtx).GetObject() // mock pvc bound Expect(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(pvc), func(pvc *corev1.PersistentVolumeClaim) { pvc.Status.Phase = corev1.ClaimBound @@ -311,7 +388,12 @@ var _ = Describe("OpsRequest Controller", func() { // wait for cluster observed generation Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) mockSetClusterStatusPhaseToRunning(clusterKey) + Eventually(testapps.GetClusterComponentPhase(&testCtx, clusterKey, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + } + createClusterHscaleOps := func(replicas int32) *appsv1alpha1.OpsRequest { By("create a opsRequest to horizontal scale") opsName := "hscale-ops-" + testCtx.GetRandomStr() ops := testapps.NewOpsRequestObj(opsName, testCtx.DefaultNamespace, @@ -319,165 +401,149 @@ var _ = Describe("OpsRequest Controller", func() { ops.Spec.HorizontalScalingList = []appsv1alpha1.HorizontalScaling{ { ComponentOps: appsv1alpha1.ComponentOps{ComponentName: mysqlCompName}, - Replicas: int32(5), + Replicas: replicas, }, } - opsKey := client.ObjectKeyFromObject(ops) + // for reconciling the ops labels + ops.Labels = nil Expect(testCtx.CreateObj(testCtx.Ctx, ops)).Should(Succeed()) + return ops + } - By("expect component is Running if don't support volume snapshot during doing h-scale ops") - Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) - // cluster phase changes to HorizontalScalingPhase first. then, it will be ConditionsError because it does not support snapshot backup after a period of time. - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) // HorizontalScalingPhase - Eventually(testapps.GetClusterComponentPhase(testCtx, clusterKey.Name, mysqlCompName)).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + It("issue an VerticalScalingOpsRequest should change Cluster's resource requirements successfully", func() { + ctx := verticalScalingContext{ + source: resourceContext{class: &testapps.Class1c1g}, + target: resourceContext{resource: testapps.Class2c4g.ToResourceRequirements()}, + } + testVerticalScaleCPUAndMemory(testapps.ConsensusMySQLComponent, ctx) + }) - By("delete h-scale ops") - testapps.DeleteObject(&testCtx, opsKey, ops) - Expect(testapps.ChangeObj(&testCtx, ops, func() { - ops.Finalizers = []string{} - })).ShouldNot(HaveOccurred()) + It("HorizontalScaling when not support snapshot", func() { + By("init backup policy template, mysql cluster and hscale ops") + viper.Set("VOLUMESNAPSHOT", false) - By("reset replicas to 1 and cluster should reconcile to Running") + createMysqlCluster(3) + cluster := &appsv1alpha1.Cluster{} + Expect(testCtx.Cli.Get(testCtx.Ctx, clusterKey, cluster)).Should(Succeed()) + initGeneration := cluster.Status.ObservedGeneration + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(Equal(initGeneration)) + + ops := createClusterHscaleOps(5) + opsKey := client.ObjectKeyFromObject(ops) + + By("expect component is Running if don't support volume snapshot during doing h-scale ops") + Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, fetched *appsv1alpha1.Cluster) { + // the cluster spec has been updated by ops-controller to scale out. + g.Expect(fetched.Generation == initGeneration+1).Should(BeTrue()) + // expect cluster phase is Updating during Hscale. + g.Expect(fetched.Generation > fetched.Status.ObservedGeneration).Should(BeTrue()) + g.Expect(fetched.Status.Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + // when snapshot is not supported, the expected component phase is running. + g.Expect(fetched.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + // expect preCheckFailed condition to occur. + condition := meta.FindStatusCondition(fetched.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) + g.Expect(condition).ShouldNot(BeNil()) + g.Expect(condition.Status).Should(BeFalse()) + g.Expect(condition.Reason).Should(Equal(lifecycle.ReasonPreCheckFailed)) + g.Expect(condition.Message).Should(Equal("HorizontalScaleFailed: volume snapshot not support")) + })) + + By("reset replicas to 3 and cluster phase should be reconciled to Running") Expect(testapps.GetAndChangeObj(&testCtx, clusterKey, func(cluster *appsv1alpha1.Cluster) { cluster.Spec.ComponentSpecs[0].Replicas = int32(3) })()).ShouldNot(HaveOccurred()) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, lcluster *appsv1alpha1.Cluster) { + g.Expect(lcluster.Generation == initGeneration+2).Should(BeTrue()) + g.Expect(lcluster.Generation == lcluster.Status.ObservedGeneration).Should(BeTrue()) + g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) + })).Should(Succeed()) }) - }) - Context("with Cluster which has redis Replication", func() { - var podList []*corev1.Pod - var stsList = &appsv1.StatefulSetList{} - - createStsPodAndMockStsReady := func() { - Eventually(testapps.GetListLen(&testCtx, intctrlutil.StatefulSetSignature, - client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterObj.Name, - }, client.InNamespace(clusterObj.Namespace))).Should(BeEquivalentTo(2)) - stsList = testk8s.ListAndCheckStatefulSetWithComponent(&testCtx, client.ObjectKeyFromObject(clusterObj), testapps.DefaultRedisCompName) - for _, v := range stsList.Items { - Expect(testapps.ChangeObjStatus(&testCtx, &v, func() { - testk8s.MockStatefulSetReady(&v) - })).ShouldNot(HaveOccurred()) - podName := v.Name + "-0" - pod := testapps.MockReplicationComponentStsPod(testCtx, &v, clusterObj.Name, testapps.DefaultRedisCompName, podName, v.Labels[constant.RoleLabelKey]) - podList = append(podList, pod) - } - } - BeforeEach(func() { - By("init replication cluster") - // init storageClass - storageClassName := "standard" - testapps.CreateStorageClass(testCtx, storageClassName, true) - clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). - Create(&testCtx).GetObject() + // TODO(refactor): review the case before merge. + It("HorizontalScaling via volume snapshot backup", func() { + By("init backup policy template, mysql cluster and hscale ops") + viper.Set("VOLUMESNAPSHOT", true) + createMysqlCluster(3) - By("Create a clusterVersion obj with replication workloadType.") - clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(testapps.DefaultRedisCompType).AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). - Create(&testCtx).GetObject() + replicas := int32(5) + ops := createClusterHscaleOps(replicas) + opsKey := client.ObjectKeyFromObject(ops) - By("Creating a cluster with replication workloadType.") - pvcSpec := testapps.NewPVCSpec("1Gi") - pvcSpec.StorageClassName = &storageClassName - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, - clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). - AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec).SetPrimaryIndex(0). - SetReplicas(testapps.DefaultReplicationReplicas). - Create(&testCtx).GetObject() - // mock sts ready and create pod - createStsPodAndMockStsReady() - // wait for cluster to running - Eventually(testapps.GetClusterPhase(&testCtx, client.ObjectKeyFromObject(clusterObj))).Should(Equal(appsv1alpha1.RunningClusterPhase)) - }) + By("expect cluster and component is reconciling the new spec") + Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + g.Expect(cluster.Generation == 2).Should(BeTrue()) + g.Expect(cluster.Status.ObservedGeneration == 2).Should(BeTrue()) + // component phase should be running during snapshot backup + // g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + // TODO(REVIEW): component phase is Updating after refactor, does it meet expectations? + g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + // the expected cluster phase is Updating during Hscale. + g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + })).Should(Succeed()) - It("test stop/start ops", func() { - By("Create a stop ops") - stopOpsName := "stop-ops" + testCtx.GetRandomStr() - stopOps := testapps.NewOpsRequestObj(stopOpsName, clusterObj.Namespace, - clusterObj.Name, appsv1alpha1.StopType) - Expect(testCtx.CreateObj(testCtx.Ctx, stopOps)).Should(Succeed()) + By("mock VolumeSnapshot status is ready, component phase should change to Updating when component is horizontally scaling.") + snapshotKey := types.NamespacedName{Name: fmt.Sprintf("%s-%s-scaling", + clusterKey.Name, mysqlCompName), Namespace: testCtx.DefaultNamespace} + volumeSnapshot := &snapshotv1.VolumeSnapshot{} + Expect(k8sClient.Get(testCtx.Ctx, snapshotKey, volumeSnapshot)).Should(Succeed()) + readyToUse := true + volumeSnapshot.Status = &snapshotv1.VolumeSnapshotStatus{ReadyToUse: &readyToUse} + Expect(k8sClient.Status().Update(testCtx.Ctx, volumeSnapshot)).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) + g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + })).Should(Succeed()) - clusterKey = client.ObjectKeyFromObject(clusterObj) - opsKey := client.ObjectKeyFromObject(stopOps) - Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) - // mock deleting pod - for _, pod := range podList { - testk8s.MockPodIsTerminating(ctx, testCtx, pod) - } - // reconcile opsRequest - Expect(testapps.ChangeObj(&testCtx, stopOps, func() { - stopOps.Annotations = map[string]string{ - constant.ReconcileAnnotationKey: time.Now().Format(time.RFC3339Nano), - } - })).ShouldNot(HaveOccurred()) - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.StoppedClusterPhase)) + By("check the underlying workload been updated") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(componentWorkload()), + func(g Gomega, sts *appsv1.StatefulSet) { + g.Expect(*sts.Spec.Replicas).Should(Equal(replicas)) + })).Should(Succeed()) - By("should be Running before pods are not deleted successfully") - Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) - checkLatestOpsIsProcessing(clusterKey, stopOps.Spec.Type) - // mock pod deleted successfully - for _, pod := range podList { - Expect(testapps.ChangeObj(&testCtx, pod, func() { - pod.Finalizers = make([]string, 0) - })).ShouldNot(HaveOccurred()) - } - By("ops phase should be Succeed") - // reconcile opsRequest - Expect(testapps.ChangeObj(&testCtx, stopOps, func() { - stopOps.Annotations = map[string]string{ - constant.ReconcileAnnotationKey: time.Now().Format(time.RFC3339Nano), + By("mock all new PVCs scaled bounded") + for i := 0; i < int(replicas); i++ { + pvcKey := types.NamespacedName{ + Namespace: testCtx.DefaultNamespace, + Name: fmt.Sprintf("%s-%s-%s-%d", testapps.DataVolumeName, clusterKey.Name, mysqlCompName, i), } - })).ShouldNot(HaveOccurred()) - Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsSucceedPhase)) - checkLatestOpsHasProcessed(clusterKey) - - By("test start ops") - startOpsName := "start-ops" + testCtx.GetRandomStr() - startOps := testapps.NewOpsRequestObj(startOpsName, clusterObj.Namespace, - clusterObj.Name, appsv1alpha1.StartType) - opsKey = client.ObjectKeyFromObject(startOps) - Expect(testCtx.CreateObj(testCtx.Ctx, startOps)).Should(Succeed()) - Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) - // mock sts ready and create pod - createStsPodAndMockStsReady() - Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.RunningClusterPhase)) + Expect(testapps.GetAndChangeObjStatus(&testCtx, pvcKey, func(pvc *corev1.PersistentVolumeClaim) { + pvc.Status.Phase = corev1.ClaimBound + })()).Should(Succeed()) + } + + By("check the volumesnapshot created for scaling has been deleted") + Eventually(testapps.CheckObjExists(&testCtx, snapshotKey, volumeSnapshot, false)).Should(Succeed()) + + By("mock component workload is running and expect cluster and component are running") + mockCompRunning(replicas) + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, cluster *appsv1alpha1.Cluster) { + g.Expect(cluster.Status.Components[mysqlCompName].Phase).Should(Equal(appsv1alpha1.RunningClusterCompPhase)) + g.Expect(cluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) + })).Should(Succeed()) Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsSucceedPhase)) }) It("delete Running opsRequest", func() { - By("Create a volume-expand ops") - opsName := "volume-expand" + testCtx.GetRandomStr() - volumeExpandOps := testapps.NewOpsRequestObj(opsName, clusterObj.Namespace, - clusterObj.Name, appsv1alpha1.VolumeExpansionType) - volumeExpandOps.Spec.VolumeExpansionList = []appsv1alpha1.VolumeExpansion{ - { - ComponentOps: appsv1alpha1.ComponentOps{ComponentName: testapps.DefaultRedisCompName}, - VolumeClaimTemplates: []appsv1alpha1.OpsRequestVolumeClaimTemplate{ - { - Name: testapps.DataVolumeName, - Storage: resource.MustParse("3Gi"), - }, - }, - }, - } - Expect(testCtx.CreateObj(testCtx.Ctx, volumeExpandOps)).Should(Succeed()) - clusterKey = client.ObjectKeyFromObject(clusterObj) - opsKey := client.ObjectKeyFromObject(volumeExpandOps) + By("Create a horizontalScaling ops") + viper.Set("VOLUMESNAPSHOT", true) + createMysqlCluster(3) + ops := createClusterHscaleOps(5) + opsKey := client.ObjectKeyFromObject(ops) Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) + + By("check if existing horizontalScaling opsRequest annotation in cluster") Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmlCluster *appsv1alpha1.Cluster) { opsSlice, _ := opsutil.GetOpsRequestSliceFromCluster(tmlCluster) g.Expect(opsSlice).Should(HaveLen(1)) - g.Expect(tmlCluster.Status.Components[testapps.DefaultRedisCompName].Phase).Should(Equal(appsv1alpha1.SpecReconcilingClusterCompPhase)) // VolumeExpandingPhase - // TODO: status conditions for VolumeExpandingPhase + g.Expect(opsSlice[0].Name).Should(Equal(ops.Name)) })).Should(Succeed()) By("delete the Running ops") - testapps.DeleteObject(&testCtx, opsKey, volumeExpandOps) - Expect(testapps.ChangeObj(&testCtx, volumeExpandOps, func() { - volumeExpandOps.Finalizers = []string{} + testapps.DeleteObject(&testCtx, opsKey, ops) + Expect(testapps.ChangeObj(&testCtx, ops, func(lopsReq *appsv1alpha1.OpsRequest) { + lopsReq.SetFinalizers([]string{}) })).ShouldNot(HaveOccurred()) By("check the cluster annotation") @@ -487,5 +553,57 @@ var _ = Describe("OpsRequest Controller", func() { })).Should(Succeed()) }) + It("cancel HorizontalScaling opsRequest which is Running", func() { + By("create cluster and mock it to running") + viper.Set("VOLUMESNAPSHOT", false) + oldReplicas := int32(3) + createMysqlCluster(oldReplicas) + mockCompRunning(oldReplicas) + + By("create a horizontalScaling ops") + ops := createClusterHscaleOps(5) + opsKey := client.ObjectKeyFromObject(ops) + Eventually(testapps.GetOpsRequestPhase(&testCtx, opsKey)).Should(Equal(appsv1alpha1.OpsRunningPhase)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.SpecReconcilingClusterPhase)) + + By("create one pod") + podName := fmt.Sprintf("%s-%s-%d", clusterObj.Name, mysqlCompName, 3) + pod := testapps.MockConsensusComponentStsPod(&testCtx, nil, clusterObj.Name, mysqlCompName, podName, "follower", "Readonly") + + By("cancel the opsRequest") + Eventually(testapps.ChangeObj(&testCtx, ops, func(opsRequest *appsv1alpha1.OpsRequest) { + opsRequest.Spec.Cancel = true + })).Should(Succeed()) + + By("check opsRequest is Cancelling") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, opsRequest *appsv1alpha1.OpsRequest) { + g.Expect(opsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancellingPhase)) + g.Expect(opsRequest.Status.CancelTimestamp.IsZero()).Should(BeFalse()) + cancelCondition := meta.FindStatusCondition(opsRequest.Status.Conditions, appsv1alpha1.ConditionTypeCancelled) + g.Expect(cancelCondition).ShouldNot(BeNil()) + g.Expect(cancelCondition.Reason).Should(Equal(appsv1alpha1.ReasonOpsCanceling)) + })).Should(Succeed()) + + By("delete the created pod") + pod.Kind = constant.PodKind + testk8s.MockPodIsTerminating(ctx, testCtx, pod) + testk8s.RemovePodFinalizer(ctx, testCtx, pod) + + By("opsRequest phase should be Cancelled") + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(ops), func(g Gomega, opsRequest *appsv1alpha1.OpsRequest) { + g.Expect(opsRequest.Status.Phase).Should(Equal(appsv1alpha1.OpsCancelledPhase)) + cancelCondition := meta.FindStatusCondition(opsRequest.Status.Conditions, appsv1alpha1.ConditionTypeCancelled) + g.Expect(cancelCondition).ShouldNot(BeNil()) + g.Expect(cancelCondition.Reason).Should(Equal(appsv1alpha1.ReasonOpsCancelSucceed)) + })).Should(Succeed()) + + By("cluster phase should be Running and delete the opsRequest annotation") + Eventually(testapps.CheckObj(&testCtx, clusterKey, func(g Gomega, tmlCluster *appsv1alpha1.Cluster) { + opsSlice, _ := opsutil.GetOpsRequestSliceFromCluster(tmlCluster) + g.Expect(opsSlice).Should(HaveLen(0)) + g.Expect(tmlCluster.Status.Phase).Should(Equal(appsv1alpha1.RunningClusterPhase)) + })).Should(Succeed()) + }) }) + }) diff --git a/controllers/apps/suite_test.go b/controllers/apps/suite_test.go index 3e8b1cfcc..f6c260791 100644 --- a/controllers/apps/suite_test.go +++ b/controllers/apps/suite_test.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( "context" + "fmt" "go/build" "path/filepath" "testing" @@ -52,6 +56,11 @@ import ( // These tests use Ginkgo (BDD-style Go testing framework). Refer to // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. +const ( + testDataPlaneNodeAffinityKey = "testDataPlaneNodeAffinityKey" + testDataPlaneTolerationKey = "testDataPlaneTolerationKey" +) + var cfg *rest.Config var k8sClient client.Client var testEnv *envtest.Environment @@ -81,6 +90,10 @@ var _ = BeforeSuite(func() { } viper.SetDefault(constant.CfgKeyCtrlrReconcileRetryDurationMS, 10) + viper.Set(constant.CfgKeyDataPlaneTolerations, + fmt.Sprintf("[{\"key\":\"%s\", \"operator\": \"Exists\", \"effect\": \"NoSchedule\"}]", testDataPlaneTolerationKey)) + viper.Set(constant.CfgKeyDataPlaneAffinity, + fmt.Sprintf("{\"nodeAffinity\":{\"preferredDuringSchedulingIgnoredDuringExecution\":[{\"preference\":{\"matchExpressions\":[{\"key\":\"%s\",\"operator\":\"In\",\"values\":[\"true\"]}]},\"weight\":100}]}}", testDataPlaneNodeAffinityKey)) ctx, cancel = context.WithCancel(context.TODO()) logger = logf.FromContext(ctx).WithValues() logger.Info("logger start") @@ -158,18 +171,10 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) - err = (&components.StatefulSetReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("stateful-set-controller"), - }).SetupWithManager(k8sManager) + err = components.NewStatefulSetReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) - err = (&components.DeploymentReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("deployment-controller"), - }).SetupWithManager(k8sManager) + err = components.NewDeploymentReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) err = (&k8score.EventReconciler{ @@ -210,8 +215,17 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) + err = (&ComponentClassReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: k8sManager.GetEventRecorderFor("class-controller"), + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + testCtx = testutil.NewDefaultTestContext(ctx, k8sClient, testEnv) + appsv1alpha1.RegisterWebhookManager(k8sManager) + go func() { defer GinkgoRecover() err = k8sManager.Start(ctx) diff --git a/controllers/apps/systemaccount_controller.go b/controllers/apps/systemaccount_controller.go index 21d002727..15045fd3a 100644 --- a/controllers/apps/systemaccount_controller.go +++ b/controllers/apps/systemaccount_controller.go @@ -1,23 +1,28 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( "context" + "fmt" + "strings" "github.com/go-logr/logr" "github.com/spf13/viper" @@ -25,43 +30,29 @@ import ( corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" "k8s.io/client-go/tools/record" + "k8s.io/client-go/util/workqueue" ctrl "sigs.k8s.io/controller-runtime" - "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/predicate" "sigs.k8s.io/controller-runtime/pkg/source" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/sqlchannel" ) // SystemAccountReconciler reconciles a SystemAccount object. type SystemAccountReconciler struct { client.Client - Scheme *runtime.Scheme - Recorder record.EventRecorder - SecretMapStore *secretMapStore -} - -// jobCompleditionPredicate implements a default delete predicate function on job deletion. -type jobCompletitionPredicate struct { - predicate.Funcs - reconciler *SystemAccountReconciler - Log logr.Logger -} - -// clusterDeletionPredicate implements a default delete predication function on cluster deletion. -// It is used to clean cached secrets from SystemAccountReconciler.SecretMapStore -type clusterDeletionPredicate struct { - predicate.Funcs - reconciler *SystemAccountReconciler - clusterLog logr.Logger + Scheme *runtime.Scheme + Recorder record.EventRecorder } // componentUniqueKey is used internally to uniquely identify a component, by namespace-clusterName-componentName. @@ -69,8 +60,17 @@ type componentUniqueKey struct { namespace string clusterName string componentName string + characterType string } +// updateStrategy is used to specify the update strategy for a component. +type updateStrategy int8 + +const ( + inPlaceUpdate updateStrategy = 1 + reCreate updateStrategy = 2 +) + // SysAccountDeletion and SysAccountCreation are used as event reasons. const ( SysAcctDelete = "SysAcctDelete" @@ -84,25 +84,15 @@ const ( kbAccountEndPointEnvName = "KB_ACCOUNT_ENDPOINT" ) -// username and password are keys in created secrets for others to refer to. -const ( - accountNameForSecret = "username" - accountPasswdForSecret = "password" -) - // ENABLE_DEBUG_SYSACCOUNTS is used for debug only. const ( - systemAccountsDebugMode string = "ENABLE_DEBUG_SYSACCOUNTS" + systemAccountsDebugMode string = "ENABLE_DEBUG_SYSACCOUNTS" + systemAccountPasswdAnnotation string = "passwd" + systemAccountjobPrefix = "sysacc" ) -// compile-time assert that the local data object satisfies the phases data interface. -var _ predicate.Predicate = &jobCompletitionPredicate{} - -// compile-time assert that the local data object satisfies the phases data interface. -var _ predicate.Predicate = &clusterDeletionPredicate{} - var ( - // systemAccountLog is a logger for use during runtime + // systemAccountLog is a logger during runtime systemAccountLog logr.Logger ) @@ -146,7 +136,13 @@ func (r *SystemAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reques } // cluster is under deletion, do nothing if !cluster.GetDeletionTimestamp().IsZero() { - reqCtx.Log.Info("Cluster is under deletion.", "cluster", req.NamespacedName) + reqCtx.Log.V(1).Info("Cluster is under deletion.", "cluster", req.NamespacedName) + return intctrlutil.Reconciled() + } + + // wait till the cluster is running + if cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { + reqCtx.Log.V(1).Info("Cluster is not ready yet", "cluster", req.NamespacedName) return intctrlutil.Reconciled() } @@ -156,41 +152,55 @@ func (r *SystemAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reques return intctrlutil.RequeueWithErrorAndRecordEvent(cluster, r.Recorder, err, reqCtx.Log) } - // wait till the cluster is running - if cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { - reqCtx.Log.V(1).Info("Cluster is not ready yet", "cluster", req.NamespacedName) - return intctrlutil.Reconciled() + clusterVersion := &appsv1alpha1.ClusterVersion{} + if err := r.Client.Get(reqCtx.Ctx, types.NamespacedName{Name: cluster.Spec.ClusterVersionRef}, clusterVersion); err != nil { + return intctrlutil.RequeueWithErrorAndRecordEvent(cluster, r.Recorder, err, reqCtx.Log) } - // process accounts per component + componentVersions := clusterVersion.Spec.GetDefNameMappingComponents() + + // process accounts for each component processAccountsForComponent := func(compDef *appsv1alpha1.ClusterComponentDefinition, compDecl *appsv1alpha1.ClusterComponentSpec, svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints) error { var ( - err error - toCreate appsv1alpha1.KBAccountType - detectedFacts appsv1alpha1.KBAccountType - engine *customizedEngine - compKey = componentUniqueKey{ + err error + toCreate appsv1alpha1.KBAccountType + detectedK8SFacts appsv1alpha1.KBAccountType + detectedEngineFacts appsv1alpha1.KBAccountType + engine *customizedEngine + compKey = componentUniqueKey{ namespace: cluster.Namespace, clusterName: cluster.Name, componentName: compDecl.Name, + characterType: compDef.CharacterType, } ) // expectations: collect accounts from default setting, cluster and cluster definition. toCreate = getDefaultAccounts() - // facts: accounts have been created. - detectedFacts, err = r.getAccountFacts(reqCtx, compKey) - if err != nil { + reqCtx.Log.V(1).Info("accounts to create", "cluster", req.NamespacedName, "accounts", toCreate) + + // facts: accounts have been created, in form of k8s secrets. + if detectedK8SFacts, err = r.getAccountFacts(reqCtx, compKey); err != nil { reqCtx.Log.Error(err, "failed to get secrets") return err } + reqCtx.Log.V(1).Info("detected k8s facts", "cluster", req.NamespacedName, "accounts", detectedK8SFacts) + // toCreate = account to create - account exists - toCreate &= toCreate ^ detectedFacts + // (toCreate \intersect detectedEngineFacts) means the set of account exists in engine but not in k8s, and should be updated or altered, not re-created. + toCreate &= toCreate ^ detectedK8SFacts if toCreate == 0 { return nil } + // facts: accounts have been created in engine. + if detectedEngineFacts, err = r.getEngineFacts(reqCtx, compKey); err != nil { + reqCtx.Log.Error(err, "failed to get accounts", "cluster", cluster.Name, "component", compDecl.Name) + // we don't return error here, because we can still create accounts in k8s and will give it a try. + } + reqCtx.Log.V(1).Info("detected database facts", "cluster", req.NamespacedName, "accounts", detectedEngineFacts) + // replace KubeBlocks ENVs. replaceEnvsValues(cluster.Name, compDef.SystemAccounts) @@ -200,13 +210,21 @@ func (r *SystemAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reques continue } + strategy := reCreate + if detectedEngineFacts&accountID != 0 { + strategy = inPlaceUpdate + } + switch account.ProvisionPolicy.Type { case appsv1alpha1.CreateByStmt: if engine == nil { execConfig := compDef.SystemAccounts.CmdExecutorConfig + // complete execConfig with settings from component version + completeExecConfig(execConfig, componentVersions[compDef.Name]) engine = newCustomizedEngine(execConfig, cluster, compDecl.Name) } - if err := r.createByStmt(reqCtx, cluster, compDef, compKey, engine, account, svcEP, headlessEP); err != nil { + reqCtx.Log.V(1).Info("create account by stmt", "cluster", req.NamespacedName, "account", account.Name, "strategy", strategy) + if err := r.createByStmt(reqCtx, cluster, compDef, compKey, engine, account, svcEP, headlessEP, strategy); err != nil { return err } case appsv1alpha1.ReferToExisting: @@ -255,13 +273,10 @@ func (r *SystemAccountReconciler) Reconcile(ctx context.Context, req ctrl.Reques // SetupWithManager sets up the controller with the Manager. func (r *SystemAccountReconciler) SetupWithManager(mgr ctrl.Manager) error { - r.SecretMapStore = newSecretMapStore() return ctrl.NewControllerManagedBy(mgr). - For(&appsv1alpha1.Cluster{}, builder.WithPredicates(&clusterDeletionPredicate{reconciler: r, clusterLog: systemAccountLog.WithName("clusterDeletionPredicate")})). + For(&appsv1alpha1.Cluster{}). Owns(&corev1.Secret{}). - Watches(&source.Kind{Type: &batchv1.Job{}}, - &handler.EnqueueRequestForObject{}, - builder.WithPredicates(&jobCompletitionPredicate{reconciler: r, Log: log.FromContext(context.TODO())})). + Watches(&source.Kind{Type: &batchv1.Job{}}, r.jobCompletionHander()). Complete(r) } @@ -271,40 +286,48 @@ func (r *SystemAccountReconciler) createByStmt(reqCtx intctrlutil.RequestCtx, compKey componentUniqueKey, engine *customizedEngine, account appsv1alpha1.SystemAccountConfig, - svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints) error { - // render statements - scheme, _ := appsv1alpha1.SchemeBuilder.Build() + svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints, strategy updateStrategy) error { policy := account.ProvisionPolicy - stmts, secret := getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account) - - uprefErr := controllerutil.SetOwnerReference(cluster, secret, scheme) - if uprefErr != nil { - return uprefErr + generateJobName := func() string { + // render a job object, named after account name + randSuffix := rand.String(5) + fullJobName := strings.Join([]string{systemAccountjobPrefix, compKey.clusterName, compKey.componentName, string(account.Name), randSuffix}, "-") + if len(fullJobName) > 63 { + return systemAccountjobPrefix + "-" + string(account.Name) + "-" + randSuffix + } else { + return fullJobName + } } + stmts, passwd := getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account, strategy) + for _, ep := range retrieveEndpoints(policy.Scope, svcEP, headlessEP) { - // render a job object - job := renderJob(engine, compKey, stmts, ep) - // before create job, we adjust job's attributes, such as labels, tolerations w.r.t cluster info. - calibrateJobMetaAndSpec(job, cluster, compKey, account.Name) + job := renderJob(generateJobName(), engine, compKey, stmts, ep) + controllerutil.AddFinalizer(job, constant.DBClusterFinalizerName) + if job.Annotations == nil { + job.Annotations = map[string]string{} + } + job.Annotations[systemAccountPasswdAnnotation] = passwd + + // before creating job, we adjust job's attributes, such as labels, tolerations w.r.t cluster info. + if err := calibrateJobMetaAndSpec(job, cluster, compKey, account.Name); err != nil { + return err + } // update owner reference - if err := controllerutil.SetOwnerReference(cluster, job, scheme); err != nil { + if err := controllerutil.SetControllerReference(cluster, job, r.Scheme); err != nil { return err } // create job if err := r.Client.Create(reqCtx.Ctx, job); err != nil { return err } + reqCtx.Log.V(1).Info("created job", "job", job.Name, "passwd", passwd) } - // push secret to global SecretMapStore, and secret will not be created until job succeeds. - key := concatSecretName(compKey, (string)(account.Name)) - return r.SecretMapStore.addSecret(key, secret) + return nil } func (r *SystemAccountReconciler) createByReferingToExisting(reqCtx intctrlutil.RequestCtx, cluster *appsv1alpha1.Cluster, key componentUniqueKey, account appsv1alpha1.SystemAccountConfig) error { - scheme, _ := appsv1alpha1.SchemeBuilder.Build() - // get secret secret := &corev1.Secret{} secretRef := account.ProvisionPolicy.SecretRef @@ -314,7 +337,7 @@ func (r *SystemAccountReconciler) createByReferingToExisting(reqCtx intctrlutil. } // and make a copy of it newSecret := renderSecretByCopy(key, (string)(account.Name), secret) - if uprefErr := controllerutil.SetOwnerReference(cluster, newSecret, scheme); uprefErr != nil { + if uprefErr := controllerutil.SetControllerReference(cluster, newSecret, r.Scheme); uprefErr != nil { return uprefErr } @@ -341,7 +364,7 @@ func (r *SystemAccountReconciler) isComponentReady(reqCtx intctrlutil.RequestCtx if headlessSvcErr != nil { return false, nil, nil, headlessSvcErr } - // either service or endpoints is not ready. + // Neither service nor endpoints is ready. if len(svcEP.Subsets) == 0 || len(headlessEP.Subsets) == 0 { return false, nil, nil, nil } @@ -354,7 +377,7 @@ func (r *SystemAccountReconciler) isComponentReady(reqCtx intctrlutil.RequestCtx return true, svcEP, headlessEP, nil } -// getAccountFacts parse secrets for given cluster as facts, i.e., accounts created +// getAccountFacts parses secrets for given cluster as facts, i.e., accounts created // TODO: @shanshan, should verify accounts on database cluster as well. func (r *SystemAccountReconciler) getAccountFacts(reqCtx intctrlutil.RequestCtx, key componentUniqueKey) (appsv1alpha1.KBAccountType, error) { // get account facts, i.e., secrets created @@ -372,35 +395,50 @@ func (r *SystemAccountReconciler) getAccountFacts(reqCtx intctrlutil.RequestCtx, } detectedFacts := getAccountFacts(secrets, jobs) + reqCtx.Log.V(1).Info("Detected account facts", "facts", detectedFacts) return detectedFacts, nil } -// Delete implements default DeleteEvent filter on job deletion. -// If the job for creating account completes successfully, corresponding secret will be created. -func (r *jobCompletitionPredicate) Delete(e event.DeleteEvent) bool { - if e.Object == nil { - return false +func (r *SystemAccountReconciler) getEngineFacts(reqCtx intctrlutil.RequestCtx, key componentUniqueKey) (appsv1alpha1.KBAccountType, error) { + // get pods for this cluster-component, by lable + ml := getLabelsForSecretsAndJobs(key) + pods := &corev1.PodList{} + if err := r.Client.List(reqCtx.Ctx, pods, client.InNamespace(key.namespace), ml); err != nil { + return appsv1alpha1.KBAccountInvalid, err } - job, ok := e.Object.(*batchv1.Job) - if !ok { - return false + if len(pods.Items) == 0 { + return appsv1alpha1.KBAccountInvalid, fmt.Errorf("no pods available for cluster: %s, component %s", key.clusterName, key.componentName) + } + // find the first running pod + var target *corev1.Pod + for _, pod := range pods.Items { + if pod.Status.Phase == corev1.PodRunning { + target = &pod + } + } + if target == nil { + return appsv1alpha1.KBAccountInvalid, fmt.Errorf("no pod is running for cluster: %s, component %s", key.clusterName, key.componentName) } - ml := job.ObjectMeta.Labels - accountName, ok := ml[constant.ClusterAccountLabelKey] - if !ok { - return false + sqlChanClient, err := sqlchannel.NewClientWithPod(target, key.characterType) + if err != nil { + return appsv1alpha1.KBAccountInvalid, err } - clusterName, ok := ml[constant.AppInstanceLabelKey] - if !ok { - return false + accounts, err := sqlChanClient.GetSystemAccounts() + if err != nil { + return appsv1alpha1.KBAccountInvalid, err } - componentName, ok := ml[constant.KBAppComponentLabelKey] - if !ok { - return false + accountsID := appsv1alpha1.KBAccountInvalid + for _, acc := range accounts { + updateFacts((appsv1alpha1.AccountName(acc)), &accountsID) } + return accountsID, nil +} + +func (r *SystemAccountReconciler) jobCompletionHander() *handler.Funcs { + logger := systemAccountLog.WithName("jobCompletionHandler") - containsJobCondition := func(jobConditions []batchv1.JobCondition, + containsJobCondition := func(job batchv1.Job, jobConditions []batchv1.JobCondition, jobCondType batchv1.JobConditionType, jobCondStatus corev1.ConditionStatus) bool { for _, jobCond := range job.Status.Conditions { if jobCond.Type == jobCondType && jobCond.Status == jobCondStatus { @@ -410,78 +448,94 @@ func (r *jobCompletitionPredicate) Delete(e event.DeleteEvent) bool { return false } - // job failed, reconcile - if !containsJobCondition(job.Status.Conditions, batchv1.JobComplete, corev1.ConditionTrue) { - return true - } + // check against a job to make sure it + // 1. works for sysaccount (by checking labels) + // 2. has completed (either successed or failed) + // 3. is under deletion (either by user or by TTL, where deletionTimestamp is set) + return &handler.Funcs{ + UpdateFunc: func(e event.UpdateEvent, q workqueue.RateLimitingInterface) { + var ( + jobTerminated = false + job *batchv1.Job + ok bool + ) + + defer func() { + // prepare a patch by removing finalizer + if jobTerminated { + patch := client.MergeFrom(job.DeepCopy()) + controllerutil.RemoveFinalizer(job, constant.DBClusterFinalizerName) + _ = r.Client.Patch(context.Background(), job, patch) + } + }() - // job for cluster-component-account succeeded - // create secret for this account - compKey := componentUniqueKey{ - namespace: job.Namespace, - clusterName: clusterName, - componentName: componentName, - } - key := concatSecretName(compKey, accountName) - entry, ok, err := r.reconciler.SecretMapStore.getSecret(key) - if err != nil || !ok { - return false - } + if e.ObjectNew == nil { + return + } - err = r.reconciler.Client.Create(context.TODO(), entry.value) - if err != nil { - r.Log.Error(err, "failed to create secret, will try later", "secret key", key) - return false - } - clusterKey := types.NamespacedName{Namespace: job.Namespace, Name: clusterName} - cluster := &appsv1alpha1.Cluster{} - if err := r.reconciler.Client.Get(context.TODO(), clusterKey, cluster); err != nil { - r.Log.Error(err, "failed to get cluster", "cluster key", clusterKey) - return false - } else { - r.reconciler.Recorder.Eventf(cluster, corev1.EventTypeNormal, SysAcctCreate, - "Created Accounts for cluster: %s, component: %s, accounts: %s", cluster.Name, componentName, accountName) - // delete secret from cache store - if err = r.reconciler.SecretMapStore.deleteSecret(key); err != nil { - r.Log.Error(err, "failed to delete secret by key", "secret key", key) - } - } - return false -} + if job, ok = e.ObjectNew.(*batchv1.Job); !ok { + return + } -// Delete removes cached entries from SystemAccountReconciler.SecretMapStore -func (r *clusterDeletionPredicate) Delete(e event.DeleteEvent) bool { - if e.Object == nil { - return false - } - cluster, ok := e.Object.(*appsv1alpha1.Cluster) - if !ok { - return false - } + if job.DeletionTimestamp != nil || job.Annotations == nil || job.Labels == nil { + return + } - // for each component from the cluster, delete cached secrets - for _, comp := range cluster.Spec.ComponentSpecs { - compKey := componentUniqueKey{ - namespace: cluster.Namespace, - clusterName: cluster.Name, - componentName: comp.Name, - } - for _, accName := range getAllSysAccounts() { - key := concatSecretName(compKey, string(accName)) - // delete left-over secrets, and ignore errors if it has been removed. - _, exists, err := r.reconciler.SecretMapStore.getSecret(key) - if err != nil { - r.clusterLog.Error(err, "failed to get secrets", "secret key", key) - continue + accountName := job.ObjectMeta.Labels[constant.ClusterAccountLabelKey] + clusterName := job.ObjectMeta.Labels[constant.AppInstanceLabelKey] + componentName := job.ObjectMeta.Labels[constant.KBAppComponentLabelKey] + + if len(accountName) == 0 || len(clusterName) == 0 || len(componentName) == 0 { + return } - if !exists { - continue + + if containsJobCondition(*job, job.Status.Conditions, batchv1.JobFailed, corev1.ConditionTrue) { + logger.V(1).Info("job failed", "job", job.Name) + jobTerminated = true + return } - err = r.reconciler.SecretMapStore.deleteSecret(key) - if err != nil { - r.clusterLog.Error(err, "failed to delete secrets", "secret key", key) + + if !containsJobCondition(*job, job.Status.Conditions, batchv1.JobComplete, corev1.ConditionTrue) { + return } - } + + logger.V(1).Info("job succeeded", "job", job.Name) + jobTerminated = true + clusterKey := types.NamespacedName{Namespace: job.Namespace, Name: clusterName} + cluster := &appsv1alpha1.Cluster{} + if err := r.Client.Get(context.TODO(), clusterKey, cluster); err != nil { + logger.Error(err, "failed to get cluster", "cluster key", clusterKey) + return + } + + compKey := componentUniqueKey{ + namespace: job.Namespace, + clusterName: clusterName, + componentName: componentName, + } + + // get password from job + passwd := job.Annotations[systemAccountPasswdAnnotation] + secret := renderSecretWithPwd(compKey, accountName, passwd) + if err := controllerutil.SetControllerReference(cluster, secret, r.Scheme); err != nil { + logger.Error(err, "failed to set ownere reference for secret", "secret", secret.Name) + return + } + + if err := r.Client.Create(context.TODO(), secret); err != nil { + logger.Error(err, "failed to create secret", "secret", secret.Name) + return + } + + r.Recorder.Eventf(cluster, corev1.EventTypeNormal, SysAcctCreate, + "Created Accounts for cluster: %s, component: %s, accounts: %s", cluster.Name, componentName, accountName) + }, } - return false +} + +// existsOperations checks if the cluster is doing operations +func existsOperations(cluster *appsv1alpha1.Cluster) bool { + opsRequestMap, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) + _, isRestoring := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] + return len(opsRequestMap) > 0 || isRestoring } diff --git a/controllers/apps/systemaccount_controller_test.go b/controllers/apps/systemaccount_controller_test.go index 53c88a341..a48a01b08 100644 --- a/controllers/apps/systemaccount_controller_test.go +++ b/controllers/apps/systemaccount_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -24,6 +27,7 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" @@ -39,24 +43,24 @@ import ( var _ = Describe("SystemAccount Controller", func() { const ( - clusterDefName = "test-clusterdef" - clusterVersionName = "test-clusterversion" - clusterNamePrefix = "test-cluster" - mysqlCompType = "replicasets" - mysqlCompTypeWOSysAcct = "wo-sysacct" - mysqlCompName = "mysql" - mysqlCompNameWOSysAcct = "wo-sysacct" - orphanFinalizerName = "orphan" - clusterEndPointsSize = 3 + clusterDefName = "test-clusterdef" + clusterVersionName = "test-clusterversion" + clusterNamePrefix = "test-cluster" + mysqlCompDefName = "replicasets" + mysqlCompTypeWOSysAcctDefName = "wo-sysacct" + mysqlCompName = "mysql" + mysqlCompNameWOSysAcct = "wo-sysacct" + orphanFinalizerName = "orphan" + clusterEndPointsSize = 3 ) /** * To test the behavior of system accounts controller, we conduct following tests: * 1. construct two components, one with all accounts set, and one with none. * 2. create two clusters, one cluster for each component, and verify - * a) the number of secrets, jobs, and cached secrets are as expected + * a) the number of secrets, jobs are as expected * b) secret will be created, once corresponding job succeeds. - * c) secrets, deleted accidentially, will be re-created during next cluster reconciliation round. + * c) secrets, deleted accidentally, will be re-created during next cluster reconciliation round. * * Each test case, used in following IT(integration test), consists of two parts: * a) how to build the test cluster, and @@ -65,9 +69,8 @@ var _ = Describe("SystemAccount Controller", func() { // sysAcctResourceInfo defines the number of jobs and secrets to be created per account. type sysAcctResourceInfo struct { - jobNum int - secretNum int - cachedSecretsNum int + jobNum int + secretNum int } // sysAcctTestCase defines the info to setup test env, cluster and their expected result to verify against. type sysAcctTestCase struct { @@ -84,7 +87,7 @@ var _ = Describe("SystemAccount Controller", func() { ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -98,13 +101,6 @@ var _ = Describe("SystemAccount Controller", func() { testapps.ClearResources(&testCtx, intctrlutil.EndpointsSignature, inNS, ml) } - cleanInternalCache := func() { - secretKeys := systemAccountReconciler.SecretMapStore.ListKeys() - for _, key := range secretKeys { - _ = systemAccountReconciler.SecretMapStore.deleteSecret(key) - } - } - /** * Start of mock functions. **/ @@ -186,25 +182,25 @@ var _ = Describe("SystemAccount Controller", func() { } initSysAccountTestsAndCluster := func(testCases map[string]*sysAcctTestCase) (clustersMap map[string]types.NamespacedName) { - // create clusterdef and cluster verions, but not clusters + // create clusterdef and cluster versions, but not clusters By("Create a clusterDefinition obj") systemAccount := mockSystemAccountsSpec() clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddSystemAccountSpec(systemAccount). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompTypeWOSysAcct). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompTypeWOSysAcctDefName). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - AddComponent(mysqlCompNameWOSysAcct).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompNameWOSysAcct).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() Expect(clusterDefObj).NotTo(BeNil()) Expect(len(testCases)).To(BeNumerically(">", 0)) - // fill the number of secrets, jobs, and cached secrets + // fill the number of secrets, jobs for _, testCase := range testCases { compDef := clusterDefObj.GetComponentDefByName(testCase.componentDefRef) Expect(compDef).NotTo(BeNil()) @@ -214,14 +210,13 @@ var _ = Describe("SystemAccount Controller", func() { if testCase.resourceMap == nil { testCase.resourceMap = make(map[appsv1alpha1.AccountName]sysAcctResourceInfo) } - var jobNum, secretNum, cachedSecretNum int + var jobNum, secretNum int for _, account := range compDef.SystemAccounts.Accounts { name := account.Name policy := account.ProvisionPolicy switch policy.Type { case appsv1alpha1.CreateByStmt: - secretNum = 0 - cachedSecretNum = 1 + secretNum = 1 if policy.Scope == appsv1alpha1.AnyPods { jobNum = 1 } else { @@ -229,13 +224,11 @@ var _ = Describe("SystemAccount Controller", func() { } case appsv1alpha1.ReferToExisting: jobNum = 0 - cachedSecretNum = 0 secretNum = 1 } testCase.resourceMap[name] = sysAcctResourceInfo{ - jobNum: jobNum, - cachedSecretsNum: cachedSecretNum, - secretNum: secretNum, + jobNum: jobNum, + secretNum: secretNum, } } } @@ -278,32 +271,28 @@ var _ = Describe("SystemAccount Controller", func() { cleanEnv() DeferCleanup(cleanEnv) - cleanInternalCache() - DeferCleanup(cleanInternalCache) - // setup testcase mysqlTestCases = map[string]*sysAcctTestCase{ "wesql-no-accts": { componentName: mysqlCompNameWOSysAcct, - componentDefRef: mysqlCompTypeWOSysAcct, + componentDefRef: mysqlCompTypeWOSysAcctDefName, accounts: []appsv1alpha1.AccountName{}, }, "wesql-with-accts": { componentName: mysqlCompName, - componentDefRef: mysqlCompType, + componentDefRef: mysqlCompDefName, accounts: getAllSysAccounts(), }, } clustersMap = initSysAccountTestsAndCluster(mysqlTestCases) }) - It("Should create jobs and cache secrets as expected for each test case", func() { + It("Should create jobs and secrets as expected for each test case", func() { for testName, testCase := range mysqlTestCases { var ( - acctList appsv1alpha1.KBAccountType - jobsNum int - secretsNum int - cachedSecretNum int + acctList appsv1alpha1.KBAccountType + jobsNum int + secretsNum int ) for _, acc := range testCase.accounts { @@ -311,7 +300,6 @@ var _ = Describe("SystemAccount Controller", func() { acctList |= acc.GetAccountID() jobsNum += resource.jobNum secretsNum += resource.secretNum - cachedSecretNum += resource.cachedSecretsNum } clusterKey, ok := clustersMap[testName] @@ -325,9 +313,9 @@ var _ = Describe("SystemAccount Controller", func() { ml := getLabelsForSecretsAndJobs(componentUniqueKey{namespace: cluster.Namespace, clusterName: cluster.Name, componentName: testCase.componentName}) - if secretsNum == 0 && jobsNum == 0 && cachedSecretNum == 0 { + if secretsNum == 0 && jobsNum == 0 { By("No accouts should be create for test case: " + testName) - // verify nothing will be created or cached till timeout + // verify nothing will be created till timeout Consistently(func(g Gomega) { accounts := getAccounts(g, cluster, ml) g.Expect(accounts).To(BeEquivalentTo(acctList)) @@ -341,12 +329,7 @@ var _ = Describe("SystemAccount Controller", func() { g.Expect(accounts).To(BeEquivalentTo(acctList)) }).Should(Succeed()) - By("Assure some secrets have been cached") - Eventually(func() int { - return len(systemAccountReconciler.SecretMapStore.ListKeys()) - }).Should(BeEquivalentTo(cachedSecretNum)) - - By("Verify all jobs created have their lables set correctly") + By("Verify all jobs created have their labels set correctly") // get all jobs Eventually(func(g Gomega) { // all jobs matching filter `ml` should be a job for sys account. @@ -363,13 +346,12 @@ var _ = Describe("SystemAccount Controller", func() { } }) - It("Cached secrets should be created when jobs succeeds", func() { + It("Secrets should be created when jobs succeeds", func() { for testName, testCase := range mysqlTestCases { var ( - acctList appsv1alpha1.KBAccountType - jobsNum int - secretsNum int - cachedSecretNum int + acctList appsv1alpha1.KBAccountType + jobsNum int + secretsNum int ) for _, acc := range testCase.accounts { @@ -377,10 +359,9 @@ var _ = Describe("SystemAccount Controller", func() { acctList |= acc.GetAccountID() jobsNum += resource.jobNum secretsNum += resource.secretNum - cachedSecretNum += resource.cachedSecretsNum } - if secretsNum == 0 && jobsNum == 0 && cachedSecretNum == 0 { + if secretsNum == 0 && jobsNum == 0 { continue } // get a cluster instance from map, created during preparation @@ -411,9 +392,6 @@ var _ = Describe("SystemAccount Controller", func() { g.Expect(len(jobs.Items)).To(BeEquivalentTo(jobsNum)) }).Should(Succeed()) - By("Verify secrets cached are correct") - Eventually(len(systemAccountReconciler.SecretMapStore.ListKeys())).Should(BeEquivalentTo(cachedSecretNum)) - // wait for a while till all jobs are created By("Mock all jobs are completed and deleted") Eventually(func(g Gomega) { @@ -436,7 +414,9 @@ var _ = Describe("SystemAccount Controller", func() { jobs := &batchv1.JobList{} g.Expect(k8sClient.List(ctx, jobs, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) for _, job := range jobs.Items { - g.Expect(testapps.ChangeObj(&testCtx, &job, func() { controllerutil.RemoveFinalizer(&job, orphanFinalizerName) })).To(Succeed()) + g.Expect(testapps.ChangeObj(&testCtx, &job, func(ljob *batchv1.Job) { + controllerutil.RemoveFinalizer(ljob, orphanFinalizerName) + })).To(Succeed()) } g.Expect(len(jobs.Items)).To(Equal(0), "Verify all jobs completed and deleted") }).Should(Succeed()) @@ -445,25 +425,23 @@ var _ = Describe("SystemAccount Controller", func() { Eventually(func(g Gomega) { secrets := &corev1.SecretList{} g.Expect(k8sClient.List(ctx, secrets, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) - g.Expect(len(secrets.Items)).To(BeEquivalentTo(secretsNum + cachedSecretNum)) + g.Expect(len(secrets.Items)).To(BeEquivalentTo(secretsNum)) }).Should(Succeed()) - By("Verify all secrets created have their finalizer and lables set correctly") - // get all secrets, and check their lables and finalizer + By("Verify all secrets created have their finalizer and labels set correctly") + // get all secrets, and check their labels and finalizer Eventually(func(g Gomega) { // get secrets matching filter secretsForAcct := &corev1.SecretList{} g.Expect(k8sClient.List(ctx, secretsForAcct, ml)).To(Succeed()) for _, secret := range secretsForAcct.Items { // each secret has finalizer - g.Expect(controllerutil.ContainsFinalizer(&secret, dbClusterFinalizerName)).To(BeTrue()) + g.Expect(controllerutil.ContainsFinalizer(&secret, constant.DBClusterFinalizerName)).To(BeTrue()) g.Expect(len(secret.ObjectMeta.OwnerReferences)).To(BeEquivalentTo(1)) g.Expect(checkOwnerReferenceToObj(secret.OwnerReferences[0], cluster)).To(BeTrue()) } }).Should(Succeed()) } - // all jobs succeeded, and there should be no cached secrets left behind. - Expect(len(systemAccountReconciler.SecretMapStore.ListKeys())).To(BeEquivalentTo(0)) }) }) // end of context @@ -477,19 +455,16 @@ var _ = Describe("SystemAccount Controller", func() { cleanEnv() DeferCleanup(cleanEnv) - cleanInternalCache() - DeferCleanup(cleanInternalCache) - // setup testcase mysqlTestCases = map[string]*sysAcctTestCase{ "wesql-with-accts": { componentName: mysqlCompName, - componentDefRef: mysqlCompType, + componentDefRef: mysqlCompDefName, accounts: getAllSysAccounts(), }, "wesql-with-accts-dup": { componentName: mysqlCompName, - componentDefRef: mysqlCompType, + componentDefRef: mysqlCompDefName, accounts: getAllSysAccounts(), }, } @@ -498,13 +473,12 @@ var _ = Describe("SystemAccount Controller", func() { }) It("Should clear relevant expectations and secrets after cluster deletion", func() { - var totalJobs, totalSecrets, totalCachedSecrets int + var totalJobs, totalSecrets int for testName, testCase := range mysqlTestCases { var ( - acctList appsv1alpha1.KBAccountType - jobsNum int - secretsNum int - cachedSecretNum int + acctList appsv1alpha1.KBAccountType + jobsNum int + secretsNum int ) for _, acc := range testCase.accounts { @@ -512,11 +486,9 @@ var _ = Describe("SystemAccount Controller", func() { acctList |= acc.GetAccountID() jobsNum += resource.jobNum secretsNum += resource.secretNum - cachedSecretNum += resource.cachedSecretsNum } totalJobs += jobsNum totalSecrets += secretsNum - totalCachedSecrets += cachedSecretNum // get a cluster instance from map, created during preparation clusterKey, ok := clustersMap[testName] @@ -543,31 +515,22 @@ var _ = Describe("SystemAccount Controller", func() { }).Should(Succeed()) } - By("Verify secrets and jobs size") - Eventually(func(g Gomega) { - g.Expect(len(systemAccountReconciler.SecretMapStore.ListKeys())).To(BeEquivalentTo(totalCachedSecrets), "before delete, there are %d cached secrets", totalCachedSecrets) - }).Should(Succeed()) - clusterKeys := make([]types.NamespacedName, 0, len(clustersMap)) for _, v := range clustersMap { clusterKeys = append(clusterKeys, v) } - By("Delete 0-th cluster from list, there should be no change in cached secrets size") + By("Delete 0-th cluster from list, there should be no change in secrets size") cluster := &appsv1alpha1.Cluster{} Expect(k8sClient.Get(ctx, clusterKeys[0], cluster)).To(Succeed()) Expect(k8sClient.Delete(ctx, cluster)).To(Succeed()) - By("Delete remaining cluster before jobs are done, all cached secrets should be removed") + By("Delete remaining cluster before jobs are done, all secrets should be removed") for i := 1; i < len(clusterKeys); i++ { cluster = &appsv1alpha1.Cluster{} Expect(k8sClient.Get(ctx, clusterKeys[i], cluster)).To(Succeed()) Expect(k8sClient.Delete(ctx, cluster)).To(Succeed()) } - - Eventually(func(g Gomega) { - g.Expect(len(systemAccountReconciler.SecretMapStore.ListKeys())).To(BeEquivalentTo(0)) - }).Should(Succeed()) }) }) // end of context @@ -581,19 +544,16 @@ var _ = Describe("SystemAccount Controller", func() { cleanEnv() DeferCleanup(cleanEnv) - cleanInternalCache() - DeferCleanup(cleanInternalCache) - // setup testcase mysqlTestCases = map[string]*sysAcctTestCase{ "wesql-with-accts": { componentName: mysqlCompName, - componentDefRef: mysqlCompType, + componentDefRef: mysqlCompDefName, accounts: getAllSysAccounts(), }, "wesql-with-accts-dup": { componentName: mysqlCompName, - componentDefRef: mysqlCompType, + componentDefRef: mysqlCompDefName, accounts: getAllSysAccounts(), }, } @@ -632,10 +592,6 @@ var _ = Describe("SystemAccount Controller", func() { jobs := &batchv1.JobList{} g.Expect(k8sClient.List(ctx, jobs, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) g.Expect(len(jobs.Items)).To(BeEquivalentTo(jobsNum)) - - secrets := &corev1.SecretList{} - g.Expect(k8sClient.List(ctx, secrets, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) - g.Expect(len(secrets.Items)).To(BeEquivalentTo(secretsNum)) }).Should(Succeed()) By("Enable monitor, no more jobs or secrets should be created") @@ -650,13 +606,8 @@ var _ = Describe("SystemAccount Controller", func() { Eventually(func(g Gomega) { g.Expect(k8sClient.List(ctx, jobs, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) g.Expect(len(jobs.Items)).To(BeEquivalentTo(jobsNum)) - // nothing changed since last time updates - secrets := &corev1.SecretList{} - g.Expect(k8sClient.List(ctx, secrets, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) - g.Expect(len(secrets.Items)).To(BeEquivalentTo(secretsNum)) }).Should(Succeed()) - cachedSecretNum := len(systemAccountReconciler.SecretMapStore.ListKeys()) By("Mark partial jobs as completed and make sure it cannot be found") // mark one jobs as completed if jobsNum < 2 { @@ -671,11 +622,15 @@ var _ = Describe("SystemAccount Controller", func() { Eventually(func(g Gomega) { tmpJob := &batchv1.Job{} g.Expect(k8sClient.Get(ctx, jobKey, tmpJob)).To(Succeed()) - g.Expect(len(tmpJob.ObjectMeta.Finalizers)).To(BeEquivalentTo(1)) - g.Expect(testapps.ChangeObj(&testCtx, tmpJob, func() { controllerutil.RemoveFinalizer(tmpJob, orphanFinalizerName) })).To(Succeed()) + g.Expect(len(tmpJob.ObjectMeta.Finalizers)).To(BeEquivalentTo(2)) + g.Expect(testapps.ChangeObj(&testCtx, tmpJob, func(ljob *batchv1.Job) { + controllerutil.RemoveFinalizer(ljob, orphanFinalizerName) + controllerutil.RemoveFinalizer(ljob, constant.DBClusterFinalizerName) + })).To(Succeed()) }).Should(Succeed()) - By("Verify jobs size decreased and secrets size increased") + By("Verify jobs size decreased and secrets size does not increase") + var secretsLen int Eventually(func(g Gomega) { g.Expect(k8sClient.List(ctx, jobs, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) jobSize2 := len(jobs.Items) @@ -683,15 +638,11 @@ var _ = Describe("SystemAccount Controller", func() { secrets := &corev1.SecretList{} g.Expect(k8sClient.List(ctx, secrets, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) - secretsSize2 := len(secrets.Items) - g.Expect(secretsSize2).To(BeEquivalentTo(secretsNum)) - - cachedSecretsSize2 := len(systemAccountReconciler.SecretMapStore.ListKeys()) - g.Expect(cachedSecretsSize2).To(BeEquivalentTo(cachedSecretNum)) + secretsLen = len(secrets.Items) }).Should(Succeed()) // delete one job directly, but the job is completed. - By("Delete one job directly, the system should not create new secrets.") + By("Delete one job and mark it as JobComplete, the system should create new secrets.") jobKey = client.ObjectKeyFromObject(&jobs.Items[0]) Eventually(func(g Gomega) { tmpJob := &batchv1.Job{} @@ -702,15 +653,16 @@ var _ = Describe("SystemAccount Controller", func() { Status: corev1.ConditionTrue, }} })).To(Succeed()) - g.Expect(k8sClient.Delete(ctx, tmpJob)).To(Succeed()) + g.Expect(k8sClient.Delete(ctx, tmpJob)).Should(Succeed()) + g.Expect(testapps.ChangeObj(&testCtx, tmpJob, func(ljob *batchv1.Job) { + controllerutil.RemoveFinalizer(ljob, orphanFinalizerName) + })).To(Succeed()) + }).Should(Succeed()) Eventually(func(g Gomega) { - tmpJob := &batchv1.Job{} - err := k8sClient.Get(ctx, jobKey, tmpJob) - g.Expect(err).To(Succeed()) - g.Expect(len(tmpJob.ObjectMeta.Finalizers)).To(BeEquivalentTo(1)) - g.Expect(testapps.ChangeObj(&testCtx, tmpJob, func() { controllerutil.RemoveFinalizer(tmpJob, orphanFinalizerName) })).To(Succeed()) + err := k8sClient.Get(ctx, jobKey, &batchv1.Job{}) + g.Expect(apierrors.IsNotFound(err)).To(BeTrue()) }).Should(Succeed()) By("Verify jobs size decreased and secrets size increased") @@ -718,10 +670,7 @@ var _ = Describe("SystemAccount Controller", func() { secrets := &corev1.SecretList{} g.Expect(k8sClient.List(ctx, secrets, client.InNamespace(cluster.Namespace), ml)).To(Succeed()) secretsSize2 := len(secrets.Items) - g.Expect(secretsSize2).To(BeNumerically(">", secretsNum)) - - cachedSecretsSize2 := len(systemAccountReconciler.SecretMapStore.ListKeys()) - g.Expect(cachedSecretsSize2).To(BeNumerically("<", cachedSecretNum)) + g.Expect(secretsSize2).To(BeNumerically(">", secretsLen)) }).Should(Succeed()) } }) diff --git a/controllers/apps/systemaccount_util.go b/controllers/apps/systemaccount_util.go index 0f6d5c8fc..7dfdf881b 100644 --- a/controllers/apps/systemaccount_util.go +++ b/controllers/apps/systemaccount_util.go @@ -1,23 +1,25 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( - "fmt" "strconv" "strings" @@ -26,7 +28,6 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/cache" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -34,21 +35,6 @@ import ( componetutil "github.com/apecloud/kubeblocks/internal/controller/component" ) -const ( - jobPrefix = "job-system-account-" -) - -// SecretMapStore is a cache, recording all (key, secret) pair for accounts to be created. -type secretMapStore struct { - cache.Store -} - -// SecretMapEntry records (key, secret) pair for account to be created. -type secretMapEntry struct { - key string - value *corev1.Secret -} - // customizedEngine helps render jobs. type customizedEngine struct { cluster *appsv1alpha1.Cluster @@ -59,52 +45,6 @@ type customizedEngine struct { envVarList []corev1.EnvVar } -// SecretKeyFunc to parse out the key from a SecretMapEntry. -var secretKeyFunc = func(obj interface{}) (string, error) { - if e, ok := obj.(*secretMapEntry); ok { - return e.key, nil - } - return "", fmt.Errorf("could not find key for obj %#v", obj) -} - -func newSecretMapStore() *secretMapStore { - return &secretMapStore{cache.NewStore(secretKeyFunc)} -} - -func (r *secretMapStore) addSecret(key string, value *corev1.Secret) error { - _, exists, err := r.getSecret(key) - if err != nil { - return err - } - entry := &secretMapEntry{key: key, value: value} - if exists { - return r.Update(entry) - } - return r.Add(entry) -} - -func (r *secretMapStore) getSecret(key string) (*secretMapEntry, bool, error) { - exp, exists, err := r.GetByKey(key) - if err != nil { - return nil, false, err - } - if exists { - return exp.(*secretMapEntry), true, nil - } - return nil, false, nil -} - -func (r *secretMapStore) deleteSecret(key string) error { - exp, exist, err := r.GetByKey(key) - if err != nil { - return err - } - if exist { - return r.Delete(exp) - } - return nil -} - func (e *customizedEngine) getImage() string { return e.image } @@ -157,7 +97,7 @@ func replaceEnvsValues(clusterName string, sysAccounts *appsv1alpha1.SystemAccou } } -// getLabelsForSecretsAndJobs construct matching labels for secrets and jobs. +// getLabelsForSecretsAndJobs constructs matching labels for secrets and jobs. // This is consistent with that of secrets created during cluster initialization. func getLabelsForSecretsAndJobs(key componentUniqueKey) client.MatchingLabels { return client.MatchingLabels{ @@ -167,10 +107,7 @@ func getLabelsForSecretsAndJobs(key componentUniqueKey) client.MatchingLabels { } } -func renderJob(engine *customizedEngine, key componentUniqueKey, statement []string, endpoint string) *batchv1.Job { - randomStr, _ := password.Generate(6, 0, 0, true, false) - jobName := jobPrefix + key.clusterName + "-" + randomStr - +func renderJob(jobName string, engine *customizedEngine, key componentUniqueKey, statement []string, endpoint string) *batchv1.Job { // inject one more system env variables statementEnv := corev1.EnvVar{ Name: kbAccountStmtEnvName, @@ -183,7 +120,9 @@ func renderJob(engine *customizedEngine, key componentUniqueKey, statement []str // place statements and endpoints before user defined envs. envs := make([]corev1.EnvVar, 0, 2+len(engine.getEnvs())) envs = append(envs, statementEnv, endpointEnv) - envs = append(envs, engine.getEnvs()...) + if len(engine.getEnvs()) > 0 { + envs = append(envs, engine.getEnvs()...) + } job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ @@ -199,7 +138,7 @@ func renderJob(engine *customizedEngine, key componentUniqueKey, statement []str RestartPolicy: corev1.RestartPolicyNever, Containers: []corev1.Container{ { - Name: randomStr, + Name: jobName, Image: engine.getImage(), ImagePullPolicy: corev1.PullIfNotPresent, Command: engine.getCommand(), @@ -216,9 +155,10 @@ func renderJob(engine *customizedEngine, key componentUniqueKey, statement []str } func renderSecretWithPwd(key componentUniqueKey, username, passwd string) *corev1.Secret { - secretData := map[string][]byte{} - secretData[accountNameForSecret] = []byte(username) - secretData[accountPasswdForSecret] = []byte(passwd) + secretData := map[string][]byte{ + constant.AccountNameForSecret: []byte(username), + constant.AccountPasswdForSecret: []byte(passwd), + } ml := getLabelsForSecretsAndJobs(key) ml[constant.ClusterAccountLabelKey] = username @@ -232,21 +172,20 @@ func renderSecretByCopy(key componentUniqueKey, username string, fromSecret *cor } func renderSecret(key componentUniqueKey, username string, labels client.MatchingLabels, data map[string][]byte) *corev1.Secret { - // secret labels and secret fianlizers should be consistent with that of Cluster secret created by Cluster Controller. + // secret labels and secret finalizers should be consistent with that of Cluster secret created by Cluster Controller. secret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Namespace: key.namespace, Name: strings.Join([]string{key.clusterName, key.componentName, username}, "-"), Labels: labels, - Finalizers: []string{dbClusterFinalizerName}, + Finalizers: []string{constant.DBClusterFinalizerName}, }, Data: data, } return secret } -func retrieveEndpoints(scope appsv1alpha1.ProvisionScope, - svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints) []string { +func retrieveEndpoints(scope appsv1alpha1.ProvisionScope, svcEP *corev1.Endpoints, headlessEP *corev1.Endpoints) []string { // parse endpoints endpoints := make([]string, 0) if scope == appsv1alpha1.AnyPods { @@ -298,15 +237,11 @@ func updateFacts(accountName appsv1alpha1.AccountName, detectedFacts *appsv1alph } } -func concatSecretName(key componentUniqueKey, username string) string { - return fmt.Sprintf("%s-%s-%s-%s", key.namespace, key.clusterName, key.componentName, username) -} - func getCreationStmtForAccount(key componentUniqueKey, passConfig appsv1alpha1.PasswordConfig, - accountConfig appsv1alpha1.SystemAccountConfig) ([]string, *corev1.Secret) { + accountConfig appsv1alpha1.SystemAccountConfig, strategy updateStrategy) ([]string, string) { // generated password with mixedcases = true passwd, _ := password.Generate((int)(passConfig.Length), (int)(passConfig.NumDigits), (int)(passConfig.NumSymbols), false, false) - // refine pasword to upper or lower cases w.r.t configuration + // refine password to upper or lower cases w.r.t configuration switch passConfig.LetterCase { case appsv1alpha1.UpperCases: passwd = strings.ToUpper(passwd) @@ -319,17 +254,23 @@ func getCreationStmtForAccount(key componentUniqueKey, passConfig appsv1alpha1.P namedVars := getEnvReplacementMapForAccount(userName, passwd) execStmts := make([]string, 0) - // drop if exists + create if not exists + statements := accountConfig.ProvisionPolicy.Statements - if len(statements.DeletionStatement) > 0 { - stmt := componetutil.ReplaceNamedVars(namedVars, statements.DeletionStatement, -1, true) + if strategy == inPlaceUpdate { + // use update statement + stmt := componetutil.ReplaceNamedVars(namedVars, statements.UpdateStatement, -1, true) + execStmts = append(execStmts, stmt) + } else { + // drop if exists + create if not exists + if len(statements.DeletionStatement) > 0 { + stmt := componetutil.ReplaceNamedVars(namedVars, statements.DeletionStatement, -1, true) + execStmts = append(execStmts, stmt) + } + stmt := componetutil.ReplaceNamedVars(namedVars, statements.CreationStatement, -1, true) execStmts = append(execStmts, stmt) } - stmt := componetutil.ReplaceNamedVars(namedVars, statements.CreationStatement, -1, true) - execStmts = append(execStmts, stmt) - - secret := renderSecretWithPwd(key, userName, passwd) - return execStmts, secret + // secret := renderSecretWithPwd(key, userName, passwd) + return execStmts, passwd } func getAllSysAccounts() []appsv1alpha1.AccountName { @@ -355,7 +296,7 @@ func getDebugMode(annotatedDebug string) bool { return viper.GetBool(systemAccountsDebugMode) || debugOn } -func calibrateJobMetaAndSpec(job *batchv1.Job, cluster *appsv1alpha1.Cluster, compKey componentUniqueKey, account appsv1alpha1.AccountName) { +func calibrateJobMetaAndSpec(job *batchv1.Job, cluster *appsv1alpha1.Cluster, compKey componentUniqueKey, account appsv1alpha1.AccountName) error { debugModeOn := getDebugMode(cluster.Annotations[debugClusterAnnotationKey]) // add label ml := getLabelsForSecretsAndJobs(compKey) @@ -366,19 +307,39 @@ func calibrateJobMetaAndSpec(job *batchv1.Job, cluster *appsv1alpha1.Cluster, co if debugModeOn { job.Spec.TTLSecondsAfterFinished = nil } else { - defaultTTLZero := (int32)(0) + defaultTTLZero := (int32)(1) job.Spec.TTLSecondsAfterFinished = &defaultTTLZero } // add toleration - tolerations := cluster.Spec.Tolerations clusterComp := cluster.Spec.GetComponentByName(compKey.componentName) - if clusterComp != nil { - if len(clusterComp.Tolerations) != 0 { - tolerations = clusterComp.Tolerations - } + tolerations, err := componetutil.BuildTolerations(cluster, clusterComp) + if err != nil { + return err } - // add built-in toleration - tolerations = componetutil.PatchBuiltInToleration(tolerations) job.Spec.Template.Spec.Tolerations = tolerations + + return nil +} + +// completeExecConfig overrides the image of execConfig if version is not nil. +func completeExecConfig(execConfig *appsv1alpha1.CmdExecutorConfig, version *appsv1alpha1.ClusterComponentVersion) { + if version == nil || version.SystemAccountSpec == nil || version.SystemAccountSpec.CmdExecutorConfig == nil { + return + } + sysAccountSpec := version.SystemAccountSpec + if len(sysAccountSpec.CmdExecutorConfig.Image) > 0 { + execConfig.Image = sysAccountSpec.CmdExecutorConfig.Image + } + + // envs from sysAccountSpec will override the envs from execConfig + if sysAccountSpec.CmdExecutorConfig.Env == nil { + return + } + if len(sysAccountSpec.CmdExecutorConfig.Env) == 0 { + // clean up envs + execConfig.Env = nil + } else { + execConfig.Env = sysAccountSpec.CmdExecutorConfig.Env + } } diff --git a/controllers/apps/systemaccount_util_test.go b/controllers/apps/systemaccount_util_test.go index 8d77fa352..78664c6a7 100644 --- a/controllers/apps/systemaccount_util_test.go +++ b/controllers/apps/systemaccount_util_test.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( "math/rand" + "reflect" "strings" "testing" @@ -54,6 +58,7 @@ func mockSystemAccountsSpec() *appsv1alpha1.SystemAccountSpec { PasswordConfig: pwdConfig, Accounts: []appsv1alpha1.SystemAccountConfig{}, } + var account appsv1alpha1.SystemAccountConfig var scope appsv1alpha1.ProvisionScope for _, name := range getAllSysAccounts() { @@ -81,6 +86,7 @@ func mockCreateByStmtSystemAccount(name appsv1alpha1.AccountName) appsv1alpha1.S Type: appsv1alpha1.CreateByStmt, Statements: &appsv1alpha1.ProvisionStatements{ CreationStatement: "CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY \"$(PASSWD)\";", + UpdateStatement: "ALTER USER $(USERNAME) IDENTIFIED BY \"$(PASSWD)\";", DeletionStatement: "DROP USER IF EXISTS $(USERNAME);", }, }, @@ -146,20 +152,20 @@ func TestRenderJob(t *testing.T) { clusterDefName = "test-clusterdef" clusterVersionName = "test-clusterversion" clusterNamePrefix = "test-cluster" - mysqlCompType = "replicasets" + mysqlCompDefName = "replicasets" mysqlCompName = "mysql" ) systemAccount := mockSystemAccountsSpec() clusterDef := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddSystemAccountSpec(systemAccount). GetObject() assert.NotNil(t, clusterDef) assert.NotNil(t, clusterDef.Spec.ComponentDefs[0].SystemAccounts) cluster := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDef.Name, clusterVersionName). - AddComponent(mysqlCompType, mysqlCompName).GetObject() + AddComponent(mysqlCompDefName, mysqlCompName).GetObject() assert.NotNil(t, cluster) if cluster.Annotations == nil { cluster.Annotations = make(map[string]string, 0) @@ -195,7 +201,7 @@ func TestRenderJob(t *testing.T) { for _, acc := range accountsSetting.Accounts { switch acc.ProvisionPolicy.Type { case appsv1alpha1.CreateByStmt: - creationStmt, secrets := getCreationStmtForAccount(compKey, accountsSetting.PasswordConfig, acc) + creationStmt, secrets := getCreationStmtForAccount(compKey, accountsSetting.PasswordConfig, acc, reCreate) // make sure all variables have been replaced for _, stmt := range creationStmt { assert.False(t, strings.Contains(stmt, "$(USERNAME)")) @@ -203,20 +209,21 @@ func TestRenderJob(t *testing.T) { } // render job with debug mode off endpoint := "10.0.0.1" - job := renderJob(engine, compKey, creationStmt, endpoint) + mockJobName := "mock-job" + testCtx.GetRandomStr() + job := renderJob(mockJobName, engine, compKey, creationStmt, endpoint) assert.NotNil(t, job) - calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) + _ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) assert.NotNil(t, job.Spec.TTLSecondsAfterFinished) - assert.Equal(t, (int32)(0), *job.Spec.TTLSecondsAfterFinished) + assert.Equal(t, (int32)(1), *job.Spec.TTLSecondsAfterFinished) envList := job.Spec.Template.Spec.Containers[0].Env assert.GreaterOrEqual(t, len(envList), 1) assert.Equal(t, job.Spec.Template.Spec.Containers[0].Image, cmdExecutorConfig.Image) // render job with debug mode on - job = renderJob(engine, compKey, creationStmt, endpoint) + job = renderJob(mockJobName, engine, compKey, creationStmt, endpoint) assert.NotNil(t, job) // set debug mode on cluster.Annotations[debugClusterAnnotationKey] = "True" - calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) + _ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) assert.Nil(t, job.Spec.TTLSecondsAfterFinished) assert.NotNil(t, secrets) // set debug mode off @@ -225,9 +232,9 @@ func TestRenderJob(t *testing.T) { toleration := make([]corev1.Toleration, 0) toleration = append(toleration, generateToleration()) cluster.Spec.Tolerations = toleration - job = renderJob(engine, compKey, creationStmt, endpoint) + job = renderJob(mockJobName, engine, compKey, creationStmt, endpoint) assert.NotNil(t, job) - calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) + _ = calibrateJobMetaAndSpec(job, cluster, compKey, acc.Name) jobToleration := job.Spec.Template.Spec.Tolerations assert.Equal(t, 2, len(jobToleration)) // make sure the toleration is added to job and contains our built-in toleration @@ -235,10 +242,10 @@ func TestRenderJob(t *testing.T) { for _, t := range jobToleration { tolerationKeys = append(tolerationKeys, t.Key) } - assert.Contains(t, tolerationKeys, constant.KubeBlocksDataNodeTolerationKey) + assert.Contains(t, tolerationKeys, testDataPlaneTolerationKey) assert.Contains(t, tolerationKeys, toleration[0].Key) case appsv1alpha1.ReferToExisting: - assert.False(t, strings.Contains(acc.ProvisionPolicy.SecretRef.Name, constant.ConnCredentialPlaceHolder)) + assert.False(t, strings.Contains(acc.ProvisionPolicy.SecretRef.Name, constant.KBConnCredentialPlaceHolder)) } } } @@ -306,20 +313,20 @@ func TestAccountDebugMode(t *testing.T) { func TestRenderCreationStmt(t *testing.T) { var ( - clusterDefName = "test-clusterdef" - clusterName = "test-cluster" - mysqlCompType = "replicasets" - mysqlCompName = "mysql" + clusterDefName = "test-clusterdef" + clusterName = "test-cluster" + mysqlCompDefName = "replicasets" + mysqlCompName = "mysql" ) systemAccount := mockSystemAccountsSpec() clusterDef := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddSystemAccountSpec(systemAccount). GetObject() assert.NotNil(t, clusterDef) - compDef := clusterDef.GetComponentDefByName(mysqlCompType) + compDef := clusterDef.GetComponentDefByName(mysqlCompDefName) assert.NotNil(t, compDef.SystemAccounts) accountsSetting := compDef.SystemAccounts @@ -332,7 +339,7 @@ func TestRenderCreationStmt(t *testing.T) { } for _, account := range accountsSetting.Accounts { - // for each accounts, we randomly remove deletion stmt + // for each account, we randomly remove deletion stmt if account.ProvisionPolicy.Type == appsv1alpha1.CreateByStmt { toss := rand.Intn(10) % 2 if toss == 1 { @@ -340,13 +347,103 @@ func TestRenderCreationStmt(t *testing.T) { account.ProvisionPolicy.Statements.DeletionStatement = "" } - stmts, secret := getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account) + stmts, secret := getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account, reCreate) if toss == 1 { assert.Equal(t, 1, len(stmts)) } else { assert.Equal(t, 2, len(stmts)) } assert.NotNil(t, secret) + + stmts, secret = getCreationStmtForAccount(compKey, compDef.SystemAccounts.PasswordConfig, account, inPlaceUpdate) + assert.Equal(t, 1, len(stmts)) + assert.NotNil(t, secret) } } } + +func TestMergeSystemAccountConfig(t *testing.T) { + systemAccount := mockSystemAccountsSpec() + // Make sure env is not empty + if systemAccount.CmdExecutorConfig.Env == nil { + systemAccount.CmdExecutorConfig.Env = []corev1.EnvVar{} + } + + if len(systemAccount.CmdExecutorConfig.Env) == 0 { + systemAccount.CmdExecutorConfig.Env = append(systemAccount.CmdExecutorConfig.Env, corev1.EnvVar{ + Name: "cluster-def-env", + Value: "cluster-def-env-value", + }) + } + // nil spec + componentVersion := &appsv1alpha1.ClusterComponentVersion{ + SystemAccountSpec: nil, + } + accountConfig := systemAccount.CmdExecutorConfig.DeepCopy() + completeExecConfig(accountConfig, componentVersion) + assert.Equal(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image) + assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env)) + if len(systemAccount.CmdExecutorConfig.Env) > 0 { + assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env)) + } + + // empty spec + accountConfig = systemAccount.CmdExecutorConfig.DeepCopy() + componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{ + CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{}, + } + + completeExecConfig(accountConfig, componentVersion) + assert.Equal(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image) + assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env)) + if len(systemAccount.CmdExecutorConfig.Env) > 0 { + assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env)) + } + + // spec with image + mockImageName := "test-image" + accountConfig = systemAccount.CmdExecutorConfig.DeepCopy() + componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{ + CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{ + Image: mockImageName, + Env: nil, + }, + } + completeExecConfig(accountConfig, componentVersion) + assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image) + assert.Equal(t, mockImageName, accountConfig.Image) + assert.Len(t, accountConfig.Env, len(systemAccount.CmdExecutorConfig.Env)) + if len(systemAccount.CmdExecutorConfig.Env) > 0 { + assert.True(t, reflect.DeepEqual(accountConfig.Env, systemAccount.CmdExecutorConfig.Env)) + } + // spec with empty envs + accountConfig = systemAccount.CmdExecutorConfig.DeepCopy() + componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{ + CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{ + Image: mockImageName, + Env: []corev1.EnvVar{}, + }, + } + completeExecConfig(accountConfig, componentVersion) + assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image) + assert.Equal(t, mockImageName, accountConfig.Image) + assert.Len(t, accountConfig.Env, 0) + + // spec with envs + testEnv := corev1.EnvVar{ + Name: "test-env", + Value: "test-value", + } + accountConfig = systemAccount.CmdExecutorConfig.DeepCopy() + componentVersion.SystemAccountSpec = &appsv1alpha1.SystemAccountShortSpec{ + CmdExecutorConfig: &appsv1alpha1.CommandExecutorEnvItem{ + Image: mockImageName, + Env: []corev1.EnvVar{testEnv}, + }, + } + completeExecConfig(accountConfig, componentVersion) + assert.NotEqual(t, systemAccount.CmdExecutorConfig.Image, accountConfig.Image) + assert.Equal(t, mockImageName, accountConfig.Image) + assert.Len(t, accountConfig.Env, 1) + assert.Contains(t, accountConfig.Env, testEnv) +} diff --git a/controllers/apps/tls_utils_test.go b/controllers/apps/tls_utils_test.go index 6c02e6b30..177c1ea32 100644 --- a/controllers/apps/tls_utils_test.go +++ b/controllers/apps/tls_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -30,6 +33,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/plan" "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" @@ -38,13 +42,13 @@ import ( var _ = Describe("TLS self-signed cert function", func() { const ( - clusterDefName = "test-clusterdef-tls" - clusterVersionName = "test-clusterversion-tls" - clusterNamePrefix = "test-cluster" - statefulCompType = "replicasets" - statefulCompName = "mysql" - mysqlContainerName = "mysql" - configSpecName = "mysql-config-tpl" + clusterDefName = "test-clusterdef-tls" + clusterVersionName = "test-clusterversion-tls" + clusterNamePrefix = "test-cluster" + statefulCompDefName = "mysql" + statefulCompName = "mysql" + mysqlContainerName = "mysql" + configSpecName = "mysql-config-tpl" ) ctx := context.Background() @@ -80,7 +84,8 @@ var _ = Describe("TLS self-signed cert function", func() { configMapObj := testapps.CheckedCreateCustomizedObj(&testCtx, "resources/mysql-tls-config-template.yaml", &corev1.ConfigMap{}, - testCtx.UseDefaultNamespace()) + testCtx.UseDefaultNamespace(), + testapps.WithAnnotations(constant.CMInsEnableRerenderTemplateKey, "true")) configConstraintObj := testapps.CheckedCreateCustomizedObj(&testCtx, "resources/mysql-config-constraint.yaml", @@ -89,14 +94,14 @@ var _ = Describe("TLS self-signed cert function", func() { By("Create a clusterDef obj") testapps.NewClusterDefFactory(clusterDefName). SetConnectionCredential(map[string]string{"username": "root", "password": ""}, nil). - AddComponent(testapps.ConsensusMySQLComponent, statefulCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, statefulCompDefName). AddConfigTemplate(configSpecName, configMapObj.Name, configConstraintObj.Name, testCtx.DefaultNamespace, testapps.ConfVolumeName). AddContainerEnv(mysqlContainerName, corev1.EnvVar{Name: "MYSQL_ALLOW_EMPTY_PASSWORD", Value: "yes"}). CheckedCreate(&testCtx).GetObject() By("Create a clusterVersion obj") testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(statefulCompType).AddContainerShort(mysqlContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(statefulCompDefName).AddContainerShort(mysqlContainerName, testapps.ApeCloudMySQLImage). CheckedCreate(&testCtx).GetObject() }) @@ -124,7 +129,7 @@ var _ = Describe("TLS self-signed cert function", func() { // clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, // clusterNamePrefix, clusterDefName, clusterVersionName). // WithRandomName(). - // AddComponent(statefulCompName, statefulCompType). + // AddComponentDef(statefulCompName, statefulCompDefName). // SetReplicas(3). // SetTLS(true). // SetIssuer(tlsIssuer). @@ -230,7 +235,7 @@ var _ = Describe("TLS self-signed cert function", func() { By("create cluster obj") clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefName, clusterVersionName). WithRandomName(). - AddComponent(statefulCompName, statefulCompType). + AddComponent(statefulCompName, statefulCompDefName). SetReplicas(3). SetTLS(true). SetIssuer(tlsIssuer). @@ -258,7 +263,7 @@ var _ = Describe("TLS self-signed cert function", func() { // By("create cluster obj") // clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefName, clusterVersionName). // WithRandomName(). - // AddComponent(statefulCompName, statefulCompType). + // AddComponentDef(statefulCompName, statefulCompDefName). // SetReplicas(3). // SetTLS(true). // SetIssuer(tlsIssuer). @@ -281,18 +286,24 @@ var _ = Describe("TLS self-signed cert function", func() { By("create cluster with tls disabled") clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefName, clusterVersionName). WithRandomName(). - AddComponent(statefulCompName, statefulCompType). + AddComponent(statefulCompName, statefulCompDefName). SetReplicas(3). SetTLS(false). Create(&testCtx). GetObject() - Eventually(testapps.GetClusterObservedGeneration(&testCtx, client.ObjectKeyFromObject(clusterObj))).Should(BeEquivalentTo(1)) - stsList := testk8s.ListAndCheckStatefulSet(&testCtx, client.ObjectKeyFromObject(clusterObj)) + clusterKey := client.ObjectKeyFromObject(clusterObj) + Eventually(k8sClient.Get(ctx, clusterKey, clusterObj)).Should(Succeed()) + Eventually(testapps.GetClusterObservedGeneration(&testCtx, clusterKey)).Should(BeEquivalentTo(1)) + Eventually(testapps.GetClusterPhase(&testCtx, clusterKey)).Should(Equal(appsv1alpha1.CreatingClusterPhase)) + stsList := testk8s.ListAndCheckStatefulSet(&testCtx, clusterKey) sts := stsList.Items[0] cd := &appsv1alpha1.ClusterDefinition{} Expect(k8sClient.Get(ctx, types.NamespacedName{Name: clusterDefName, Namespace: testCtx.DefaultNamespace}, cd)).Should(Succeed()) cmName := cfgcore.GetInstanceCMName(&sts, &cd.Spec.ComponentDefs[0].ConfigSpecs[0].ComponentTemplateSpec) cmKey := client.ObjectKey{Namespace: sts.Namespace, Name: cmName} + Eventually(testapps.GetAndChangeObj(&testCtx, cmKey, func(cm *corev1.ConfigMap) { + cm.Annotations[constant.CMInsEnableRerenderTemplateKey] = "true" + })).Should(Succeed()) hasTLSSettings := func() bool { cm := &corev1.ConfigMap{} Expect(k8sClient.Get(ctx, cmKey, cm)).Should(Succeed()) diff --git a/controllers/apps/utils.go b/controllers/apps/utils.go index 199e98e1d..0dd20cf06 100644 --- a/controllers/apps/utils.go +++ b/controllers/apps/utils.go @@ -1,25 +1,42 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( + "context" + "fmt" "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) +// default reconcile requeue after duration +var requeueDuration = time.Millisecond * 100 + func getEnvReplacementMapForAccount(name, passwd string) map[string]string { return map[string]string{ "$(USERNAME)": name, @@ -27,5 +44,39 @@ func getEnvReplacementMapForAccount(name, passwd string) map[string]string { } } -// default reconcile requeue after duration -var requeueDuration = time.Millisecond * 100 +// notifyClusterStatusChange notifies cluster changes occurred and triggers it to reconcile. +func notifyClusterStatusChange(ctx context.Context, cli client.Client, recorder record.EventRecorder, obj client.Object, event *corev1.Event) error { + if obj == nil || !intctrlutil.WorkloadFilterPredicate(obj) { + return nil + } + + cluster, ok := obj.(*appsv1alpha1.Cluster) + if !ok { + var err error + if cluster, err = util.GetClusterByObject(ctx, cli, obj); err != nil { + return err + } + } + + patch := client.MergeFrom(cluster.DeepCopy()) + if cluster.Annotations == nil { + cluster.Annotations = map[string]string{} + } + cluster.Annotations[constant.ReconcileAnnotationKey] = time.Now().Format(time.RFC3339Nano) + if err := cli.Patch(ctx, cluster, patch); err != nil { + return err + } + + if recorder != nil && event != nil { + recorder.Eventf(cluster, corev1.EventTypeWarning, event.Reason, getFinalEventMessageForRecorder(event)) + } + return nil +} + +// getFinalEventMessageForRecorder gets final event message by event involved object kind for recorded it +func getFinalEventMessageForRecorder(event *corev1.Event) string { + if event.InvolvedObject.Kind == constant.PodKind { + return fmt.Sprintf("Pod %s: %s", event.InvolvedObject.Name, event.Message) + } + return event.Message +} diff --git a/controllers/dataprotection/backup_controller.go b/controllers/dataprotection/backup_controller.go index d6810e785..5d9a9bde6 100644 --- a/controllers/dataprotection/backup_controller.go +++ b/controllers/dataprotection/backup_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection @@ -26,15 +29,17 @@ import ( "strings" "time" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/tools/record" "k8s.io/utils/clock" ctrl "sigs.k8s.io/controller-runtime" @@ -51,12 +56,18 @@ import ( intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) +const ( + backupPathBase = "/backupdata" + deleteBackupFilesJobNamePrefix = "delete-" +) + // BackupReconciler reconciles a Backup object type BackupReconciler struct { client.Client - Scheme *k8sruntime.Scheme - Recorder record.EventRecorder - clock clock.RealClock + Scheme *k8sruntime.Scheme + Recorder record.EventRecorder + clock clock.RealClock + snapshotCli *intctrlutil.VolumeSnapshotCompatClient } // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backups,verbs=get;list;watch;create;update;patch;delete @@ -73,8 +84,6 @@ type BackupReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - // NOTES: // setup common request context reqCtx := intctrlutil.RequestCtx{ @@ -83,6 +92,11 @@ func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr Log: log.FromContext(ctx).WithValues("backup", req.NamespacedName), Recorder: r.Recorder, } + // initialize snapshotCompatClient + r.snapshotCli = &intctrlutil.VolumeSnapshotCompatClient{ + Client: r.Client, + Ctx: ctx, + } // Get backup obj backup := &dataprotectionv1alpha1.Backup{} if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, backup); err != nil { @@ -98,7 +112,6 @@ func (r *BackupReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctr return *res, err } - // backup reconcile logic here switch backup.Status.Phase { case "", dataprotectionv1alpha1.BackupNew: return r.doNewPhaseAction(reqCtx, backup) @@ -122,12 +135,40 @@ func (r *BackupReconciler) SetupWithManager(mgr ctrl.Manager) error { Owns(&batchv1.Job{}) if viper.GetBool("VOLUMESNAPSHOT") { - b.Owns(&snapshotv1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + if intctrlutil.InVolumeSnapshotV1Beta1() { + b.Owns(&snapshotv1beta1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + } else { + b.Owns(&snapshotv1.VolumeSnapshot{}, builder.OnlyMetadata, builder.Predicates{}) + } } return b.Complete(r) } +func (r *BackupReconciler) getBackupPolicyAndValidate( + reqCtx intctrlutil.RequestCtx, + backup *dataprotectionv1alpha1.Backup) (*dataprotectionv1alpha1.BackupPolicy, error) { + // get referenced backup policy + backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} + backupPolicyNameSpaceName := types.NamespacedName{ + Namespace: reqCtx.Req.Namespace, + Name: backup.Spec.BackupPolicyName, + } + if err := r.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { + return nil, err + } + + if len(backupPolicy.Name) == 0 { + return nil, intctrlutil.NewNotFound(`backup policy "%s" not found`, backupPolicyNameSpaceName) + } + + // validate backup spec + if err := backup.Spec.Validate(backupPolicy); err != nil { + return nil, err + } + return backupPolicy, nil +} + func (r *BackupReconciler) doNewPhaseAction( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) (ctrl.Result, error) { @@ -143,30 +184,35 @@ func (r *BackupReconciler) doNewPhaseAction( return intctrlutil.Reconciled() } - // update labels - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyNameSpaceName := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, - } - if err := r.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { - r.Recorder.Eventf(backup, corev1.EventTypeWarning, "CreatingBackup", - "Unable to get backupPolicy for backup %s.", backupPolicyNameSpaceName) + backupPolicy, err := r.getBackupPolicyAndValidate(reqCtx, backup) + if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } - if backupPolicy.Status.Phase != dataprotectionv1alpha1.ConfigAvailable { - if backupPolicy.Status.Phase == dataprotectionv1alpha1.ConfigFailed { - // REVIEW/TODO: need avoid using dynamic error string, this is bad for - // error type checking (errors.Is) - err := fmt.Errorf("backupPolicy %s status is failed", backupPolicy.Name) + + // TODO: get pod with matching labels to do backup. + var targetCluster dataprotectionv1alpha1.TargetCluster + switch backup.Spec.BackupType { + case dataprotectionv1alpha1.BackupTypeSnapshot: + targetCluster = backupPolicy.Spec.Snapshot.Target + default: + commonPolicy := backupPolicy.Spec.GetCommonPolicy(backup.Spec.BackupType) + if commonPolicy == nil { + return r.updateStatusIfFailed(reqCtx, backup, intctrlutil.NewBackupNotSupported(string(backup.Spec.BackupType), backupPolicy.Name)) + } + // save the backup message for restore + backup.Status.PersistentVolumeClaimName = commonPolicy.PersistentVolumeClaim.Name + backup.Status.BackupToolName = commonPolicy.BackupToolName + targetCluster = commonPolicy.Target + if err = r.handlePersistentVolumeClaim(reqCtx, backupPolicy.Name, commonPolicy); err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } - // requeue to wait backupPolicy available - return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") } - - // TODO: get pod with matching labels to do backup. - target, err := r.getTargetPod(reqCtx, backup, backupPolicy.Spec.Target.LabelsSelector.MatchLabels) + // clean cached annotations if in NEW phase + backupCopy := backup.DeepCopy() + if backupCopy.Annotations[dataProtectionBackupTargetPodKey] != "" { + delete(backupCopy.Annotations, dataProtectionBackupTargetPodKey) + } + target, err := r.getTargetPod(reqCtx, backupCopy, targetCluster.LabelsSelector.MatchLabels) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } @@ -177,57 +223,166 @@ func (r *BackupReconciler) doNewPhaseAction( return intctrlutil.Reconciled() } - // save the backup message for restore - backup.Status.RemoteVolume = &backupPolicy.Spec.RemoteVolume - backup.Status.BackupToolName = backupPolicy.Spec.BackupToolName - // update Phase to InProgress backup.Status.Phase = dataprotectionv1alpha1.BackupInProgress backup.Status.StartTimestamp = &metav1.Time{Time: r.clock.Now().UTC()} - if backup.Spec.TTL != nil { + if backupPolicy.Spec.Retention != nil && backupPolicy.Spec.Retention.TTL != nil { backup.Status.Expiration = &metav1.Time{ - Time: backup.Status.StartTimestamp.Add(backup.Spec.TTL.Duration), + Time: backup.Status.StartTimestamp.Add(dataprotectionv1alpha1.ToDuration(backupPolicy.Spec.Retention.TTL)), } } - if err := r.Client.Status().Patch(reqCtx.Ctx, backup, patch); err != nil { + + if err = r.Client.Status().Patch(reqCtx.Ctx, backup, patch); err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") } return intctrlutil.Reconciled() } +// handlePersistentVolumeClaim handles the persistent volume claim for the backup, the rules are as follows +// - if CreatePolicy is "Never", it will check if the pvc exists. if not existed, then report an error. +// - if CreatePolicy is "IfNotPresent" and the pvc not existed, then create the pvc automatically. +func (r *BackupReconciler) handlePersistentVolumeClaim(reqCtx intctrlutil.RequestCtx, + backupPolicyName string, + commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy) error { + pvcConfig := commonPolicy.PersistentVolumeClaim + if len(pvcConfig.Name) == 0 { + return intctrlutil.NewBackupPVCNameIsEmpty(backupPolicyName) + } + pvc := &corev1.PersistentVolumeClaim{} + if err := r.Client.Get(reqCtx.Ctx, client.ObjectKey{Namespace: reqCtx.Req.Namespace, + Name: pvcConfig.Name}, pvc); err != nil && !apierrors.IsNotFound(err) { + return err + } + if len(pvc.Name) > 0 { + return nil + } + if pvcConfig.CreatePolicy == dataprotectionv1alpha1.CreatePVCPolicyNever { + return intctrlutil.NewNotFound(`persistent volume claim "%s" not found`, pvcConfig.Name) + } + if pvcConfig.PersistentVolumeConfigMap != nil && + (pvcConfig.StorageClassName == nil || *pvcConfig.StorageClassName == "") { + // if the storageClassName is empty and the PersistentVolumeConfigMap is not empty, + // create the persistentVolume with the template + if err := r.createPersistentVolumeWithTemplate(reqCtx, backupPolicyName, &pvcConfig); err != nil { + return err + } + } + return r.createPVCWithStorageClassName(reqCtx, backupPolicyName, pvcConfig) +} + +// createPVCWithStorageClassName creates the persistent volume claim with the storageClassName. +func (r *BackupReconciler) createPVCWithStorageClassName(reqCtx intctrlutil.RequestCtx, + backupPolicyName string, + pvcConfig dataprotectionv1alpha1.PersistentVolumeClaim) error { + pvc := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: pvcConfig.Name, + Namespace: reqCtx.Req.Namespace, + Annotations: r.buildAutoCreationAnnotations(backupPolicyName), + }, + Spec: corev1.PersistentVolumeClaimSpec{ + StorageClassName: pvcConfig.StorageClassName, + Resources: corev1.ResourceRequirements{ + Requests: map[corev1.ResourceName]resource.Quantity{ + corev1.ResourceStorage: pvcConfig.InitCapacity, + }, + }, + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteMany, + }, + }, + } + err := r.Client.Create(reqCtx.Ctx, pvc) + return client.IgnoreAlreadyExists(err) +} + +// createPersistentVolumeWithTemplate creates the persistent volume with the template. +func (r *BackupReconciler) createPersistentVolumeWithTemplate(reqCtx intctrlutil.RequestCtx, + backupPolicyName string, + pvcConfig *dataprotectionv1alpha1.PersistentVolumeClaim) error { + pvConfig := pvcConfig.PersistentVolumeConfigMap + configMap := &corev1.ConfigMap{} + if err := r.Client.Get(reqCtx.Ctx, client.ObjectKey{Namespace: pvConfig.Namespace, + Name: pvConfig.Name}, configMap); err != nil { + return err + } + pvTemplate := configMap.Data[persistentVolumeTemplateKey] + if pvTemplate == "" { + return intctrlutil.NewBackupPVTemplateNotFound(pvConfig.Namespace, pvConfig.Name) + } + pvName := fmt.Sprintf("%s-%s", pvcConfig.Name, reqCtx.Req.Namespace) + pvTemplate = strings.ReplaceAll(pvTemplate, "$(GENERATE_NAME)", pvName) + pv := &corev1.PersistentVolume{} + if err := yaml.Unmarshal([]byte(pvTemplate), pv); err != nil { + return err + } + pv.Name = pvName + pv.Spec.ClaimRef = &corev1.ObjectReference{ + Namespace: reqCtx.Req.Namespace, + Name: pvcConfig.Name, + } + pv.Annotations = r.buildAutoCreationAnnotations(backupPolicyName) + // set the storageClassName to empty for the persistentVolumeClaim to avoid the dynamic provisioning + emptyStorageClassName := "" + pvcConfig.StorageClassName = &emptyStorageClassName + controllerutil.AddFinalizer(pv, dataProtectionFinalizerName) + return r.Client.Create(reqCtx.Ctx, pv) +} + +func (r *BackupReconciler) buildAutoCreationAnnotations(backupPolicyName string) map[string]string { + return map[string]string{ + dataProtectionAnnotationCreateByPolicyKey: "true", + dataProtectionLabelBackupPolicyKey: backupPolicyName, + } +} + +// getBackupPathPrefix gets the backup path prefix. +func (r *BackupReconciler) getBackupPathPrefix(req ctrl.Request, pathPrefix string) string { + pathPrefix = strings.TrimRight(pathPrefix, "/") + if strings.TrimSpace(pathPrefix) == "" || strings.HasPrefix(pathPrefix, "/") { + return fmt.Sprintf("/%s%s/%s", req.Namespace, pathPrefix, req.Name) + } + return fmt.Sprintf("/%s/%s/%s", req.Namespace, pathPrefix, req.Name) +} + func (r *BackupReconciler) doInProgressPhaseAction( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) (ctrl.Result, error) { + backupPolicy, err := r.getBackupPolicyAndValidate(reqCtx, backup) + if err != nil { + return r.updateStatusIfFailed(reqCtx, backup, err) + } patch := client.MergeFrom(backup.DeepCopy()) if backup.Spec.BackupType == dataprotectionv1alpha1.BackupTypeSnapshot { // 1. create and ensure pre-command job completed // 2. create and ensure volume snapshot ready // 3. create and ensure post-command job completed - isOK, err := r.createPreCommandJobAndEnsure(reqCtx, backup) + isOK, err := r.createPreCommandJobAndEnsure(reqCtx, backup, backupPolicy.Spec.Snapshot) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } if !isOK { return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") } - if err = r.createUpdatesJobs(reqCtx, backup, dataprotectionv1alpha1.PRE); err != nil { + if err = r.createUpdatesJobs(reqCtx, backup, &backupPolicy.Spec.Snapshot.BasePolicy, dataprotectionv1alpha1.PRE); err != nil { r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedPreUpdatesJob", err.Error()) } - if err = r.createVolumeSnapshot(reqCtx, backup); err != nil { + if err = r.createVolumeSnapshot(reqCtx, backup, backupPolicy.Spec.Snapshot); err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } + key := types.NamespacedName{Namespace: reqCtx.Req.Namespace, Name: backup.Name} isOK, err = r.ensureVolumeSnapshotReady(reqCtx, key) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } if !isOK { - return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") + return intctrlutil.Reconciled() } msg := fmt.Sprintf("Created volumeSnapshot %s ready.", key.Name) r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedVolumeSnapshot", msg) - isOK, err = r.createPostCommandJobAndEnsure(reqCtx, backup) + isOK, err = r.createPostCommandJobAndEnsure(reqCtx, backup, backupPolicy.Spec.Snapshot) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } @@ -236,21 +391,31 @@ func (r *BackupReconciler) doInProgressPhaseAction( } // Failure MetadataCollectionJob does not affect the backup status. - if err = r.createUpdatesJobs(reqCtx, backup, dataprotectionv1alpha1.POST); err != nil { + if err = r.createUpdatesJobs(reqCtx, backup, &backupPolicy.Spec.Snapshot.BasePolicy, dataprotectionv1alpha1.POST); err != nil { r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedPostUpdatesJob", err.Error()) } backup.Status.Phase = dataprotectionv1alpha1.BackupCompleted backup.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now().UTC()} snap := &snapshotv1.VolumeSnapshot{} - exists, _ := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, key, snap) + exists, _ := r.snapshotCli.CheckResourceExists(key, snap) if exists { backup.Status.TotalSize = snap.Status.RestoreSize.String() } } else { // 1. create and ensure backup tool job finished // 2. get job phase and update - err := r.createBackupToolJob(reqCtx, backup) + commonPolicy := backupPolicy.Spec.GetCommonPolicy(backup.Spec.BackupType) + if commonPolicy == nil { + // TODO: add error type + return r.updateStatusIfFailed(reqCtx, backup, fmt.Errorf("not found the %s policy", backup.Spec.BackupType)) + } + // createUpdatesJobs should not affect the backup status, just need to record events when the run fails + if err = r.createUpdatesJobs(reqCtx, backup, &commonPolicy.BasePolicy, dataprotectionv1alpha1.PRE); err != nil { + r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedPreUpdatesJob", err.Error()) + } + pathPrefix := r.getBackupPathPrefix(reqCtx.Req, backupPolicy.Annotations[constant.BackupDataPathPrefixAnnotationKey]) + err = r.createBackupToolJob(reqCtx, backup, commonPolicy, pathPrefix) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } @@ -260,21 +425,39 @@ func (r *BackupReconciler) doInProgressPhaseAction( return r.updateStatusIfFailed(reqCtx, backup, err) } if !isOK { - return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") + return intctrlutil.Reconciled() } job, err := r.getBatchV1Job(reqCtx, backup) if err != nil { return r.updateStatusIfFailed(reqCtx, backup, err) } + // createUpdatesJobs should not affect the backup status, just need to record events when the run fails + if err = r.createUpdatesJobs(reqCtx, backup, &commonPolicy.BasePolicy, dataprotectionv1alpha1.POST); err != nil { + r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatedPostUpdatesJob", err.Error()) + } jobStatusConditions := job.Status.Conditions if jobStatusConditions[0].Type == batchv1.JobComplete { - // update Phase to in Completed + // update Phase to Completed backup.Status.Phase = dataprotectionv1alpha1.BackupCompleted backup.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now().UTC()} + if backup.Status.Manifests == nil { + backup.Status.Manifests = &dataprotectionv1alpha1.ManifestsStatus{} + } + if backup.Status.Manifests.BackupTool == nil { + backup.Status.Manifests.BackupTool = &dataprotectionv1alpha1.BackupToolManifestsStatus{} + } + backup.Status.Manifests.BackupTool.FilePath = pathPrefix } else if jobStatusConditions[0].Type == batchv1.JobFailed { backup.Status.Phase = dataprotectionv1alpha1.BackupFailed backup.Status.FailureReason = job.Status.Conditions[0].Reason } + if backup.Spec.BackupType == dataprotectionv1alpha1.BackupTypeLogFile { + if backup.Status.Manifests != nil && + backup.Status.Manifests.BackupLog != nil && + backup.Status.Manifests.BackupLog.StartTime == nil { + backup.Status.Manifests.BackupLog.StartTime = backup.Status.Manifests.BackupLog.StopTime + } + } } // finally, update backup status @@ -304,8 +487,13 @@ func (r *BackupReconciler) doCompletedPhaseAction( func (r *BackupReconciler) updateStatusIfFailed(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, err error) (ctrl.Result, error) { patch := client.MergeFrom(backup.DeepCopy()) - r.Recorder.Eventf(backup, corev1.EventTypeWarning, "FailedCreatedBackup", - "Failed creating backup, error: %s", err.Error()) + controllerErr := intctrlutil.ToControllerError(err) + if controllerErr != nil { + r.Recorder.Eventf(backup, corev1.EventTypeWarning, string(controllerErr.Type), err.Error()) + } else { + r.Recorder.Eventf(backup, corev1.EventTypeWarning, "FailedCreatedBackup", + "Creating backup failed, error: %s", err.Error()) + } backup.Status.Phase = dataprotectionv1alpha1.BackupFailed backup.Status.FailureReason = err.Error() if errUpdate := r.Client.Status().Patch(reqCtx.Ctx, backup, patch); errUpdate != nil { @@ -314,7 +502,7 @@ func (r *BackupReconciler) updateStatusIfFailed(reqCtx intctrlutil.RequestCtx, return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") } -// patchBackupLabelsAndAnnotations patch backup labels and the annotations include cluster snapshot. +// patchBackupLabelsAndAnnotations patches backup labels and the annotations include cluster snapshot. func (r *BackupReconciler) patchBackupLabelsAndAnnotations( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, @@ -344,40 +532,42 @@ func (r *BackupReconciler) patchBackupLabelsAndAnnotations( } func (r *BackupReconciler) createPreCommandJobAndEnsure(reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup) (bool, error) { + backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy) (bool, error) { - emptyCmd, err := r.ensureEmptyHooksCommand(reqCtx, backup, true) + emptyCmd, err := r.ensureEmptyHooksCommand(snapshotPolicy, true) if err != nil { return false, err } - // if not defined commands, skip create job. + // if undefined commands, skip create job. if emptyCmd { return true, err } mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) key := types.NamespacedName{Namespace: mgrNS, Name: backup.Name + "-pre"} - if err := r.createHooksCommandJob(reqCtx, backup, key, true); err != nil { + if err := r.createHooksCommandJob(reqCtx, backup, snapshotPolicy, key, true); err != nil { return false, err } return r.ensureBatchV1JobCompleted(reqCtx, key) } func (r *BackupReconciler) createPostCommandJobAndEnsure(reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup) (bool, error) { + backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy) (bool, error) { - emptyCmd, err := r.ensureEmptyHooksCommand(reqCtx, backup, false) + emptyCmd, err := r.ensureEmptyHooksCommand(snapshotPolicy, false) if err != nil { return false, err } - // if not defined commands, skip create job. + // if undefined commands, skip create job. if emptyCmd { return true, err } mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) key := types.NamespacedName{Namespace: mgrNS, Name: backup.Name + "-post"} - if err := r.createHooksCommandJob(reqCtx, backup, key, false); err != nil { + if err = r.createHooksCommandJob(reqCtx, backup, snapshotPolicy, key, false); err != nil { return false, err } return r.ensureBatchV1JobCompleted(reqCtx, key) @@ -396,7 +586,7 @@ func (r *BackupReconciler) ensureBatchV1JobCompleted( if jobStatusConditions[0].Type == batchv1.JobComplete { return true, nil } else if jobStatusConditions[0].Type == batchv1.JobFailed { - return false, errors.New(errorJobFailed) + return false, intctrlutil.NewBackupJobFailed(job.Name) } } } @@ -405,10 +595,11 @@ func (r *BackupReconciler) ensureBatchV1JobCompleted( func (r *BackupReconciler) createVolumeSnapshot( reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup) error { + backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy) error { snap := &snapshotv1.VolumeSnapshot{} - exists, err := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, reqCtx.Req.NamespacedName, snap) + exists, err := r.snapshotCli.CheckResourceExists(reqCtx.Req.NamespacedName, snap) if err != nil { return err } @@ -429,7 +620,7 @@ func (r *BackupReconciler) createVolumeSnapshot( return err } - targetPVCs, err := r.getTargetPVCs(reqCtx, backup, backupPolicy.Spec.Target.LabelsSelector.MatchLabels) + targetPVCs, err := r.getTargetPVCs(reqCtx, backup, snapshotPolicy.Target.LabelsSelector.MatchLabels) if err != nil { return err } @@ -456,16 +647,16 @@ func (r *BackupReconciler) createVolumeSnapshot( controllerutil.AddFinalizer(snap, dataProtectionFinalizerName) scheme, _ := dataprotectionv1alpha1.SchemeBuilder.Build() - if err = controllerutil.SetOwnerReference(backup, snap, scheme); err != nil { + if err = controllerutil.SetControllerReference(backup, snap, scheme); err != nil { return err } reqCtx.Log.V(1).Info("create a volumeSnapshot from backup", "snapshot", snap.Name) - if err = r.Client.Create(reqCtx.Ctx, snap); err != nil && !apierrors.IsAlreadyExists(err) { + if err = r.snapshotCli.Create(snap); err != nil && !apierrors.IsAlreadyExists(err) { return err } } - msg := fmt.Sprintf("Waiting for a volume snapshot %s to be created by the backup.", snap.Name) + msg := fmt.Sprintf("Waiting for the volume snapshot %s creation to complete in backup.", snap.Name) r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatingVolumeSnapshot", msg) return nil } @@ -474,13 +665,14 @@ func (r *BackupReconciler) ensureVolumeSnapshotReady(reqCtx intctrlutil.RequestC key types.NamespacedName) (bool, error) { snap := &snapshotv1.VolumeSnapshot{} - exists, err := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, key, snap) + // not found, continue the creation process + exists, err := r.snapshotCli.CheckResourceExists(key, snap) if err != nil { return false, err } ready := false if exists && snap.Status != nil { - // check if snapshot status throw error, e.g. csi does not support volume snapshot + // check if snapshot status throws an error, e.g. csi does not support volume snapshot if snap.Status.Error != nil && snap.Status.Error.Message != nil { return ready, errors.New(*snap.Status.Error.Message) } @@ -493,7 +685,9 @@ func (r *BackupReconciler) ensureVolumeSnapshotReady(reqCtx intctrlutil.RequestC } func (r *BackupReconciler) createUpdatesJobs(reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup, stage dataprotectionv1alpha1.BackupStatusUpdateStage) error { + backup *dataprotectionv1alpha1.Backup, + basePolicy *dataprotectionv1alpha1.BasePolicy, + stage dataprotectionv1alpha1.BackupStatusUpdateStage) error { // get backup policy backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} backupPolicyNameSpaceName := types.NamespacedName{ @@ -504,11 +698,11 @@ func (r *BackupReconciler) createUpdatesJobs(reqCtx intctrlutil.RequestCtx, reqCtx.Log.V(1).Error(err, "Unable to get backupPolicy for backup.", "backupPolicy", backupPolicyNameSpaceName) return err } - for _, update := range backupPolicy.Spec.BackupStatusUpdates { + for _, update := range basePolicy.BackupStatusUpdates { if update.UpdateStage != stage { continue } - if err := r.createMetadataCollectionJob(reqCtx, backup, update); err != nil { + if err := r.createMetadataCollectionJob(reqCtx, backup, basePolicy, update); err != nil { return err } } @@ -516,9 +710,19 @@ func (r *BackupReconciler) createUpdatesJobs(reqCtx intctrlutil.RequestCtx, } func (r *BackupReconciler) createMetadataCollectionJob(reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup, updateInfo dataprotectionv1alpha1.BackupStatusUpdate) error { + backup *dataprotectionv1alpha1.Backup, + basePolicy *dataprotectionv1alpha1.BasePolicy, + updateInfo dataprotectionv1alpha1.BackupStatusUpdate) error { mgrNS := viper.GetString(constant.CfgKeyCtrlrMgrNS) - key := types.NamespacedName{Namespace: mgrNS, Name: backup.Name + "-" + strings.ToLower(updateInfo.Path)} + updatePath := updateInfo.Path + if updateInfo.Path == "" { + updatePath = "status" + } + jobName := backup.Name + if len(backup.Name) > 30 { + jobName = backup.Name[:30] + } + key := types.NamespacedName{Namespace: mgrNS, Name: jobName + "-" + strings.ToLower(updatePath)} job := &batchv1.Job{} // check if job is created if exists, err := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, key, job); err != nil { @@ -528,7 +732,7 @@ func (r *BackupReconciler) createMetadataCollectionJob(reqCtx intctrlutil.Reques } // build job and create - jobPodSpec, err := r.buildMetadataCollectionPodSpec(reqCtx, backup, updateInfo) + jobPodSpec, err := r.buildMetadataCollectionPodSpec(reqCtx, backup, basePolicy, updateInfo) if err != nil { return err } @@ -540,9 +744,104 @@ func (r *BackupReconciler) createMetadataCollectionJob(reqCtx intctrlutil.Reques return client.IgnoreAlreadyExists(r.Client.Create(reqCtx.Ctx, job)) } +func (r *BackupReconciler) createDeleteBackupFileJob( + reqCtx intctrlutil.RequestCtx, + jobKey types.NamespacedName, + backup *dataprotectionv1alpha1.Backup, + backupPVCName string, + backupFilePath string) error { + + // make sure the path has a leading slash + if !strings.HasPrefix(backupFilePath, "/") { + backupFilePath = "/" + backupFilePath + } + + // this script first deletes the directory where the backup is located (including files + // in the directory), and then traverses up the path level by level to clean up empty directories. + deleteScript := fmt.Sprintf(` + backupPathBase=%s; + targetPath="${backupPathBase}%s"; + + echo "removing backup files in ${targetPath}"; + rm -rf "${targetPath}"; + + absBackupPathBase=$(realpath "${backupPathBase}"); + curr=$(realpath "${targetPath}"); + while true; do + parent=$(dirname "${curr}"); + if [ "${parent}" == "${absBackupPathBase}" ]; then + echo "reach backupPathBase ${backupPathBase}, done"; + break; + fi; + if [ ! "$(ls -A "${parent}")" ]; then + echo "${parent} is empty, removing it..."; + rmdir "${parent}"; + else + echo "${parent} is not empty, done"; + break; + fi; + curr="${parent}"; + done + `, backupPathBase, backupFilePath) + + // build container + container := corev1.Container{} + container.Name = backup.Name + container.Command = []string{"sh", "-c"} + container.Args = []string{deleteScript} + container.Image = viper.GetString(constant.KBToolsImage) + container.ImagePullPolicy = corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)) + + allowPrivilegeEscalation := false + runAsUser := int64(0) + container.SecurityContext = &corev1.SecurityContext{ + AllowPrivilegeEscalation: &allowPrivilegeEscalation, + RunAsUser: &runAsUser, + } + + // build pod + podSpec := corev1.PodSpec{ + Containers: []corev1.Container{container}, + RestartPolicy: corev1.RestartPolicyNever, + } + + // mount the backup volume to the pod + r.appendBackupVolumeMount(backupPVCName, &podSpec, &podSpec.Containers[0]) + + if err := addTolerations(&podSpec); err != nil { + return err + } + + // build job + backOffLimit := int32(3) + ttlSecondsAfterSuccess := int32(600) + job := &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: jobKey.Namespace, + Name: jobKey.Name, + }, + Spec: batchv1.JobSpec{ + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: jobKey.Namespace, + Name: jobKey.Name, + }, + Spec: podSpec, + }, + BackoffLimit: &backOffLimit, + TTLSecondsAfterFinished: &ttlSecondsAfterSuccess, + }, + } + + reqCtx.Log.V(1).Info("create a job from delete backup files", "job", job) + return client.IgnoreAlreadyExists(r.Client.Create(reqCtx.Ctx, job)) +} + func (r *BackupReconciler) createBackupToolJob( reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup) error { + backup *dataprotectionv1alpha1.Backup, + commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy, + pathPrefix string) error { key := types.NamespacedName{Namespace: backup.Namespace, Name: backup.Name} job := batchv1.Job{} @@ -555,7 +854,7 @@ func (r *BackupReconciler) createBackupToolJob( return nil } - toolPodSpec, err := r.buildBackupToolPodSpec(reqCtx, backup) + toolPodSpec, err := r.buildBackupToolPodSpec(reqCtx, backup, commonPolicy, pathPrefix) if err != nil { return err } @@ -563,45 +862,23 @@ func (r *BackupReconciler) createBackupToolJob( if err = r.createBatchV1Job(reqCtx, key, backup, toolPodSpec); err != nil { return err } - msg := fmt.Sprintf("Waiting for a job %s to be created.", key.Name) + msg := fmt.Sprintf("Waiting for the job %s creation to complete.", key.Name) r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatingJob", msg) return nil } // ensureEmptyHooksCommand determines whether it has empty commands in the hooks func (r *BackupReconciler) ensureEmptyHooksCommand( - reqCtx intctrlutil.RequestCtx, - backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy, preCommand bool) (bool, error) { - - // get backup policy - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyKey := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, - } - - policyExists, err := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, backupPolicyKey, backupPolicy) - if err != nil { - msg := fmt.Sprintf("Failed to get backupPolicy %s .", backupPolicyKey.Name) - r.Recorder.Event(backup, corev1.EventTypeWarning, "BackupPolicyFailed", msg) - return false, err - } - - if !policyExists { - msg := fmt.Sprintf("Not Found backupPolicy %s .", backupPolicyKey.Name) - r.Recorder.Event(backup, corev1.EventTypeWarning, "BackupPolicyFailed", msg) - return false, errors.New(msg) - } - // return true directly, means hooks commands is empty, skip subsequent hook jobs. - if backupPolicy.Spec.Hooks == nil { + if snapshotPolicy.Hooks == nil { return true, nil } - commands := backupPolicy.Spec.Hooks.PostCommands + commands := snapshotPolicy.Hooks.PostCommands if preCommand { - commands = backupPolicy.Spec.Hooks.PreCommands + commands = snapshotPolicy.Hooks.PreCommands } if len(commands) == 0 { return true, nil @@ -612,6 +889,7 @@ func (r *BackupReconciler) ensureEmptyHooksCommand( func (r *BackupReconciler) createHooksCommandJob( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy, key types.NamespacedName, preCommand bool) error { @@ -625,12 +903,12 @@ func (r *BackupReconciler) createHooksCommandJob( return nil } - jobPodSpec, err := r.buildSnapshotPodSpec(reqCtx, backup, preCommand) + jobPodSpec, err := r.buildSnapshotPodSpec(reqCtx, backup, snapshotPolicy, preCommand) if err != nil { return err } - msg := fmt.Sprintf("Waiting for a job %s to be created.", key.Name) + msg := fmt.Sprintf("Waiting for the job %s creation to complete.", key.Name) r.Recorder.Event(backup, corev1.EventTypeNormal, "CreatingJob-"+key.Name, msg) return r.createBatchV1Job(reqCtx, key, backup, jobPodSpec) @@ -670,6 +948,11 @@ func (r *BackupReconciler) createBatchV1Job( }, } controllerutil.AddFinalizer(job, dataProtectionFinalizerName) + if backup.Namespace == job.Namespace { + if err := controllerutil.SetControllerReference(backup, job, r.Scheme); err != nil { + return err + } + } reqCtx.Log.V(1).Info("create a built-in job from backup", "job", job) return client.IgnoreAlreadyExists(r.Client.Create(reqCtx.Ctx, job)) @@ -720,27 +1003,89 @@ func (r *BackupReconciler) deleteReferenceBatchV1Jobs(reqCtx intctrlutil.Request func (r *BackupReconciler) deleteReferenceVolumeSnapshot(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) error { snaps := &snapshotv1.VolumeSnapshotList{} - if err := r.Client.List(reqCtx.Ctx, snaps, + if err := r.snapshotCli.List(snaps, client.InNamespace(reqCtx.Req.Namespace), client.MatchingLabels(buildBackupLabels(backup))); err != nil { return err } for _, i := range snaps.Items { if controllerutil.ContainsFinalizer(&i, dataProtectionFinalizerName) { - patch := client.MergeFrom(i.DeepCopy()) + patch := i.DeepCopy() controllerutil.RemoveFinalizer(&i, dataProtectionFinalizerName) - if err := r.Patch(reqCtx.Ctx, &i, patch); err != nil { + if err := r.snapshotCli.Patch(&i, patch); err != nil { return err } } - if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, &i); err != nil { + if err := r.snapshotCli.Delete(&i); err != nil { + return err + } + } + return nil +} + +func (r *BackupReconciler) deleteBackupFiles(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) error { + if backup.Spec.BackupType == dataprotectionv1alpha1.BackupTypeSnapshot { + // no file to delete for this type + return nil + } + if backup.Status.Phase == dataprotectionv1alpha1.BackupNew || + backup.Status.Phase == dataprotectionv1alpha1.BackupFailed { + // nothing to delete + return nil + } + + jobName := deleteBackupFilesJobNamePrefix + backup.Name + if len(jobName) > 60 { + jobName = jobName[:60] + } + jobKey := types.NamespacedName{Namespace: backup.Namespace, Name: jobName} + job := batchv1.Job{} + exists, err := intctrlutil.CheckResourceExists(reqCtx.Ctx, r.Client, jobKey, &job) + if err != nil { + return err + } + // create job for deleting backup files + if !exists { + pvcName := backup.Status.PersistentVolumeClaimName + if pvcName == "" { + reqCtx.Log.Info("skip deleting backup files because PersistentVolumeClaimName is empty", + "backup", backup.Name) + return nil + } + // check if pvc exists + if err = r.Client.Get(reqCtx.Ctx, types.NamespacedName{Namespace: backup.Namespace, Name: pvcName}, &corev1.PersistentVolumeClaim{}); err != nil { + if apierrors.IsNotFound(err) { + return nil + } + return err + } + + backupFilePath := "" + if backup.Status.Manifests != nil && backup.Status.Manifests.BackupTool != nil { + backupFilePath = backup.Status.Manifests.BackupTool.FilePath + } + if backupFilePath == "" || !strings.Contains(backupFilePath, backup.Name) { + // For compatibility: the FilePath field is changing from time to time, + // and it may not contain the backup name as a path component if the Backup object + // was created in a previous version. In this case, it's dangerous to execute + // the deletion command. For example, files belongs to other Backups can be deleted as well. + reqCtx.Log.Info("skip deleting backup files because backupFilePath is invalid", + "backupFilePath", backupFilePath, "backup", backup.Name) + return nil + } + // the job will run in the background + if err = r.createDeleteBackupFileJob(reqCtx, jobKey, backup, pvcName, backupFilePath); err != nil { return err } } + return nil } func (r *BackupReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) error { + if err := r.deleteBackupFiles(reqCtx, backup); err != nil { + return err + } if err := r.deleteReferenceBatchV1Jobs(reqCtx, backup); err != nil { return err } @@ -750,6 +1095,9 @@ func (r *BackupReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx return nil } +// getTargetPod gets the target pod by label selector. +// if the backup has obtained the target pod from label selector, it will be set to the annotations. +// then get the pod from this annotation to ensure that the same pod is picked up in future. func (r *BackupReconciler) getTargetPod(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, labels map[string]string) (*corev1.Pod, error) { if targetPodName, ok := backup.Annotations[dataProtectionBackupTargetPodKey]; ok { @@ -803,7 +1151,7 @@ func (r *BackupReconciler) getTargetPVCs(reqCtx intctrlutil.RequestCtx, } if dataPVC == nil { - return nil, errors.New("can not find any pvc to backup by labelsSelector") + return nil, errors.New("can not find any pvc to backup with labelsSelector") } allPVCs := []corev1.PersistentVolumeClaim{*dataPVC} @@ -814,34 +1162,45 @@ func (r *BackupReconciler) getTargetPVCs(reqCtx intctrlutil.RequestCtx, return allPVCs, nil } -func (r *BackupReconciler) buildBackupToolPodSpec(reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup) (corev1.PodSpec, error) { - podSpec := corev1.PodSpec{} - logger := reqCtx.Log - - // get backup policy - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyNameSpaceName := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, +func (r *BackupReconciler) appendBackupVolumeMount( + pvcName string, + podSpec *corev1.PodSpec, + container *corev1.Container) { + // TODO(dsj): mount multi remote backup volumes + remoteVolumeName := fmt.Sprintf("backup-%s", pvcName) + remoteVolume := corev1.Volume{ + Name: remoteVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: pvcName, + }, + }, } - - if err := r.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { - logger.Error(err, "Unable to get backupPolicy for backup.", "backupPolicy", backupPolicyNameSpaceName) - return podSpec, err + remoteVolumeMount := corev1.VolumeMount{ + Name: remoteVolumeName, + MountPath: backupPathBase, } + podSpec.Volumes = append(podSpec.Volumes, remoteVolume) + container.VolumeMounts = append(container.VolumeMounts, remoteVolumeMount) +} +func (r *BackupReconciler) buildBackupToolPodSpec(reqCtx intctrlutil.RequestCtx, + backup *dataprotectionv1alpha1.Backup, + commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy, + pathPrefix string) (corev1.PodSpec, error) { + podSpec := corev1.PodSpec{} // get backup tool backupTool := &dataprotectionv1alpha1.BackupTool{} backupToolNameSpaceName := types.NamespacedName{ Namespace: reqCtx.Req.Namespace, - Name: backupPolicy.Spec.BackupToolName, + Name: commonPolicy.BackupToolName, } if err := r.Client.Get(reqCtx.Ctx, backupToolNameSpaceName, backupTool); err != nil { - logger.Error(err, "Unable to get backupTool for backup.", "BackupTool", backupToolNameSpaceName) + reqCtx.Log.Error(err, "Unable to get backupTool for backup.", "BackupTool", backupToolNameSpaceName) return podSpec, err } - - clusterPod, err := r.getTargetPod(reqCtx, backup, backupPolicy.Spec.Target.LabelsSelector.MatchLabels) + // TODO: check if pvc exists + clusterPod, err := r.getTargetPod(reqCtx, backup, commonPolicy.Target.LabelsSelector.MatchLabels) if err != nil { return podSpec, err } @@ -860,111 +1219,96 @@ func (r *BackupReconciler) buildBackupToolPodSpec(reqCtx intctrlutil.RequestCtx, Value: strings.Join(hostDNS, "."), } - envDBUser := corev1.EnvVar{ - Name: "DB_USER", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backupPolicy.Spec.Target.Secret.Name, - }, - Key: backupPolicy.Spec.Target.Secret.UserKeyword, - }, - }, - } - container := corev1.Container{} container.Name = backup.Name container.Command = []string{"sh", "-c"} container.Args = backupTool.Spec.BackupCommands container.Image = backupTool.Spec.Image + if container.Image == "" { + // TODO(dsj): need determine container name to get, temporary use first container + container.Image = clusterPod.Spec.Containers[0].Image + } if backupTool.Spec.Resources != nil { container.Resources = *backupTool.Spec.Resources } - - remoteBackupPath := "/backupdata" - - // TODO(dsj): mount multi remote backup volumes - randomVolumeName := fmt.Sprintf("%s-%s", backupPolicy.Spec.RemoteVolume.Name, rand.String(6)) - backupPolicy.Spec.RemoteVolume.Name = randomVolumeName - remoteVolumeMount := corev1.VolumeMount{ - Name: randomVolumeName, - MountPath: remoteBackupPath, - } container.VolumeMounts = clusterPod.Spec.Containers[0].VolumeMounts - container.VolumeMounts = append(container.VolumeMounts, remoteVolumeMount) + allowPrivilegeEscalation := false runAsUser := int64(0) container.SecurityContext = &corev1.SecurityContext{ AllowPrivilegeEscalation: &allowPrivilegeEscalation, RunAsUser: &runAsUser} - envDBPassword := corev1.EnvVar{ - Name: "DB_PASSWORD", - ValueFrom: &corev1.EnvVarSource{ - SecretKeyRef: &corev1.SecretKeySelector{ - LocalObjectReference: corev1.LocalObjectReference{ - Name: backupPolicy.Spec.Target.Secret.Name, - }, - Key: backupPolicy.Spec.Target.Secret.PasswordKeyword, - }, - }, - } - envBackupName := corev1.EnvVar{ Name: "BACKUP_NAME", Value: backup.Name, } - envBackupDirPrefix := corev1.EnvVar{ - Name: "BACKUP_DIR_PREFIX", - ValueFrom: &corev1.EnvVarSource{ - FieldRef: &corev1.ObjectFieldSelector{ - FieldPath: "metadata.namespace", - }, - }, - } - envBackupDir := corev1.EnvVar{ Name: "BACKUP_DIR", - Value: remoteBackupPath + "/$(BACKUP_DIR_PREFIX)", + Value: backupPathBase + pathPrefix, + } + + container.Env = []corev1.EnvVar{envDBHost, envBackupName, envBackupDir} + if commonPolicy.Target.Secret != nil { + envDBUser := corev1.EnvVar{ + Name: "DB_USER", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: commonPolicy.Target.Secret.Name, + }, + Key: commonPolicy.Target.Secret.UsernameKey, + }, + }, + } + + envDBPassword := corev1.EnvVar{ + Name: "DB_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: commonPolicy.Target.Secret.Name, + }, + Key: commonPolicy.Target.Secret.PasswordKey, + }, + }, + } + container.Env = append(container.Env, envDBUser, envDBPassword) } - container.Env = []corev1.EnvVar{envDBHost, envDBUser, envDBPassword, envBackupName, envBackupDirPrefix, envBackupDir} // merge env from backup tool. container.Env = append(container.Env, backupTool.Spec.Env...) podSpec.Containers = []corev1.Container{container} - podSpec.Volumes = clusterPod.Spec.Volumes - podSpec.Volumes = append(podSpec.Volumes, backupPolicy.Spec.RemoteVolume) podSpec.RestartPolicy = corev1.RestartPolicyNever - // the pod of job needs to be scheduled on the same node as the workload pod, because it needs to share one pvc - // see: https://kubernetes.io/docs/concepts/scheduling-eviction/assign-pod-node/#nodename - podSpec.NodeName = clusterPod.Spec.NodeName + // mount the backup volume to the pod of backup tool + pvcName := commonPolicy.PersistentVolumeClaim.Name + r.appendBackupVolumeMount(pvcName, &podSpec, &podSpec.Containers[0]) + // the pod of job needs to be scheduled on the same node as the workload pod, because it needs to share one pvc + podSpec.NodeSelector = map[string]string{ + hostNameLabelKey: clusterPod.Spec.NodeName, + } + // ignore taints + podSpec.Tolerations = []corev1.Toleration{ + { + Operator: corev1.TolerationOpExists, + }, + } return podSpec, nil } func (r *BackupReconciler) buildSnapshotPodSpec( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, + snapshotPolicy *dataprotectionv1alpha1.SnapshotPolicy, preCommand bool) (corev1.PodSpec, error) { podSpec := corev1.PodSpec{} - logger := reqCtx.Log - - // get backup policy - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyNameSpaceName := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, - } - if err := r.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { - logger.Error(err, "Unable to get backupPolicy for backup.", "backupPolicy", backupPolicyNameSpaceName) - return podSpec, err - } - clusterPod, err := r.getTargetPod(reqCtx, backup, backupPolicy.Spec.Target.LabelsSelector.MatchLabels) + clusterPod, err := r.getTargetPod(reqCtx, backup, snapshotPolicy.Target.LabelsSelector.MatchLabels) if err != nil { return podSpec, err } @@ -972,13 +1316,13 @@ func (r *BackupReconciler) buildSnapshotPodSpec( container := corev1.Container{} container.Name = backup.Name container.Command = []string{"kubectl", "exec", "-n", backup.Namespace, - "-i", clusterPod.Name, "-c", backupPolicy.Spec.Hooks.ContainerName, "--", "sh", "-c"} + "-i", clusterPod.Name, "-c", snapshotPolicy.Hooks.ContainerName, "--", "sh", "-c"} if preCommand { - container.Args = backupPolicy.Spec.Hooks.PreCommands + container.Args = snapshotPolicy.Hooks.PreCommands } else { - container.Args = backupPolicy.Spec.Hooks.PostCommands + container.Args = snapshotPolicy.Hooks.PostCommands } - container.Image = backupPolicy.Spec.Hooks.Image + container.Image = snapshotPolicy.Hooks.Image if container.Image == "" { container.Image = viper.GetString(constant.KBToolsImage) container.ImagePullPolicy = corev1.PullPolicy(viper.GetString(constant.KBImagePullPolicy)) @@ -1031,22 +1375,10 @@ func addTolerations(podSpec *corev1.PodSpec) (err error) { func (r *BackupReconciler) buildMetadataCollectionPodSpec( reqCtx intctrlutil.RequestCtx, backup *dataprotectionv1alpha1.Backup, + basePolicy *dataprotectionv1alpha1.BasePolicy, updateInfo dataprotectionv1alpha1.BackupStatusUpdate) (corev1.PodSpec, error) { podSpec := corev1.PodSpec{} - logger := reqCtx.Log - - // get backup policy - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyNameSpaceName := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, - } - - if err := r.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { - logger.Error(err, "Unable to get backupPolicy for backup.", "backupPolicy", backupPolicyNameSpaceName) - return podSpec, err - } - targetPod, err := r.getTargetPod(reqCtx, backup, backupPolicy.Spec.Target.LabelsSelector.MatchLabels) + targetPod, err := r.getTargetPod(reqCtx, backup, basePolicy.Target.LabelsSelector.MatchLabels) if err != nil { return podSpec, err } @@ -1057,7 +1389,11 @@ func (r *BackupReconciler) buildMetadataCollectionPodSpec( args := "set -o errexit; set -o nounset;" + "OUTPUT=$(kubectl -n %s exec -it pod/%s -c %s -- %s);" + "kubectl -n %s patch backup %s --subresource=status --type=merge --patch \"%s\";" - patchJSON := generateJSON("status."+updateInfo.Path, "$OUTPUT") + statusPath := "status." + updateInfo.Path + if updateInfo.Path == "" { + statusPath = "status" + } + patchJSON := generateJSON(statusPath, "$OUTPUT") args = fmt.Sprintf(args, targetPod.Namespace, targetPod.Name, updateInfo.ContainerName, updateInfo.Script, backup.Namespace, backup.Name, patchJSON) container.Args = []string{args} diff --git a/controllers/dataprotection/backup_controller_test.go b/controllers/dataprotection/backup_controller_test.go index 6d640e5c7..f1858c341 100644 --- a/controllers/dataprotection/backup_controller_test.go +++ b/controllers/dataprotection/backup_controller_test.go @@ -1,26 +1,33 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection import ( + "fmt" "strings" + "github.com/ghodss/yaml" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" @@ -40,16 +47,15 @@ var _ = Describe("Backup Controller test", func() { const componentName = "replicasets-primary" const containerName = "mysql" const backupPolicyName = "test-backup-policy" - const backupRemoteVolumeName = "backup-remote-volume" const backupRemotePVCName = "backup-remote-pvc" const defaultSchedule = "0 3 * * *" - const defaultTTL = "168h0m0s" + const defaultTTL = "7d" const backupName = "test-backup-job" viper.SetDefault(constant.CfgKeyCtrlrMgrNS, testCtx.DefaultNamespace) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -65,8 +71,7 @@ var _ = Describe("Backup Controller test", func() { testapps.ClearResources(&testCtx, intctrlutil.BackupPolicySignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.JobSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.CronJobSignature, inNS, ml) - testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PersistentVolumeClaimSignature, true, inNS, ml) - // + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PersistentVolumeClaimSignature, true, inNS) // non-namespaced testapps.ClearResources(&testCtx, intctrlutil.BackupToolSignature, ml) } @@ -129,15 +134,20 @@ var _ = Describe("Backup Controller test", func() { By("By creating a backupPolicy from backupTool: " + backupTool.Name) _ = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupToolName(backupTool.Name). - SetSchedule(defaultSchedule). SetTTL(defaultTTL). + AddSnapshotPolicy(). + SetSchedule(defaultSchedule, true). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). AddMatchLabels(constant.RoleLabelKey, "leader"). SetTargetSecretName(clusterName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + AddFullPolicy(). + SetBackupToolName(backupTool.Name). + AddMatchLabels(constant.AppInstanceLabelKey, clusterName). + AddMatchLabels(constant.RoleLabelKey, "leader"). + SetTargetSecretName(clusterName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() }) @@ -147,9 +157,8 @@ var _ = Describe("Backup Controller test", func() { BeforeEach(func() { By("By creating a backup from backupPolicy: " + backupPolicyName) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() backupKey = client.ObjectKeyFromObject(backup) }) @@ -157,7 +166,7 @@ var _ = Describe("Backup Controller test", func() { It("should succeed after job completes", func() { By("Check backup job's nodeName equals pod's nodeName") Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *batchv1.Job) { - g.Expect(fetched.Spec.Template.Spec.NodeName).To(Equal(nodeName)) + g.Expect(fetched.Spec.Template.Spec.NodeSelector[hostNameLabelKey]).To(Equal(nodeName)) })).Should(Succeed()) patchK8sJobStatus(backupKey, batchv1.JobComplete) @@ -171,7 +180,7 @@ var _ = Describe("Backup Controller test", func() { })).Should(Succeed()) By("Check backup job is deleted after completed") - Eventually(testapps.CheckObjExists(&testCtx, backupKey, &batchv1.Job{}, false)) + Eventually(testapps.CheckObjExists(&testCtx, backupKey, &batchv1.Job{}, false)).Should(Succeed()) }) It("should fail after job fails", func() { @@ -184,6 +193,70 @@ var _ = Describe("Backup Controller test", func() { }) }) + Context("deletes a full backup", func() { + var backupKey types.NamespacedName + + BeforeEach(func() { + By("creating a backup from backupPolicy: " + backupPolicyName) + backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). + SetBackupPolicyName(backupPolicyName). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). + Create(&testCtx).GetObject() + backupKey = client.ObjectKeyFromObject(backup) + + By("waiting for finalizers to be added") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, backup *dataprotectionv1alpha1.Backup) { + g.Expect(backup.GetFinalizers()).ToNot(BeEmpty()) + })).Should(Succeed()) + + By("setting backup file path") + Eventually(testapps.ChangeObjStatus(&testCtx, backup, func() { + if backup.Status.Manifests == nil { + backup.Status.Manifests = &dataprotectionv1alpha1.ManifestsStatus{} + } + if backup.Status.Manifests.BackupTool == nil { + backup.Status.Manifests.BackupTool = &dataprotectionv1alpha1.BackupToolManifestsStatus{} + } + backup.Status.Manifests.BackupTool.FilePath = "/" + backupName + })).Should(Succeed()) + }) + + It("should create a Job for deleting backup files", func() { + By("deleting a Backup object") + testapps.DeleteObject(&testCtx, backupKey, &dataprotectionv1alpha1.Backup{}) + + By("checking new created Job") + jobKey := types.NamespacedName{ + Namespace: testCtx.DefaultNamespace, + Name: deleteBackupFilesJobNamePrefix + backupName, + } + Eventually(testapps.CheckObjExists(&testCtx, jobKey, + &batchv1.Job{}, true)).Should(Succeed()) + volumeName := "backup-" + backupRemotePVCName + Eventually(testapps.CheckObj(&testCtx, jobKey, func(g Gomega, job *batchv1.Job) { + Expect(job.Spec.Template.Spec.Volumes). + Should(ContainElement(corev1.Volume{ + Name: volumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backupRemotePVCName, + }, + }, + })) + Expect(job.Spec.Template.Spec.Containers[0].VolumeMounts). + Should(ContainElement(corev1.VolumeMount{ + Name: volumeName, + MountPath: backupPathBase, + })) + })).Should(Succeed()) + + By("checking Backup object, it should be deleted") + Eventually(testapps.CheckObjExists(&testCtx, backupKey, + &dataprotectionv1alpha1.Backup{}, false)).Should(Succeed()) + // TODO: add delete backup test case with the pvc not exists + }) + }) + Context("creates a snapshot backup", func() { var backupKey types.NamespacedName @@ -198,7 +271,6 @@ var _ = Describe("Backup Controller test", func() { By("By creating a backup from backupPolicy: " + backupPolicyName) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). Create(&testCtx).GetObject() @@ -297,7 +369,6 @@ var _ = Describe("Backup Controller test", func() { By("By creating a backup from backupPolicy: " + backupPolicyName) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). Create(&testCtx).GetObject() @@ -312,7 +383,6 @@ var _ = Describe("Backup Controller test", func() { It("should fail without pvc", func() { By("By creating a backup from backupPolicy: " + backupPolicyName) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). Create(&testCtx).GetObject() @@ -329,11 +399,22 @@ var _ = Describe("Backup Controller test", func() { }) }) - When("without backupTool resources", func() { + When("with backupTool resources", func() { Context("creates a full backup", func() { var backupKey types.NamespacedName + var backupPolicy *dataprotectionv1alpha1.BackupPolicy + var pathPrefix = "/mysql/backup" + createBackup := func(backupName string) { + By("By creating a backup from backupPolicy: " + backupPolicyName) + backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). + SetBackupPolicyName(backupPolicyName). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). + Create(&testCtx).GetObject() + backupKey = client.ObjectKeyFromObject(backup) + } BeforeEach(func() { + viper.SetDefault(constant.CfgKeyBackupPVCStorageClass, "") By("By creating a backupTool") backupTool := testapps.CreateCustomizedObj(&testCtx, "backup/backuptool.yaml", &dataprotectionv1alpha1.BackupTool{}, testapps.RandomizedObjName(), @@ -342,33 +423,110 @@ var _ = Describe("Backup Controller test", func() { }) By("By creating a backupPolicy from backupTool: " + backupTool.Name) - _ = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + backupPolicy = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + AddAnnotations(constant.BackupDataPathPrefixAnnotationKey, pathPrefix). + AddFullPolicy(). SetBackupToolName(backupTool.Name). - SetSchedule(defaultSchedule). + SetSchedule(defaultSchedule, true). SetTTL(defaultTTL). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). SetTargetSecretName(clusterName). - AddHookPreCommand("touch /data/mysql/.restore;sync"). - AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() - By("By creating a backup from backupPolicy: " + backupPolicyName) - backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). - SetBackupPolicyName(backupPolicyName). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). - Create(&testCtx).GetObject() - backupKey = client.ObjectKeyFromObject(backup) }) It("should succeed after job completes", func() { + createBackup(backupName) patchK8sJobStatus(backupKey, batchv1.JobComplete) - By("Check backup job completed") Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dataprotectionv1alpha1.Backup) { g.Expect(fetched.Status.Phase).To(Equal(dataprotectionv1alpha1.BackupCompleted)) + g.Expect(fetched.Status.Manifests.BackupTool.FilePath).To(Equal(fmt.Sprintf("/%s%s/%s", backupKey.Namespace, pathPrefix, backupKey.Name))) + })).Should(Succeed()) + }) + + It("creates pvc if the specified pvc not exists", func() { + createBackup(backupName) + By("Check pvc created by backup controller") + Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{ + Name: backupRemotePVCName, + Namespace: testCtx.DefaultNamespace, + }, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) + }) + + It("creates pvc if the specified pvc not exists", func() { + By("set persistentVolumeConfigmap") + configMapName := "pv-template-configmap" + Expect(testapps.ChangeObj(&testCtx, backupPolicy, func(tmpObj *dataprotectionv1alpha1.BackupPolicy) { + tmpObj.Spec.Datafile.PersistentVolumeClaim.PersistentVolumeConfigMap = &dataprotectionv1alpha1.PersistentVolumeConfigMap{ + Name: configMapName, + Namespace: testCtx.DefaultNamespace, + } })).Should(Succeed()) + + By("create backup with non existent configmap of pv template") + createBackup(backupName) + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dataprotectionv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dataprotectionv1alpha1.BackupFailed)) + g.Expect(fetched.Status.FailureReason).To(ContainSubstring(fmt.Sprintf(`ConfigMap "%s" not found`, configMapName))) + })).Should(Succeed()) + configMap := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: configMapName, + Namespace: testCtx.DefaultNamespace, + }, + Data: map[string]string{}, + } + Expect(testCtx.CreateObj(ctx, configMap)).Should(Succeed()) + + By("create backup with the configmap not contains the key 'persistentVolume'") + createBackup(backupName + "1") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dataprotectionv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dataprotectionv1alpha1.BackupFailed)) + g.Expect(fetched.Status.FailureReason).To(ContainSubstring("the persistentVolume template is empty in the configMap")) + })).Should(Succeed()) + + By("create backup with the configmap contains the key 'persistentVolume'") + Expect(testapps.ChangeObj(&testCtx, configMap, func(tmpObj *corev1.ConfigMap) { + pv := corev1.PersistentVolume{ + Spec: corev1.PersistentVolumeSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteMany, + }, + Capacity: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + PersistentVolumeReclaimPolicy: corev1.PersistentVolumeReclaimRetain, + PersistentVolumeSource: corev1.PersistentVolumeSource{ + CSI: &corev1.CSIPersistentVolumeSource{ + Driver: "kubeblocks.com", + FSType: "ext4", + VolumeHandle: pvcName, + }, + }, + }, + } + pvString, _ := yaml.Marshal(pv) + tmpObj.Data = map[string]string{ + "persistentVolume": string(pvString), + } + })).Should(Succeed()) + createBackup(backupName + "2") + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dataprotectionv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dataprotectionv1alpha1.BackupInProgress)) + })).Should(Succeed()) + + By("check pvc and pv created by backup controller") + Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{ + Name: backupRemotePVCName, + Namespace: testCtx.DefaultNamespace, + }, &corev1.PersistentVolumeClaim{}, true)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, types.NamespacedName{ + Name: backupRemotePVCName + "-" + testCtx.DefaultNamespace, + Namespace: testCtx.DefaultNamespace, + }, &corev1.PersistentVolume{}, true)).Should(Succeed()) + }) }) }) @@ -378,9 +536,8 @@ var _ = Describe("Backup Controller test", func() { BeforeEach(func() { By("By creating a backup from backupPolicy: " + backupPolicyName) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() backupKey = client.ObjectKeyFromObject(backup) }) @@ -410,7 +567,7 @@ func patchVolumeSnapshotStatus(key types.NamespacedName, readyToUse bool) { func patchBackupPolicySpecBackupStatusUpdates(key types.NamespacedName) { Eventually(testapps.GetAndChangeObj(&testCtx, key, func(fetched *dataprotectionv1alpha1.BackupPolicy) { - fetched.Spec.BackupStatusUpdates = []dataprotectionv1alpha1.BackupStatusUpdate{ + fetched.Spec.Snapshot.BackupStatusUpdates = []dataprotectionv1alpha1.BackupStatusUpdate{ { Path: "manifests.backupLog", ContainerName: "postgresql", diff --git a/controllers/dataprotection/backuppolicy_controller.go b/controllers/dataprotection/backuppolicy_controller.go index 3698baab9..c1e1d3ba2 100644 --- a/controllers/dataprotection/backuppolicy_controller.go +++ b/controllers/dataprotection/backuppolicy_controller.go @@ -1,33 +1,38 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection import ( "context" - "embed" "encoding/json" "fmt" + "reflect" "sort" - "time" + "strings" "github.com/leaanthony/debme" "github.com/spf13/viper" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -51,30 +56,10 @@ type BackupPolicyReconciler struct { Recorder record.EventRecorder } -type backupPolicyOptions struct { - Name string `json:"name"` - Namespace string `json:"namespace"` - MgrNamespace string `json:"mgrNamespace"` - Cluster string `json:"cluster"` - Schedule string `json:"schedule"` - BackupType string `json:"backupType"` - TTL *metav1.Duration `json:"ttl,omitempty"` - ServiceAccount string `json:"serviceAccount"` -} - -var ( - //go:embed cue/* - cueTemplates embed.FS -) - // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicies,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicies/status,verbs=get;update;patch // +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicies/finalizers,verbs=update -// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicytemplates,verbs=get;list;watch;create;update;patch;delete -// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicytemplates/status,verbs=get;update;patch -// +kubebuilder:rbac:groups=dataprotection.kubeblocks.io,resources=backuppolicytemplates/finalizers,verbs=update - // +kubebuilder:rbac:groups=batch,resources=cronjobs,verbs=get;list;watch;create;update;patch;delete // +kubebuilder:rbac:groups=batch,resources=cronjobs/status,verbs=get // +kubebuilder:rbac:groups=batch,resources=cronjobs/finalizers,verbs=update;patch @@ -89,8 +74,6 @@ var ( // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile func (r *BackupPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - // NOTES: // setup common request context reqCtx := intctrlutil.RequestCtx{ @@ -105,6 +88,8 @@ func (r *BackupPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") } + originBackupPolicy := backupPolicy.DeepCopy() + // handle finalizer res, err := intctrlutil.HandleCRDeletion(reqCtx, r, backupPolicy, dataProtectionFinalizerName, func() (*ctrl.Result, error) { return nil, r.deleteExternalResources(reqCtx, backupPolicy) @@ -113,235 +98,206 @@ func (r *BackupPolicyReconciler) Reconcile(ctx context.Context, req ctrl.Request return *res, err } - switch backupPolicy.Status.Phase { - case "", dataprotectionv1alpha1.ConfigNew: - return r.doNewPhaseAction(reqCtx, backupPolicy) - case dataprotectionv1alpha1.ConfigInProgress: - return r.doInProgressPhaseAction(reqCtx, backupPolicy) - case dataprotectionv1alpha1.ConfigAvailable: - return r.doAvailablePhaseAction(reqCtx, backupPolicy) - default: - return intctrlutil.Reconciled() + // try to remove expired or oldest backups, triggered by cronjob controller + if err = r.removeExpiredBackups(reqCtx); err != nil { + return r.patchStatusFailed(reqCtx, backupPolicy, "RemoveExpiredBackupsFailed", err) } -} -func (r *BackupPolicyReconciler) doNewPhaseAction( - reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) (ctrl.Result, error) { - // update status phase - patch := client.MergeFrom(backupPolicy.DeepCopy()) - backupPolicy.Status.Phase = dataprotectionv1alpha1.ConfigInProgress - if err := r.Client.Status().Patch(reqCtx.Ctx, backupPolicy, patch); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + if err = r.handleSnapshotPolicy(reqCtx, backupPolicy); err != nil { + return r.patchStatusFailed(reqCtx, backupPolicy, "HandleSnapshotPolicyFailed", err) } - return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") -} - -func (r *BackupPolicyReconciler) doInProgressPhaseAction( - reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) (ctrl.Result, error) { - // update default value from viper config if necessary - patch := client.MergeFrom(backupPolicy.DeepCopy()) - if len(backupPolicy.Spec.Schedule) == 0 { - schedule := viper.GetString("DP_BACKUP_SCHEDULE") - if len(schedule) > 0 { - backupPolicy.Spec.Schedule = schedule - } - } - if backupPolicy.Spec.TTL == nil { - ttlString := viper.GetString("DP_BACKUP_TTL") - if len(ttlString) > 0 { - ttl, err := time.ParseDuration(ttlString) - if err == nil { - backupPolicy.Spec.TTL = &metav1.Duration{Duration: ttl} - } - } - } - for k, v := range backupPolicy.Spec.Target.LabelsSelector.MatchLabels { - if backupPolicy.Labels == nil { - backupPolicy.SetLabels(map[string]string{}) - } - backupPolicy.Labels[k] = v + if err = r.handleFullPolicy(reqCtx, backupPolicy); err != nil { + return r.patchStatusFailed(reqCtx, backupPolicy, "HandleFullPolicyFailed", err) } - if backupPolicy.Spec.Target.Secret == nil { - backupPolicy.Spec.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{} + if err = r.handleIncrementalPolicy(reqCtx, backupPolicy); err != nil { + return r.patchStatusFailed(reqCtx, backupPolicy, "HandleIncrementalPolicyFailed", err) } - // merge backup policy template spec - if err := r.mergeBackupPolicyTemplate(reqCtx, backupPolicy); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } + return r.patchStatusAvailable(reqCtx, originBackupPolicy, backupPolicy) +} - if err := r.fillSecretName(reqCtx, backupPolicy, true); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - // fill remaining fields - r.fillDefaultValueIfRequired(backupPolicy) +// SetupWithManager sets up the controller with the Manager. +func (r *BackupPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&dataprotectionv1alpha1.BackupPolicy{}). + WithOptions(controller.Options{ + MaxConcurrentReconciles: viper.GetInt(maxConcurDataProtectionReconKey), + }). + Complete(r) +} - if err := r.Client.Patch(reqCtx.Ctx, backupPolicy, patch); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } +func (r *BackupPolicyReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { + // delete cronjob resource + cronjob := &batchv1.CronJob{} - // if backup policy is available, try to remove expired or oldest backups - if backupPolicy.Status.Phase == dataprotectionv1alpha1.ConfigAvailable { - if err := r.removeExpiredBackups(reqCtx); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + for _, v := range []dataprotectionv1alpha1.BackupType{dataprotectionv1alpha1.BackupTypeDataFile, + dataprotectionv1alpha1.BackupTypeLogFile, dataprotectionv1alpha1.BackupTypeSnapshot} { + key := types.NamespacedName{ + Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), + Name: r.getCronJobName(backupPolicy.Name, backupPolicy.Namespace, v), } - if err := r.removeOldestBackups(reqCtx, backupPolicy); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + if err := r.Client.Get(reqCtx.Ctx, key, cronjob); err != nil { + if apierrors.IsNotFound(err) { + continue + } + return err } - return intctrlutil.Reconciled() - } - // create cronjob from cue template. - if err := r.createCronJobIfNeeded(reqCtx, backupPolicy); err != nil { - r.Recorder.Eventf(backupPolicy, corev1.EventTypeWarning, "CreatingBackupPolicy", - "Failed to create cronjob %s.", err.Error()) - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - - // update status phase - backupPolicy.Status.Phase = dataprotectionv1alpha1.ConfigAvailable - if err := r.Client.Status().Patch(reqCtx.Ctx, backupPolicy, patch); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + // TODO: checks backupPolicy's uuid to ensure the cronjob is created by this backupPolicy + if err := r.removeCronJobFinalizer(reqCtx, cronjob); err != nil { + return err + } + if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, cronjob); err != nil { + // failed delete k8s job, return error info. + return err + } } - return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") + return nil } -func (r *BackupPolicyReconciler) doAvailablePhaseAction( - reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) (ctrl.Result, error) { - // patch cronjob if backup policy spec patched - if err := r.patchCronJob(reqCtx, backupPolicy); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") - } - - // try to remove expired or oldest backups, triggered by cronjob controller - if err := r.removeExpiredBackups(reqCtx); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") +// patchStatusAvailable patches backup policy status phase to available. +func (r *BackupPolicyReconciler) patchStatusAvailable(reqCtx intctrlutil.RequestCtx, + originBackupPolicy, + backupPolicy *dataprotectionv1alpha1.BackupPolicy) (ctrl.Result, error) { + if !reflect.DeepEqual(originBackupPolicy.Spec, backupPolicy.Spec) { + if err := r.Client.Update(reqCtx.Ctx, backupPolicy); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } } - if err := r.removeOldestBackups(reqCtx, backupPolicy); err != nil { - return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + // update status phase + if backupPolicy.Status.Phase != dataprotectionv1alpha1.PolicyAvailable || + backupPolicy.Status.ObservedGeneration != backupPolicy.Generation { + patch := client.MergeFrom(backupPolicy.DeepCopy()) + backupPolicy.Status.Phase = dataprotectionv1alpha1.PolicyAvailable + backupPolicy.Status.FailureReason = "" + if err := r.Client.Status().Patch(reqCtx.Ctx, backupPolicy, patch); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } } return intctrlutil.Reconciled() } -func (r *BackupPolicyReconciler) mergeBackupPolicyTemplate( - reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { - if backupPolicy.Spec.BackupPolicyTemplateName == "" { - return nil - } - template := &dataprotectionv1alpha1.BackupPolicyTemplate{} - key := types.NamespacedName{Namespace: backupPolicy.Namespace, Name: backupPolicy.Spec.BackupPolicyTemplateName} - if err := r.Client.Get(reqCtx.Ctx, key, template); err != nil { - r.Recorder.Eventf(backupPolicy, corev1.EventTypeWarning, "BackupPolicyTemplateFailed", - "Failed to get backupPolicyTemplateName: %s, reason: %s", key.Name, err.Error()) - return err - } - - if backupPolicy.Spec.BackupToolName == "" { - backupPolicy.Spec.BackupToolName = template.Spec.BackupToolName +// patchStatusFailed patches backup policy status phase to failed. +func (r *BackupPolicyReconciler) patchStatusFailed(reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy, + reason string, + err error) (ctrl.Result, error) { + if intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeRequeue) { + return intctrlutil.RequeueAfter(reconcileInterval, reqCtx.Log, "") + } + backupPolicyDeepCopy := backupPolicy.DeepCopy() + backupPolicy.Status.Phase = dataprotectionv1alpha1.PolicyFailed + backupPolicy.Status.FailureReason = err.Error() + if !reflect.DeepEqual(backupPolicy.Status, backupPolicyDeepCopy.Status) { + if patchErr := r.Client.Status().Patch(reqCtx.Ctx, backupPolicy, client.MergeFrom(backupPolicyDeepCopy)); patchErr != nil { + return intctrlutil.RequeueWithError(patchErr, reqCtx.Log, "") + } } + r.Recorder.Event(backupPolicy, corev1.EventTypeWarning, reason, err.Error()) + return intctrlutil.RequeueWithError(err, reqCtx.Log, "") +} - // if template.Spec.CredentialKeyword is nil, use system account; else use root conn secret - useSysAcct := template.Spec.CredentialKeyword == nil - if err := r.fillSecretName(reqCtx, backupPolicy, useSysAcct); err != nil { +func (r *BackupPolicyReconciler) removeExpiredBackups(reqCtx intctrlutil.RequestCtx) error { + backups := dataprotectionv1alpha1.BackupList{} + if err := r.Client.List(reqCtx.Ctx, &backups, + client.InNamespace(reqCtx.Req.Namespace)); err != nil { return err } - - if template.Spec.CredentialKeyword != nil { - if backupPolicy.Spec.Target.Secret.UserKeyword == "" { - backupPolicy.Spec.Target.Secret.UserKeyword = template.Spec.CredentialKeyword.UserKeyword + now := metav1.Now() + for _, item := range backups.Items { + // ignore retained backup. + if strings.EqualFold(item.GetLabels()[constant.BackupProtectionLabelKey], constant.BackupRetain) { + continue } - if backupPolicy.Spec.Target.Secret.PasswordKeyword == "" { - backupPolicy.Spec.Target.Secret.PasswordKeyword = template.Spec.CredentialKeyword.PasswordKeyword + if item.Status.Expiration != nil && item.Status.Expiration.Before(&now) { + if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, &item); err != nil { + // failed delete backups, return error info. + return err + } } } - if backupPolicy.Spec.TTL == nil { - backupPolicy.Spec.TTL = template.Spec.TTL - } - if backupPolicy.Spec.Schedule == "" { - backupPolicy.Spec.Schedule = template.Spec.Schedule - } - if backupPolicy.Spec.Hooks == nil { - backupPolicy.Spec.Hooks = template.Spec.Hooks - } - if backupPolicy.Spec.OnFailAttempted == 0 { - backupPolicy.Spec.OnFailAttempted = template.Spec.OnFailAttempted - } - if len(backupPolicy.Spec.BackupStatusUpdates) == 0 { - backupPolicy.Spec.BackupStatusUpdates = template.Spec.BackupStatusUpdates - } return nil } -func (r *BackupPolicyReconciler) fillDefaultValueIfRequired(backupPolicy *dataprotectionv1alpha1.BackupPolicy) { - // set required parameter default values if template is empty - if backupPolicy.Spec.Target.Secret.UserKeyword == "" { - backupPolicy.Spec.Target.Secret.UserKeyword = "username" +// removeOldestBackups removes old backups according to backupsHistoryLimit policy. +func (r *BackupPolicyReconciler) removeOldestBackups(reqCtx intctrlutil.RequestCtx, + backupPolicyName string, + backupType dataprotectionv1alpha1.BackupType, + backupsHistoryLimit int32) error { + if backupsHistoryLimit == 0 { + return nil } - if backupPolicy.Spec.Target.Secret.PasswordKeyword == "" { - backupPolicy.Spec.Target.Secret.PasswordKeyword = "password" + matchLabels := map[string]string{ + dataProtectionLabelBackupPolicyKey: backupPolicyName, + dataProtectionLabelBackupTypeKey: string(backupType), + dataProtectionLabelAutoBackupKey: "true", } -} - -// fillSecretName fills secret name if it is empty. -// If BackupPolicy.Sect.Target.Secret is not nil, use secret specified in BackupPolicy. -// Otherwise, lookup BackupPolicyTemplate and check if username and password are specified. -// If so, use root connection secret; otherwise, try system account before root connection. -func (r *BackupPolicyReconciler) fillSecretName(reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy, useSysAccount bool) error { - if len(backupPolicy.Spec.Target.Secret.Name) > 0 { - return nil + backups := dataprotectionv1alpha1.BackupList{} + if err := r.Client.List(reqCtx.Ctx, &backups, + client.InNamespace(reqCtx.Req.Namespace), + client.MatchingLabels(matchLabels)); err != nil { + return err } - // get cluster name from labels - instanceName := backupPolicy.Spec.Target.LabelsSelector.MatchLabels[constant.AppInstanceLabelKey] - if len(instanceName) == 0 { - // REVIEW/TODO: need avoid using dynamic error string, this is bad for - // error type checking (errors.Is) - return fmt.Errorf("failed to get instance name from labels: %v", backupPolicy.Spec.Target.LabelsSelector.MatchLabels) - } - var labels map[string]string - if useSysAccount { - labels = map[string]string{ - constant.AppInstanceLabelKey: instanceName, - constant.ClusterAccountLabelKey: (string)(appsv1alpha1.DataprotectionAccount), + // filter final state backups only + backupItems := []dataprotectionv1alpha1.Backup{} + for _, item := range backups.Items { + if item.Status.Phase == dataprotectionv1alpha1.BackupCompleted || + item.Status.Phase == dataprotectionv1alpha1.BackupFailed { + backupItems = append(backupItems, item) } - } else { - labels = map[string]string{ - constant.AppInstanceLabelKey: instanceName, - constant.AppManagedByLabelKey: constant.AppName, + } + numToDelete := len(backupItems) - int(backupsHistoryLimit) + if numToDelete <= 0 { + return nil + } + sort.Sort(byBackupStartTime(backupItems)) + for i := 0; i < numToDelete; i++ { + if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, &backupItems[i]); err != nil { + // failed delete backups, return error info. + return err } } + return nil +} - secrets := corev1.SecretList{} - if err := r.Client.List(reqCtx.Ctx, &secrets, client.MatchingLabels(labels)); err != nil { - return err - } - if len(secrets.Items) > 0 { - backupPolicy.Spec.Target.Secret.Name = secrets.Items[0].GetName() - return nil +func (r *BackupPolicyReconciler) getCronJobName(backupPolicyName, backupPolicyNamespace string, backupType dataprotectionv1alpha1.BackupType) string { + name := fmt.Sprintf("%s-%s", backupPolicyName, backupPolicyNamespace) + if len(name) > 30 { + name = name[:30] } - // REVIEW/TODO: need avoid using dynamic error string, this is bad for - // error type checking (errors.Is) - return fmt.Errorf("no secret found for backup policy %s", backupPolicy.GetName()) + return fmt.Sprintf("%s-%s", name, string(backupType)) } -func (r *BackupPolicyReconciler) buildCronJob(backupPolicy *dataprotectionv1alpha1.BackupPolicy) (*batchv1.CronJob, error) { +// buildCronJob builds cronjob from backup policy. +func (r *BackupPolicyReconciler) buildCronJob( + backupPolicy *dataprotectionv1alpha1.BackupPolicy, + target dataprotectionv1alpha1.TargetCluster, + cronExpression string, + backType dataprotectionv1alpha1.BackupType) (*batchv1.CronJob, error) { tplFile := "cronjob.cue" cueFS, _ := debme.FS(cueTemplates, "cue") cueTpl, err := intctrlutil.NewCUETplFromBytes(cueFS.ReadFile(tplFile)) if err != nil { return nil, err } + var ttl metav1.Duration + if backupPolicy.Spec.Retention != nil && backupPolicy.Spec.Retention.TTL != nil { + ttl = metav1.Duration{Duration: dataprotectionv1alpha1.ToDuration(backupPolicy.Spec.Retention.TTL)} + } cueValue := intctrlutil.NewCUEBuilder(*cueTpl) options := backupPolicyOptions{ - Name: backupPolicy.Name, - Namespace: backupPolicy.Namespace, - Cluster: backupPolicy.Spec.Target.LabelsSelector.MatchLabels[constant.AppInstanceLabelKey], - Schedule: backupPolicy.Spec.Schedule, - TTL: backupPolicy.Spec.TTL, - BackupType: backupPolicy.Spec.BackupType, - ServiceAccount: viper.GetString("KUBEBLOCKS_SERVICEACCOUNT_NAME"), - MgrNamespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), + Name: r.getCronJobName(backupPolicy.Name, backupPolicy.Namespace, backType), + BackupPolicyName: backupPolicy.Name, + Namespace: backupPolicy.Namespace, + Cluster: target.LabelsSelector.MatchLabels[constant.AppInstanceLabelKey], + Schedule: cronExpression, + TTL: ttl, + BackupType: string(backType), + ServiceAccount: viper.GetString("KUBEBLOCKS_SERVICEACCOUNT_NAME"), + MgrNamespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), + Image: viper.GetString(constant.KBToolsImage), } backupPolicyOptionsByte, err := json.Marshal(options) if err != nil { @@ -350,18 +306,21 @@ func (r *BackupPolicyReconciler) buildCronJob(backupPolicy *dataprotectionv1alph if err = cueValue.Fill("options", backupPolicyOptionsByte); err != nil { return nil, err } - - cronjobByte, err := cueValue.Lookup("cronjob") + cuePath := "cronjob" + if backType == dataprotectionv1alpha1.BackupTypeLogFile { + cuePath = "cronjob_logfile" + } + cronjobByte, err := cueValue.Lookup(cuePath) if err != nil { return nil, err } - cronjob := batchv1.CronJob{} - if err = json.Unmarshal(cronjobByte, &cronjob); err != nil { + cronjob := &batchv1.CronJob{} + if err = json.Unmarshal(cronjobByte, cronjob); err != nil { return nil, err } - controllerutil.AddFinalizer(&cronjob, dataProtectionFinalizerName) + controllerutil.AddFinalizer(cronjob, dataProtectionFinalizerName) // set labels for k, v := range backupPolicy.Labels { @@ -370,144 +329,256 @@ func (r *BackupPolicyReconciler) buildCronJob(backupPolicy *dataprotectionv1alph } cronjob.Labels[k] = v } - return &cronjob, nil + cronjob.Labels[dataProtectionLabelBackupPolicyKey] = backupPolicy.Name + cronjob.Labels[dataProtectionLabelBackupTypeKey] = string(backType) + return cronjob, nil } -func (r *BackupPolicyReconciler) removeExpiredBackups(reqCtx intctrlutil.RequestCtx) error { - backups := dataprotectionv1alpha1.BackupList{} - if err := r.Client.List(reqCtx.Ctx, &backups, - client.InNamespace(reqCtx.Req.Namespace)); err != nil { +func (r *BackupPolicyReconciler) removeCronJobFinalizer(reqCtx intctrlutil.RequestCtx, cronjob *batchv1.CronJob) error { + patch := client.MergeFrom(cronjob.DeepCopy()) + controllerutil.RemoveFinalizer(cronjob, dataProtectionFinalizerName) + return r.Patch(reqCtx.Ctx, cronjob, patch) +} + +// reconcileCronJob will create/delete/patch cronjob according to cronExpression and policy changes. +func (r *BackupPolicyReconciler) reconcileCronJob(reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy, + basePolicy dataprotectionv1alpha1.BasePolicy, + cronExpression string, + backType dataprotectionv1alpha1.BackupType) error { + cronjobProto, err := r.buildCronJob(backupPolicy, basePolicy.Target, cronExpression, backType) + if err != nil { return err } - now := metav1.Now() - for _, item := range backups.Items { - // ignore retained backup. - if item.GetLabels()[constant.BackupProtectionLabelKey] == constant.BackupRetain { - continue - } - if item.Status.Expiration != nil && item.Status.Expiration.Before(&now) { - if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, &item); err != nil { - // failed delete backups, return error info. + cronJob := &batchv1.CronJob{} + if err = r.Client.Get(reqCtx.Ctx, client.ObjectKey{Name: cronjobProto.Name, + Namespace: cronjobProto.Namespace}, cronJob); err != nil && !apierrors.IsNotFound(err) { + return err + } + + if len(cronExpression) == 0 { + if len(cronJob.Name) != 0 { + // delete the old cronjob. + if err = r.removeCronJobFinalizer(reqCtx, cronJob); err != nil { return err } + return r.Client.Delete(reqCtx.Ctx, cronJob) } + // if no cron expression, return + return nil } - return nil -} -func buildBackupLabelsForRemove(backupPolicy *dataprotectionv1alpha1.BackupPolicy) map[string]string { - return map[string]string{ - constant.AppInstanceLabelKey: backupPolicy.Labels[constant.AppInstanceLabelKey], - dataProtectionLabelAutoBackupKey: "true", + if len(cronJob.Name) == 0 { + // if no cronjob, create it. + return r.Client.Create(reqCtx.Ctx, cronjobProto) } + // sync the cronjob with the current backup policy configuration. + patch := client.MergeFrom(cronJob.DeepCopy()) + cronJob.Spec.JobTemplate.Spec.BackoffLimit = &basePolicy.OnFailAttempted + cronJob.Spec.JobTemplate.Spec.Template = cronjobProto.Spec.JobTemplate.Spec.Template + cronJob.Spec.Schedule = cronExpression + return r.Client.Patch(reqCtx.Ctx, cronJob, patch) } -func (r *BackupPolicyReconciler) removeOldestBackups(reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { - if backupPolicy.Spec.BackupsHistoryLimit == 0 { - return nil - } +// handlePolicy handles backup policy. +func (r *BackupPolicyReconciler) handlePolicy(reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy, + basePolicy dataprotectionv1alpha1.BasePolicy, + cronExpression string, + backType dataprotectionv1alpha1.BackupType) error { - backups := dataprotectionv1alpha1.BackupList{} - if err := r.Client.List(reqCtx.Ctx, &backups, - client.InNamespace(reqCtx.Req.Namespace), - client.MatchingLabels(buildBackupLabelsForRemove(backupPolicy))); err != nil { + if err := r.reconfigure(reqCtx, backupPolicy, basePolicy, backType); err != nil { return err } - // filter final state backups only - backupItems := []dataprotectionv1alpha1.Backup{} - for _, item := range backups.Items { - if item.Status.Phase == dataprotectionv1alpha1.BackupCompleted || - item.Status.Phase == dataprotectionv1alpha1.BackupFailed { - backupItems = append(backupItems, item) - } + // create/delete/patch cronjob workload + if err := r.reconcileCronJob(reqCtx, backupPolicy, basePolicy, + cronExpression, backType); err != nil { + return err } - numToDelete := len(backupItems) - int(backupPolicy.Spec.BackupsHistoryLimit) - if numToDelete <= 0 { + return r.removeOldestBackups(reqCtx, backupPolicy.Name, backType, basePolicy.BackupsHistoryLimit) +} + +// handleSnapshotPolicy handles snapshot policy. +func (r *BackupPolicyReconciler) handleSnapshotPolicy( + reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { + if backupPolicy.Spec.Snapshot == nil { + // TODO delete cronjob if exists return nil } - sort.Sort(byBackupStartTime(backupItems)) - for i := 0; i < numToDelete; i++ { - if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, &backupItems[i]); err != nil { - // failed delete backups, return error info. - return err - } + var cronExpression string + schedule := backupPolicy.Spec.Schedule.Snapshot + if schedule != nil && schedule.Enable { + cronExpression = schedule.CronExpression } - return nil + return r.handlePolicy(reqCtx, backupPolicy, backupPolicy.Spec.Snapshot.BasePolicy, + cronExpression, dataprotectionv1alpha1.BackupTypeSnapshot) } -// SetupWithManager sets up the controller with the Manager. -func (r *BackupPolicyReconciler) SetupWithManager(mgr ctrl.Manager) error { - return ctrl.NewControllerManagedBy(mgr). - For(&dataprotectionv1alpha1.BackupPolicy{}). - WithOptions(controller.Options{ - MaxConcurrentReconciles: viper.GetInt(maxConcurDataProtectionReconKey), - }). - Complete(r) +// handleFullPolicy handles datafile policy. +func (r *BackupPolicyReconciler) handleFullPolicy( + reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { + if backupPolicy.Spec.Datafile == nil { + // TODO delete cronjob if exists + return nil + } + var cronExpression string + schedule := backupPolicy.Spec.Schedule.Datafile + if schedule != nil && schedule.Enable { + cronExpression = schedule.CronExpression + } + r.setGlobalPersistentVolumeClaim(backupPolicy.Spec.Datafile) + return r.handlePolicy(reqCtx, backupPolicy, backupPolicy.Spec.Datafile.BasePolicy, + cronExpression, dataprotectionv1alpha1.BackupTypeDataFile) } -func (r *BackupPolicyReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { - // delete cronjob resource - cronjob := &batchv1.CronJob{} - - key := types.NamespacedName{ - Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), - Name: backupPolicy.Name, +// handleIncrementalPolicy handles incremental policy. +func (r *BackupPolicyReconciler) handleIncrementalPolicy( + reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { + if backupPolicy.Spec.Logfile == nil { + return nil } - if err := r.Client.Get(reqCtx.Ctx, key, cronjob); err != nil { - return client.IgnoreNotFound(err) + var cronExpression string + schedule := backupPolicy.Spec.Schedule.Logfile + if schedule != nil && schedule.Enable { + cronExpression = schedule.CronExpression } - if controllerutil.ContainsFinalizer(cronjob, dataProtectionFinalizerName) { - patch := client.MergeFrom(cronjob.DeepCopy()) - controllerutil.RemoveFinalizer(cronjob, dataProtectionFinalizerName) - if err := r.Patch(reqCtx.Ctx, cronjob, patch); err != nil { - return err - } + r.setGlobalPersistentVolumeClaim(backupPolicy.Spec.Logfile) + return r.handlePolicy(reqCtx, backupPolicy, backupPolicy.Spec.Logfile.BasePolicy, + cronExpression, dataprotectionv1alpha1.BackupTypeLogFile) +} + +// setGlobalPersistentVolumeClaim sets global config of pvc to common policy. +func (r *BackupPolicyReconciler) setGlobalPersistentVolumeClaim(backupPolicy *dataprotectionv1alpha1.CommonBackupPolicy) { + pvcCfg := backupPolicy.PersistentVolumeClaim + globalPVCName := viper.GetString(constant.CfgKeyBackupPVCName) + if len(pvcCfg.Name) == 0 && globalPVCName != "" { + backupPolicy.PersistentVolumeClaim.Name = globalPVCName } - if err := intctrlutil.BackgroundDeleteObject(r.Client, reqCtx.Ctx, cronjob); err != nil { - // failed delete k8s job, return error info. - return err + + globalInitCapacity := viper.GetString(constant.CfgKeyBackupPVCInitCapacity) + if pvcCfg.InitCapacity.IsZero() && globalInitCapacity != "" { + backupPolicy.PersistentVolumeClaim.InitCapacity = resource.MustParse(globalInitCapacity) } +} - return nil +type backupReconfigureRef struct { + Name string `json:"name"` + Key string `json:"key"` + Enable parameterPairs `json:"enable,omitempty"` + Disable parameterPairs `json:"disable,omitempty"` } -// createCronJobIfNeeded create cronjob spec if backup policy set schedule -func (r *BackupPolicyReconciler) createCronJobIfNeeded( - reqCtx intctrlutil.RequestCtx, - backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { - if backupPolicy.Spec.Schedule == "" { - r.Recorder.Eventf(backupPolicy, corev1.EventTypeNormal, "BackupPolicy", - "Backups will not be automatically scheduled due to lack of schedule configuration.") +type parameterPairs map[string][]appsv1alpha1.ParameterPair + +func (r *BackupPolicyReconciler) reconfigure(reqCtx intctrlutil.RequestCtx, + backupPolicy *dataprotectionv1alpha1.BackupPolicy, + basePolicy dataprotectionv1alpha1.BasePolicy, + backType dataprotectionv1alpha1.BackupType) error { + + reconfigRef := backupPolicy.Annotations[constant.ReconfigureRefAnnotationKey] + if reconfigRef == "" { return nil } + configRef := backupReconfigureRef{} + if err := json.Unmarshal([]byte(reconfigRef), &configRef); err != nil { + return err + } - // create cronjob from cue template. - cronjob, err := r.buildCronJob(backupPolicy) - if err != nil { + enable := false + commonSchedule := backupPolicy.Spec.GetCommonSchedulePolicy(backType) + if commonSchedule != nil { + enable = commonSchedule.Enable + } + if backupPolicy.Annotations[constant.LastAppliedConfigAnnotationKey] == "" && !enable { + // disable in the first policy created, no need reconfigure because default configs had been set. + return nil + } + configParameters := configRef.Disable + if enable { + configParameters = configRef.Enable + } + if configParameters == nil { + return nil + } + parameters := configParameters[string(backType)] + if len(parameters) == 0 { + // skip reconfigure if not found parameters. + return nil + } + updateParameterPairsBytes, _ := json.Marshal(parameters) + updateParameterPairs := string(updateParameterPairsBytes) + if updateParameterPairs == backupPolicy.Annotations[constant.LastAppliedConfigAnnotationKey] { + // reconcile the config job if finished + return r.reconcileReconfigure(reqCtx, backupPolicy) + } + + ops := appsv1alpha1.OpsRequest{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: backupPolicy.Name + "-", + Namespace: backupPolicy.Namespace, + Labels: map[string]string{ + dataProtectionLabelBackupPolicyKey: backupPolicy.Name, + }, + }, + Spec: appsv1alpha1.OpsRequestSpec{ + Type: appsv1alpha1.ReconfiguringType, + ClusterRef: basePolicy.Target.LabelsSelector.MatchLabels[constant.AppInstanceLabelKey], + Reconfigure: &appsv1alpha1.Reconfigure{ + ComponentOps: appsv1alpha1.ComponentOps{ + ComponentName: basePolicy.Target.LabelsSelector.MatchLabels[constant.KBAppComponentLabelKey], + }, + Configurations: []appsv1alpha1.Configuration{ + { + Name: configRef.Name, + Keys: []appsv1alpha1.ParameterConfig{ + { + Key: configRef.Key, + Parameters: parameters, + }, + }, + }, + }, + }, + }, + } + if err := r.Client.Create(reqCtx.Ctx, &ops); err != nil { return err } - if err = r.Client.Create(reqCtx.Ctx, cronjob); err != nil { - // ignore already exists. - return client.IgnoreAlreadyExists(err) + + r.Recorder.Eventf(backupPolicy, corev1.EventTypeNormal, "Reconfiguring", "update config %s", updateParameterPairs) + patch := client.MergeFrom(backupPolicy.DeepCopy()) + if backupPolicy.Annotations == nil { + backupPolicy.Annotations = map[string]string{} } - return nil + backupPolicy.Annotations[constant.LastAppliedConfigAnnotationKey] = updateParameterPairs + if err := r.Client.Patch(reqCtx.Ctx, backupPolicy, patch); err != nil { + return err + } + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeRequeue, "requeue to waiting for ops %s finished.", ops.Name) } -// patchCronJob patch cronjob spec if backup policy patched -func (r *BackupPolicyReconciler) patchCronJob( - reqCtx intctrlutil.RequestCtx, +func (r *BackupPolicyReconciler) reconcileReconfigure(reqCtx intctrlutil.RequestCtx, backupPolicy *dataprotectionv1alpha1.BackupPolicy) error { - cronJob := &batchv1.CronJob{} - if err := r.Client.Get(reqCtx.Ctx, reqCtx.Req.NamespacedName, cronJob); err != nil { - return client.IgnoreNotFound(err) - } - patch := client.MergeFrom(cronJob.DeepCopy()) - cronJob, err := r.buildCronJob(backupPolicy) - if err != nil { + opsList := appsv1alpha1.OpsRequestList{} + if err := r.Client.List(reqCtx.Ctx, &opsList, + client.InNamespace(backupPolicy.Namespace), + client.MatchingLabels{dataProtectionLabelBackupPolicyKey: backupPolicy.Name}); err != nil { return err } - cronJob.Spec.Schedule = backupPolicy.Spec.Schedule - cronJob.Spec.JobTemplate.Spec.BackoffLimit = &backupPolicy.Spec.OnFailAttempted - return r.Client.Patch(reqCtx.Ctx, cronJob, patch) + if len(opsList.Items) > 0 { + sort.Slice(opsList.Items, func(i, j int) bool { + return opsList.Items[j].CreationTimestamp.Before(&opsList.Items[i].CreationTimestamp) + }) + latestOps := opsList.Items[0] + if latestOps.Status.Phase == appsv1alpha1.OpsFailedPhase { + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeReconfigureFailed, "ops failed %s", latestOps.Name) + } else if latestOps.Status.Phase != appsv1alpha1.OpsSucceedPhase { + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeRequeue, "requeue to waiting for ops %s finished.", latestOps.Name) + } + } + return nil } diff --git a/controllers/dataprotection/backuppolicy_controller_test.go b/controllers/dataprotection/backuppolicy_controller_test.go index ca29c20cf..f4370db7f 100644 --- a/controllers/dataprotection/backuppolicy_controller_test.go +++ b/controllers/dataprotection/backuppolicy_controller_test.go @@ -1,23 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection import ( - "context" + "fmt" "time" . "github.com/onsi/ginkgo/v2" @@ -30,7 +33,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/generics" @@ -43,20 +45,16 @@ var _ = Describe("Backup Policy Controller", func() { const containerName = "mysql" const defaultPVCSize = "1Gi" const backupPolicyName = "test-backup-policy" - const backupPolicyTplName = "test-backup-policy-template" - const backupRemoteVolumeName = "backup-remote-volume" const backupRemotePVCName = "backup-remote-pvc" const defaultSchedule = "0 3 * * *" - const defaultTTL = "168h0m0s" + const defaultTTL = "7d" const backupNamePrefix = "test-backup-job-" const mgrNamespace = "kube-system" - viper.SetDefault("DP_BACKUP_SCHEDULE", "0 3 * * *") - viper.SetDefault("DP_BACKUP_TTL", "168h0m0s") viper.SetDefault(constant.CfgKeyCtrlrMgrNS, testCtx.DefaultNamespace) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -72,13 +70,13 @@ var _ = Describe("Backup Policy Controller", func() { testapps.ClearResources(&testCtx, intctrlutil.BackupPolicySignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.JobSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.CronJobSignature, inNS, ml) - testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PersistentVolumeClaimSignature, true, inNS, ml) + testapps.ClearResources(&testCtx, intctrlutil.SecretSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, intctrlutil.PersistentVolumeClaimSignature, true, inNS) // mgr namespaced inMgrNS := client.InNamespace(mgrNamespace) testapps.ClearResources(&testCtx, intctrlutil.CronJobSignature, inMgrNS, ml) // non-namespaced testapps.ClearResources(&testCtx, intctrlutil.BackupToolSignature, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, ml) } BeforeEach(func() { @@ -110,15 +108,15 @@ var _ = Describe("Backup Policy Controller", func() { When("creating backup policy with default settings", func() { var backupToolName string - var cronjobKey types.NamespacedName - - BeforeEach(func() { - viper.Set(constant.CfgKeyCtrlrMgrNS, mgrNamespace) - cronjobKey = types.NamespacedName{ - Name: backupPolicyName, + getCronjobKey := func(backupType dpv1alpha1.BackupType) types.NamespacedName { + return types.NamespacedName{ + Name: fmt.Sprintf("%s-%s-%s", backupPolicyName, testCtx.DefaultNamespace, backupType), Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), } + } + BeforeEach(func() { + viper.Set(constant.CfgKeyCtrlrMgrNS, mgrNamespace) By("By creating a backupTool") backupTool := testapps.CreateCustomizedObj(&testCtx, "backup/backuptool.yaml", &dpv1alpha1.BackupTool{}, testapps.RandomizedObjName()) @@ -135,23 +133,24 @@ var _ = Describe("Backup Policy Controller", func() { BeforeEach(func() { By("By creating a backupPolicy from backupTool: " + backupToolName) backupPolicy = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + AddFullPolicy(). SetBackupToolName(backupToolName). SetBackupsHistoryLimit(1). - SetSchedule(defaultSchedule). + SetSchedule(defaultSchedule, true). SetTTL(defaultTTL). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). SetTargetSecretName(clusterName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() backupPolicyKey = client.ObjectKeyFromObject(backupPolicy) }) It("should success", func() { Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) })).Should(Succeed()) - Eventually(testapps.CheckObj(&testCtx, cronjobKey, func(g Gomega, fetched *batchv1.CronJob) { + Eventually(testapps.CheckObj(&testCtx, getCronjobKey(dpv1alpha1.BackupTypeDataFile), func(g Gomega, fetched *batchv1.CronJob) { g.Expect(fetched.Spec.Schedule).To(Equal(defaultSchedule)) })).Should(Succeed()) }) @@ -165,76 +164,73 @@ var _ = Describe("Backup Policy Controller", func() { } autoBackupLabel := map[string]string{ - constant.AppInstanceLabelKey: backupPolicy.Labels[constant.AppInstanceLabelKey], - dataProtectionLabelAutoBackupKey: "true", + dataProtectionLabelAutoBackupKey: "true", + dataProtectionLabelBackupPolicyKey: backupPolicyName, + dataProtectionLabelBackupTypeKey: string(dpv1alpha1.BackupTypeDataFile), } By("create a expired backup") backupExpired := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupNamePrefix). WithRandomName().AddLabelsInMap(autoBackupLabel). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dpv1alpha1.BackupTypeFull). + SetBackupType(dpv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() By("create 1st limit backup") backupOutLimit1 := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupNamePrefix). WithRandomName().AddLabelsInMap(autoBackupLabel). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dpv1alpha1.BackupTypeFull). + SetBackupType(dpv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() By("create 2nd limit backup") backupOutLimit2 := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupNamePrefix). WithRandomName().AddLabelsInMap(autoBackupLabel). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dpv1alpha1.BackupTypeFull). + SetBackupType(dpv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() - By("mock jobs completed") + By("waiting expired backup completed") backupExpiredKey := client.ObjectKeyFromObject(backupExpired) patchK8sJobStatus(backupExpiredKey, batchv1.JobComplete) - backupOutLimit1Key := client.ObjectKeyFromObject(backupOutLimit1) - patchK8sJobStatus(backupOutLimit1Key, batchv1.JobComplete) - backupOutLimit2Key := client.ObjectKeyFromObject(backupOutLimit2) - patchK8sJobStatus(backupOutLimit2Key, batchv1.JobComplete) - - By("waiting expired backup completed") Eventually(testapps.CheckObj(&testCtx, backupExpiredKey, func(g Gomega, fetched *dpv1alpha1.Backup) { g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupCompleted)) })).Should(Succeed()) + By("mock update expired backup status to expire") + backupStatus.Expiration = &metav1.Time{Time: now.Add(-time.Hour * 24)} + backupStatus.StartTimestamp = backupStatus.Expiration + patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupExpired)) + By("waiting 1st limit backup completed") + backupOutLimit1Key := client.ObjectKeyFromObject(backupOutLimit1) + patchK8sJobStatus(backupOutLimit1Key, batchv1.JobComplete) Eventually(testapps.CheckObj(&testCtx, backupOutLimit1Key, func(g Gomega, fetched *dpv1alpha1.Backup) { g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupCompleted)) })).Should(Succeed()) + By("mock update 1st limit backup NOT to expire") + backupStatus.Expiration = &metav1.Time{Time: now.Add(time.Hour * 24)} + backupStatus.StartTimestamp = &metav1.Time{Time: now.Add(time.Hour)} + patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupOutLimit1)) + By("waiting 2nd limit backup completed") + backupOutLimit2Key := client.ObjectKeyFromObject(backupOutLimit2) + patchK8sJobStatus(backupOutLimit2Key, batchv1.JobComplete) Eventually(testapps.CheckObj(&testCtx, backupOutLimit2Key, func(g Gomega, fetched *dpv1alpha1.Backup) { g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.BackupCompleted)) })).Should(Succeed()) - - By("mock update expired backup status to expire") - backupStatus.Expiration = &metav1.Time{Time: now.Add(-time.Hour * 24)} - backupStatus.StartTimestamp = backupStatus.Expiration - patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupExpired)) - By("mock update 1st limit backup NOT to expire") - backupStatus.Expiration = &metav1.Time{Time: now.Add(time.Hour * 24)} - backupStatus.StartTimestamp = &metav1.Time{Time: now.Add(time.Hour)} - patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupOutLimit1)) By("mock update 2nd limit backup NOT to expire") backupStatus.Expiration = &metav1.Time{Time: now.Add(time.Hour * 24)} backupStatus.StartTimestamp = &metav1.Time{Time: now.Add(time.Hour * 2)} patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupOutLimit2)) // trigger the backup policy controller through update cronjob - patchCronJobStatus(cronjobKey) + patchCronJobStatus(getCronjobKey(dpv1alpha1.BackupTypeDataFile)) By("retain the latest backup") - Eventually(testapps.GetListLen(&testCtx, intctrlutil.BackupSignature, - client.MatchingLabels(backupPolicy.Spec.Target.LabelsSelector.MatchLabels), - client.InNamespace(backupPolicy.Namespace))).Should(Equal(1)) + Eventually(testapps.List(&testCtx, intctrlutil.BackupSignature, + client.MatchingLabels(backupPolicy.Spec.Datafile.Target.LabelsSelector.MatchLabels), + client.InNamespace(backupPolicy.Namespace))).Should(HaveLen(1)) }) }) @@ -249,19 +245,13 @@ var _ = Describe("Backup Policy Controller", func() { SetTargetSecretName(clusterName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() backupPolicyKey = client.ObjectKeyFromObject(backupPolicy) }) It("should success", func() { Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - })).Should(Succeed()) - }) - It("should success with empty viper config", func() { - viper.SetDefault("DP_BACKUP_SCHEDULE", "") - Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) })).Should(Succeed()) }) }) @@ -272,235 +262,110 @@ var _ = Describe("Backup Policy Controller", func() { BeforeEach(func() { By("By creating a backupPolicy from backupTool: " + backupToolName) backupPolicy = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + AddSnapshotPolicy(). SetBackupToolName(backupToolName). - SetSchedule("invalid schedule"). + SetSchedule("invalid schedule", true). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). SetTargetSecretName(clusterName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() backupPolicyKey = client.ObjectKeyFromObject(backupPolicy) }) It("should failed", func() { Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).NotTo(Equal(dpv1alpha1.ConfigAvailable)) - })).Should(Succeed()) - }) - }) - - Context("creates a backup policy with backup policy template", func() { - var backupPolicyKey types.NamespacedName - var backupPolicy *dpv1alpha1.BackupPolicy - BeforeEach(func() { - viper.SetDefault("DP_BACKUP_SCHEDULE", nil) - viper.SetDefault("DP_BACKUP_TTL", nil) - By("By creating a backupPolicyTemplate") - template := testapps.NewBackupPolicyTemplateFactory(backupPolicyTplName). - SetBackupToolName(backupToolName). - SetSchedule(defaultSchedule). - SetTTL(defaultTTL). - SetCredentialKeyword("username", "password"). - AddHookPreCommand("touch /data/mysql/.restore;sync"). - AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - Create(&testCtx).GetObject() - - By("By creating a backupPolicy from backupTool: " + backupToolName) - backupPolicy = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupPolicyTplName(template.Name). - AddMatchLabels(constant.AppInstanceLabelKey, clusterName). - SetTargetSecretName(clusterName). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). - Create(&testCtx).GetObject() - backupPolicyKey = client.ObjectKeyFromObject(backupPolicy) - }) - It("should success", func() { - Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - })).Should(Succeed()) - }) - }) - - Context("creates a backup policy with nil pointer credentialKeyword in backupPolicyTemplate", func() { - var backupPolicyKey types.NamespacedName - var backupPolicy *dpv1alpha1.BackupPolicy - BeforeEach(func() { - viper.SetDefault("DP_BACKUP_SCHEDULE", nil) - viper.SetDefault("DP_BACKUP_TTL", nil) - By("By creating a backupPolicyTemplate") - template := testapps.NewBackupPolicyTemplateFactory(backupPolicyTplName). - SetBackupToolName(backupToolName). - SetSchedule(defaultSchedule). - SetTTL(defaultTTL). - Create(&testCtx).GetObject() - - By("By creating a backupPolicy from backupTool: " + backupToolName) - backupPolicy = testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupPolicyTplName(template.Name). - AddMatchLabels(constant.AppInstanceLabelKey, clusterName). - SetTargetSecretName(clusterName). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). - Create(&testCtx).GetObject() - backupPolicyKey = client.ObjectKeyFromObject(backupPolicy) - }) - It("should success", func() { - Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) + g.Expect(fetched.Status.Phase).NotTo(Equal(dpv1alpha1.PolicyAvailable)) })).Should(Succeed()) }) }) - Context("creates a backup policy with empty secret", func() { - var ( - backupSecretName = "backup-secret" - rootSecretName = "root-secret" - secretsMap map[string]*corev1.Secret - ) - - // delete secrets before test starts - cleanSecrets := func() { - // delete rest mocked objects - inNS := client.InNamespace(testCtx.DefaultNamespace) - ml := client.HasLabels{testCtx.TestObjLabelKey} - // delete secret created for backup policy - testapps.ClearResources(&testCtx, intctrlutil.SecretSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupPolicySignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, inNS, ml) - } - - fakeSecret := func(name string, labels map[string]string) *corev1.Secret { - return &corev1.Secret{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: testCtx.DefaultNamespace, - Labels: labels, - }, - } - } - - BeforeEach(func() { - secretsMap = make(map[string]*corev1.Secret) - // mock two secrets for backup policy, one for backup account, one for root conn - secretsMap[backupSecretName] = fakeSecret(backupSecretName, map[string]string{ - constant.AppInstanceLabelKey: clusterName, - constant.ClusterAccountLabelKey: (string)(appsv1alpha1.DataprotectionAccount), - }) - secretsMap[rootSecretName] = fakeSecret(rootSecretName, map[string]string{ - constant.AppInstanceLabelKey: clusterName, - constant.AppManagedByLabelKey: constant.AppName, - }) - - cleanSecrets() - }) - - AfterEach(cleanSecrets) - + Context("creating a backupPolicy with secret", func() { It("creating a backupPolicy with secret", func() { - // create two secrets - for _, v := range secretsMap { - Expect(testCtx.CreateObj(context.Background(), v)).Should(Succeed()) - Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(v), &corev1.Secret{}, true)).Should(Succeed()) - } - By("By creating a backupPolicy with empty secret") randomSecretName := testCtx.GetRandomStr() backupPolicy := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + AddFullPolicy(). SetBackupToolName(backupToolName). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). SetTargetSecretName(randomSecretName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName).Create(&testCtx).GetObject() + SetPVC(backupRemotePVCName). + Create(&testCtx).GetObject() backupPolicyKey := client.ObjectKeyFromObject(backupPolicy) Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - g.Expect(fetched.Spec.Target.Secret.Name).To(Equal(randomSecretName)) + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) + g.Expect(fetched.Spec.Datafile.Target.Secret.Name).To(Equal(randomSecretName)) })).Should(Succeed()) }) + }) - It("creating a backupPolicy with secrets missing", func() { - By("By creating a backupPolicy with empty secret") - backupPolicy := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupToolName(backupToolName). - AddMatchLabels(constant.AppInstanceLabelKey, clusterName). - AddHookPreCommand("touch /data/mysql/.restore;sync"). - AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName).Create(&testCtx).GetObject() - backupPolicyKey := client.ObjectKeyFromObject(backupPolicy) - By("Secrets missing, the backup policy should never be `ConfigAvailable`") - Consistently(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).NotTo(Equal(dpv1alpha1.ConfigAvailable)) - }), time.Millisecond*100).Should(Succeed()) - }) - - It("creating a backupPolicy uses default secret", func() { - // create two secrets - for _, v := range secretsMap { - Expect(testCtx.CreateObj(context.Background(), v)).Should(Succeed()) - Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(v), &corev1.Secret{}, true)).Should(Succeed()) - } - + Context("creating a backupPolicy with global backup config", func() { + It("creating a backupPolicy with global backup config", func() { By("By creating a backupPolicy with empty secret") + pvcName := "backup-data" + pvcInitCapacity := "10Gi" + viper.SetDefault(constant.CfgKeyBackupPVCName, pvcName) + viper.SetDefault(constant.CfgKeyBackupPVCInitCapacity, pvcInitCapacity) backupPolicy := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). + AddFullPolicy(). SetBackupToolName(backupToolName). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). AddHookPreCommand("touch /data/mysql/.restore;sync"). AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName).Create(&testCtx).GetObject() + Create(&testCtx).GetObject() backupPolicyKey := client.ObjectKeyFromObject(backupPolicy) Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - g.Expect(fetched.Spec.Target.Secret.Name).To(Equal(backupSecretName)) + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) + g.Expect(fetched.Spec.Datafile.PersistentVolumeClaim.Name).To(Equal(pvcName)) + g.Expect(fetched.Spec.Datafile.PersistentVolumeClaim.InitCapacity.String()).To(Equal(pvcInitCapacity)) })).Should(Succeed()) }) - - It("create backup policy with tempate and specify credential keyword", func() { - for _, v := range secretsMap { - Expect(testCtx.CreateObj(context.Background(), v)).Should(Succeed()) - Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(v), &corev1.Secret{}, true)).Should(Succeed()) - } - // create template - template := testapps.NewBackupPolicyTemplateFactory(backupPolicyTplName). - SetBackupToolName(backupToolName). - SetCredentialKeyword("username", "password"). - Create(&testCtx).GetObject() - - // create backup policy - By("By creating a backupPolicy from backupTool: " + backupToolName) + }) + Context("creating a logfile backupPolicy", func() { + It("with reconfigure config", func() { + By("creating a backupPolicy") + pvcName := "backup-data" + pvcInitCapacity := "10Gi" + viper.SetDefault(constant.CfgKeyBackupPVCName, pvcName) + viper.SetDefault(constant.CfgKeyBackupPVCInitCapacity, pvcInitCapacity) + reconfigureRef := `{ + "name": "postgresql-configuration", + "key": "postgresql.conf", + "enable": { + "logfile": [{"key":"archive_command","value":"''"}] + }, + "disable": { + "logfile": [{"key": "archive_command","value":"'/bin/true'"}] + } + }` backupPolicy := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupPolicyTplName(template.Name). + AddAnnotations(constant.ReconfigureRefAnnotationKey, reconfigureRef). + SetBackupToolName(backupToolName). + AddIncrementalPolicy(). + AddMatchLabels(constant.AppInstanceLabelKey, clusterName). + AddSnapshotPolicy(). AddMatchLabels(constant.AppInstanceLabelKey, clusterName). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). Create(&testCtx).GetObject() backupPolicyKey := client.ObjectKeyFromObject(backupPolicy) Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - g.Expect(fetched.Spec.Target.Secret.Name).To(Equal(rootSecretName)) + g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) + })).Should(Succeed()) + By("enable schedule for reconfigure") + Eventually(testapps.GetAndChangeObj(&testCtx, backupPolicyKey, func(fetched *dpv1alpha1.BackupPolicy) { + fetched.Spec.Schedule.Logfile = &dpv1alpha1.SchedulePolicy{Enable: true, CronExpression: "* * * * *"} + })).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { + g.Expect(fetched.Annotations[constant.LastAppliedConfigAnnotationKey]).To(Equal(`[{"key":"archive_command","value":"''"}]`)) })).Should(Succeed()) - }) - - It("create backup policy with tempate but without default credential keyword", func() { - // create two secrets - for _, v := range secretsMap { - Expect(testCtx.CreateObj(context.Background(), v)).Should(Succeed()) - Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(v), &corev1.Secret{}, true)).Should(Succeed()) - } - // create template - template := testapps.NewBackupPolicyTemplateFactory(backupPolicyTplName). - SetBackupToolName(backupToolName). - Create(&testCtx).GetObject() - // create backup policy - By("By creating a backupPolicy from backupTool: " + backupToolName) - backupPolicy := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). - SetBackupPolicyTplName(template.Name). - AddMatchLabels(constant.AppInstanceLabelKey, clusterName). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). - Create(&testCtx).GetObject() - backupPolicyKey := client.ObjectKeyFromObject(backupPolicy) + By("disable schedule for reconfigure") + Eventually(testapps.GetAndChangeObj(&testCtx, backupPolicyKey, func(fetched *dpv1alpha1.BackupPolicy) { + fetched.Spec.Schedule.Logfile.Enable = false + })).Should(Succeed()) Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, fetched *dpv1alpha1.BackupPolicy) { - g.Expect(fetched.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) - g.Expect(fetched.Spec.Target.Secret.Name).To(Equal(backupSecretName)) + g.Expect(fetched.Annotations[constant.LastAppliedConfigAnnotationKey]).To(Equal(`[{"key":"archive_command","value":"'/bin/true'"}]`)) })).Should(Succeed()) }) }) diff --git a/controllers/dataprotection/backuptool_controller.go b/controllers/dataprotection/backuptool_controller.go index 80bc59195..c6bb1c866 100644 --- a/controllers/dataprotection/backuptool_controller.go +++ b/controllers/dataprotection/backuptool_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection diff --git a/controllers/dataprotection/cronjob_controller.go b/controllers/dataprotection/cronjob_controller.go index e4ebac321..7bad6b9f2 100644 --- a/controllers/dataprotection/cronjob_controller.go +++ b/controllers/dataprotection/cronjob_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection @@ -67,7 +70,7 @@ func (r *CronJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ct backupPolicyKey := types.NamespacedName{ Namespace: cronJob.Annotations["kubeblocks.io/backup-namespace"], - Name: cronJob.Name, + Name: cronJob.Labels[dataProtectionLabelBackupPolicyKey], } if err = r.Client.Get(reqCtx.Ctx, backupPolicyKey, backupPolicy); err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") @@ -88,6 +91,6 @@ func (r *CronJobReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&batchv1.CronJob{}). Owns(&batchv1.Job{}). - WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.WorkloadFilterPredicate)). + WithEventFilter(predicate.NewPredicateFuncs(intctrlutil.ManagedByKubeBlocksFilterPredicate)). Complete(r) } diff --git a/controllers/dataprotection/cue/cronjob.cue b/controllers/dataprotection/cue/cronjob.cue index 823c6a0f4..ca98465b2 100644 --- a/controllers/dataprotection/cue/cronjob.cue +++ b/controllers/dataprotection/cue/cronjob.cue @@ -1,26 +1,31 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . options: { - name: string - namespace: string - mgrNamespace: string - cluster: string - schedule: string - backupType: string - ttl: string - serviceAccount: string + name: string + backupPolicyName: string + namespace: string + mgrNamespace: string + cluster: string + schedule: string + backupType: string + ttl: string + serviceAccount: string + image: string } cronjob: { @@ -36,7 +41,7 @@ cronjob: { } spec: { schedule: options.schedule - successfulJobsHistoryLimit: 1 + successfulJobsHistoryLimit: 0 failedJobsHistoryLimit: 1 concurrencyPolicy: "Forbid" jobTemplate: spec: template: spec: { @@ -44,7 +49,7 @@ cronjob: { serviceAccountName: options.serviceAccount containers: [{ name: "backup-policy" - image: "appscode/kubectl:1.25" + image: options.image imagePullPolicy: "IfNotPresent" command: [ "sh", @@ -63,10 +68,60 @@ metadata: name: backup-\(options.namespace)-\(options.cluster)-$(date -u +'%Y%m%d%H%M%S') namespace: \(options.namespace) spec: - backupPolicyName: \(options.name) + backupPolicyName: \(options.backupPolicyName) + backupType: \(options.backupType) +EOF +""", + ] + }] + } + } +} + +cronjob_logfile: { + apiVersion: "batch/v1" + kind: "CronJob" + metadata: { + name: options.name + namespace: options.mgrNamespace + annotations: + "kubeblocks.io/backup-namespace": options.namespace + labels: + "app.kubernetes.io/managed-by": "kubeblocks" + } + spec: { + schedule: options.schedule + successfulJobsHistoryLimit: 0 + failedJobsHistoryLimit: 1 + concurrencyPolicy: "Forbid" + jobTemplate: spec: template: spec: { + restartPolicy: "Never" + serviceAccountName: options.serviceAccount + containers: [{ + name: "backup-policy" + image: options.image + imagePullPolicy: "IfNotPresent" + command: [ + "sh", + "-c", + ] + args: [ + """ +kubectl apply -f - <. */ package dataprotection import ( "context" + "fmt" "github.com/spf13/viper" appv1 "k8s.io/api/apps/v1" @@ -60,8 +64,6 @@ type RestoreJobReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.12.1/pkg/reconcile func (r *RestoreJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { - _ = log.FromContext(ctx) - // NOTES: // setup common request context reqCtx := intctrlutil.RequestCtx{ @@ -84,7 +86,6 @@ func (r *RestoreJobReconciler) Reconcile(ctx context.Context, req ctrl.Request) return *res, err } - // restore job reconcile logic here switch restoreJob.Status.Phase { case "", dataprotectionv1alpha1.RestoreJobNew: return r.doRestoreNewPhaseAction(reqCtx, restoreJob) @@ -110,7 +111,7 @@ func (r *RestoreJobReconciler) doRestoreNewPhaseAction( restoreJob *dataprotectionv1alpha1.RestoreJob) (ctrl.Result, error) { // 1. get stateful service and - // 2. set stateful set replicate 0 + // 2. set stateful replicas to 0 patch := []byte(`{"spec":{"replicas":0}}`) if err := r.patchTargetCluster(reqCtx, restoreJob, patch); err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") @@ -170,11 +171,11 @@ func (r *RestoreJobReconciler) doRestoreInProgressPhyAction( switch jobStatusConditions[0].Type { case batchv1.JobComplete: - // update Phase to in Completed + // update Phase to Completed restoreJob.Status.Phase = dataprotectionv1alpha1.RestoreJobCompleted restoreJob.Status.CompletionTimestamp = &metav1.Time{Time: r.clock.Now().UTC()} // get stateful service and - // set stateful set replicate to 1 + // set stateful replicas to 1 patch := []byte(`{"spec":{"replicas":1}}`) if err := r.patchTargetCluster(reqCtx, restoreJob, patch); err != nil { return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") @@ -239,28 +240,21 @@ func (r *RestoreJobReconciler) buildPodSpec(reqCtx intctrlutil.RequestCtx, resto return podSpec, err } - // get backup policy - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - backupPolicyNameSpaceName := types.NamespacedName{ - Namespace: reqCtx.Req.Namespace, - Name: backup.Spec.BackupPolicyName, - } - if err := r.Client.Get(reqCtx.Ctx, backupPolicyNameSpaceName, backupPolicy); err != nil { - logger.Error(err, "Unable to get backupPolicy for backup.", "BackupPolicy", backupPolicyNameSpaceName) - return podSpec, err - } - // get backup tool backupTool := &dataprotectionv1alpha1.BackupTool{} backupToolNameSpaceName := types.NamespacedName{ Namespace: reqCtx.Req.Namespace, - Name: backupPolicy.Spec.BackupToolName, + Name: backup.Status.BackupToolName, } if err := r.Client.Get(reqCtx.Ctx, backupToolNameSpaceName, backupTool); err != nil { logger.Error(err, "Unable to get backupTool for backup.", "BackupTool", backupToolNameSpaceName) return podSpec, err } + if len(backup.Status.PersistentVolumeClaimName) == 0 { + return podSpec, nil + } + container := corev1.Container{} container.Name = restoreJob.Name container.Command = []string{"sh", "-c"} @@ -272,9 +266,19 @@ func (r *RestoreJobReconciler) buildPodSpec(reqCtx intctrlutil.RequestCtx, resto container.VolumeMounts = restoreJob.Spec.TargetVolumeMounts + // add the volumeMounts with backup volume + restoreVolumeName := fmt.Sprintf("restore-%s", backup.Status.PersistentVolumeClaimName) + remoteVolume := corev1.Volume{ + Name: restoreVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backup.Status.PersistentVolumeClaimName, + }, + }, + } // add remote volumeMounts remoteVolumeMount := corev1.VolumeMount{} - remoteVolumeMount.Name = backupPolicy.Spec.RemoteVolume.Name + remoteVolumeMount.Name = restoreVolumeName remoteVolumeMount.MountPath = "/data" container.VolumeMounts = append(container.VolumeMounts, remoteVolumeMount) @@ -299,7 +303,7 @@ func (r *RestoreJobReconciler) buildPodSpec(reqCtx intctrlutil.RequestCtx, resto podSpec.Volumes = restoreJob.Spec.TargetVolumes // add remote volumes - podSpec.Volumes = append(podSpec.Volumes, backupPolicy.Spec.RemoteVolume) + podSpec.Volumes = append(podSpec.Volumes, remoteVolume) // TODO(dsj): mount readonly remote volumes for restore. // podSpec.Volumes[0].PersistentVolumeClaim.ReadOnly = true @@ -320,9 +324,9 @@ func (r *RestoreJobReconciler) patchTargetCluster(reqCtx intctrlutil.RequestCtx, clusterItemsLen := len(clusterTarget.Items) if clusterItemsLen != 1 { if clusterItemsLen <= 0 { - restoreJob.Status.FailureReason = "Can not found any stateful sets by labelsSelector." + restoreJob.Status.FailureReason = "Can not find any statefulsets with labelsSelector." } else { - restoreJob.Status.FailureReason = "Match labels result more than one, check labelsSelector." + restoreJob.Status.FailureReason = "Match more than one results, please check the labelsSelector." } restoreJob.Status.Phase = dataprotectionv1alpha1.RestoreJobFailed reqCtx.Log.Info(restoreJob.Status.FailureReason) diff --git a/controllers/dataprotection/restorejob_controller_test.go b/controllers/dataprotection/restorejob_controller_test.go index 35117828b..10693e23d 100644 --- a/controllers/dataprotection/restorejob_controller_test.go +++ b/controllers/dataprotection/restorejob_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection @@ -28,14 +31,18 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) var _ = Describe("RestoreJob Controller", func() { - + const ( + clusterName = "mycluster" + compName = "cluster" + ) cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -46,6 +53,7 @@ var _ = Describe("RestoreJob Controller", func() { ml := client.HasLabels{testCtx.TestObjLabelKey} // namespaced testapps.ClearResources(&testCtx, intctrlutil.StatefulSetSignature, inNS, ml) + testapps.ClearResources(&testCtx, intctrlutil.PodSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.RestoreJobSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.BackupSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.BackupPolicySignature, inNS, ml) @@ -74,8 +82,7 @@ var _ = Describe("RestoreJob Controller", func() { By("By assure an backup obj") return testapps.NewBackupFactory(testCtx.DefaultNamespace, "backup-job-"). WithRandomName().SetBackupPolicyName(backupPolicy). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). - SetTTL("168h0m0s"). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() } @@ -83,12 +90,13 @@ var _ = Describe("RestoreJob Controller", func() { By("By assure an backupPolicy obj") return testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, "backup-policy-"). WithRandomName(). - SetSchedule("0 3 * * *"). - SetTTL("168h0m0s"). + AddFullPolicy(). + AddMatchLabels(constant.AppInstanceLabelKey, clusterName). + SetSchedule("0 3 * * *", true). + SetTTL("7d"). SetBackupToolName(backupTool). - SetBackupPolicyTplName("backup-config-mysql"). SetTargetSecretName("mycluster-cluster-secret"). - SetRemoteVolumePVC("backup-remote-volume", "backup-host-path-pvc"). + SetPVC("backup-host-path-pvc"). Create(&testCtx).GetObject() } @@ -110,8 +118,9 @@ var _ = Describe("RestoreJob Controller", func() { assureStatefulSetObj := func() *appsv1.StatefulSet { By("By assure an stateful obj") - return testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, "mycluster", "mycluster", "replicasets"). - AddAppInstanceLabel("mycluster"). + return testapps.NewStatefulSetFactory(testCtx.DefaultNamespace, clusterName, clusterName, compName). + SetReplicas(3). + AddAppInstanceLabel(clusterName). AddContainer(corev1.Container{Name: "mysql", Image: testapps.ApeCloudMySQLImage}). AddVolumeClaimTemplate(corev1.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{Name: testapps.DataVolumeName}, @@ -120,97 +129,71 @@ var _ = Describe("RestoreJob Controller", func() { } patchBackupStatus := func(phase dataprotectionv1alpha1.BackupPhase, key types.NamespacedName) { - backup := dataprotectionv1alpha1.Backup{} - Eventually(func() error { - return k8sClient.Get(ctx, key, &backup) - }).Should(Succeed()) - Expect(k8sClient.Get(ctx, key, &backup)).Should(Succeed()) - - patch := client.MergeFrom(backup.DeepCopy()) - backup.Status.Phase = phase - Expect(k8sClient.Status().Patch(ctx, &backup, patch)).Should(Succeed()) + Eventually(testapps.GetAndChangeObjStatus(&testCtx, key, func(backup *dataprotectionv1alpha1.Backup) { + backup.Status.Phase = phase + })).Should(Succeed()) } patchK8sJobStatus := func(jobStatus batchv1.JobConditionType, key types.NamespacedName) { - k8sJob := batchv1.Job{} - Eventually(func() error { - return k8sClient.Get(ctx, key, &k8sJob) - }).Should(Succeed()) - Expect(k8sClient.Get(ctx, key, &k8sJob)).Should(Succeed()) - - patch := client.MergeFrom(k8sJob.DeepCopy()) - jobCondition := batchv1.JobCondition{Type: jobStatus} - k8sJob.Status.Conditions = append(k8sJob.Status.Conditions, jobCondition) - Expect(k8sClient.Status().Patch(ctx, &k8sJob, patch)).Should(Succeed()) + Eventually(testapps.GetAndChangeObjStatus(&testCtx, key, func(job *batchv1.Job) { + found := false + for _, cond := range job.Status.Conditions { + if cond.Type == jobStatus { + found = true + } + } + if !found { + jobCondition := batchv1.JobCondition{Type: jobStatus} + job.Status.Conditions = append(job.Status.Conditions, jobCondition) + } + })).Should(Succeed()) + } + + testRestoreJob := func(withResources ...bool) { + By("By creating a statefulset and pod") + sts := assureStatefulSetObj() + testapps.MockConsensusComponentPods(&testCtx, sts, clusterName, compName) + + By("By creating a backupTool") + backupTool := assureBackupToolObj(withResources...) + + By("By creating a backupPolicy from backupTool: " + backupTool.Name) + backupPolicy := assureBackupPolicyObj(backupTool.Name) + + By("By creating a backup from backupPolicy: " + backupPolicy.Name) + backup := assureBackupObj(backupPolicy.Name) + + By("By creating a restoreJob from backup: " + backup.Name) + toCreate := assureRestoreJobObj(backup.Name) + key := types.NamespacedName{ + Name: toCreate.Name, + Namespace: toCreate.Namespace, + } + backupKey := types.NamespacedName{Name: backup.Name, Namespace: backup.Namespace} + Eventually(testapps.CheckObj(&testCtx, backupKey, func(g Gomega, fetched *dataprotectionv1alpha1.Backup) { + g.Expect(fetched.Status.Phase).To(Equal(dataprotectionv1alpha1.BackupInProgress)) + })).Should(Succeed()) + + patchBackupStatus(dataprotectionv1alpha1.BackupCompleted, backupKey) + + patchK8sJobStatus(batchv1.JobComplete, key) + + result := &dataprotectionv1alpha1.RestoreJob{} + Eventually(func() bool { + Expect(k8sClient.Get(ctx, key, result)).Should(Succeed()) + return result.Status.Phase == dataprotectionv1alpha1.RestoreJobCompleted || + result.Status.Phase == dataprotectionv1alpha1.RestoreJobFailed + }).Should(BeTrue()) + Expect(result.Status.Phase).Should(Equal(dataprotectionv1alpha1.RestoreJobCompleted)) } Context("When creating restoreJob", func() { It("Should success with no error", func() { - - By("By creating a statefulset") - _ = assureStatefulSetObj() - - By("By creating a backupTool") - backupTool := assureBackupToolObj() - - By("By creating a backupPolicy from backupTool: " + backupTool.Name) - backupPolicy := assureBackupPolicyObj(backupTool.Name) - - By("By creating a backup from backupPolicy: " + backupPolicy.Name) - backup := assureBackupObj(backupPolicy.Name) - - By("By creating a restoreJob from backup: " + backup.Name) - toCreate := assureRestoreJobObj(backup.Name) - key := types.NamespacedName{ - Name: toCreate.Name, - Namespace: toCreate.Namespace, - } - - patchBackupStatus(dataprotectionv1alpha1.BackupCompleted, types.NamespacedName{Name: backup.Name, Namespace: backup.Namespace}) - - patchK8sJobStatus(batchv1.JobComplete, types.NamespacedName{Name: toCreate.Name, Namespace: toCreate.Namespace}) - - result := &dataprotectionv1alpha1.RestoreJob{} - Eventually(func() bool { - Expect(k8sClient.Get(ctx, key, result)).Should(Succeed()) - return result.Status.Phase == dataprotectionv1alpha1.RestoreJobCompleted || - result.Status.Phase == dataprotectionv1alpha1.RestoreJobFailed - }).Should(BeTrue()) - Expect(result.Status.Phase).Should(Equal(dataprotectionv1alpha1.RestoreJobCompleted)) + testRestoreJob() }) It("Without backupTool resources should success with no error", func() { - - By("By creating a statefulset") - _ = assureStatefulSetObj() - - By("By creating a backupTool") - backupTool := assureBackupToolObj(true) - - By("By creating a backupPolicy from backupTool: " + backupTool.Name) - backupPolicy := assureBackupPolicyObj(backupTool.Name) - - By("By creating a backup from backupPolicy: " + backupPolicy.Name) - backup := assureBackupObj(backupPolicy.Name) - - By("By creating a restoreJob from backup: " + backup.Name) - toCreate := assureRestoreJobObj(backup.Name) - key := types.NamespacedName{ - Name: toCreate.Name, - Namespace: toCreate.Namespace, - } - - patchBackupStatus(dataprotectionv1alpha1.BackupCompleted, types.NamespacedName{Name: backup.Name, Namespace: backup.Namespace}) - - patchK8sJobStatus(batchv1.JobComplete, types.NamespacedName{Name: toCreate.Name, Namespace: toCreate.Namespace}) - - result := &dataprotectionv1alpha1.RestoreJob{} - Eventually(func() bool { - Expect(k8sClient.Get(ctx, key, result)).Should(Succeed()) - return result.Status.Phase == dataprotectionv1alpha1.RestoreJobCompleted || - result.Status.Phase == dataprotectionv1alpha1.RestoreJobFailed - }).Should(BeTrue()) - Expect(result.Status.Phase).Should(Equal(dataprotectionv1alpha1.RestoreJobCompleted)) + testRestoreJob(true) }) }) diff --git a/controllers/dataprotection/suite_test.go b/controllers/dataprotection/suite_test.go index 6358e66f4..4fe971e78 100644 --- a/controllers/dataprotection/suite_test.go +++ b/controllers/dataprotection/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection @@ -27,9 +30,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/spf13/viper" "go.uber.org/zap/zapcore" + batchv1 "k8s.io/api/batch/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/client-go/rest" ctrl "sigs.k8s.io/controller-runtime" @@ -100,6 +105,9 @@ var _ = BeforeSuite(func() { err = snapshotv1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) + err = snapshotv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) + err = appsv1alpha1.AddToScheme(scheme) Expect(err).NotTo(HaveOccurred()) @@ -113,11 +121,13 @@ var _ = BeforeSuite(func() { Expect(k8sClient).NotTo(BeNil()) uncachedObjects := []client.Object{ - &dataprotectionv1alpha1.BackupPolicyTemplate{}, &dataprotectionv1alpha1.BackupPolicy{}, &dataprotectionv1alpha1.BackupTool{}, &dataprotectionv1alpha1.Backup{}, &dataprotectionv1alpha1.RestoreJob{}, + &snapshotv1.VolumeSnapshot{}, + &snapshotv1beta1.VolumeSnapshot{}, + &batchv1.Job{}, } // run reconcile k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ diff --git a/controllers/dataprotection/type.go b/controllers/dataprotection/type.go index 0d8c1840a..26d4ed014 100644 --- a/controllers/dataprotection/type.go +++ b/controllers/dataprotection/type.go @@ -1,26 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection import ( + "embed" "runtime" "time" "github.com/spf13/viper" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) const ( @@ -30,14 +35,19 @@ const ( maxConcurDataProtectionReconKey = "MAXCONCURRENTRECONCILES_DATAPROTECTION" // label keys + dataProtectionLabelBackupPolicyKey = "dataprotection.kubeblocks.io/backup-policy" dataProtectionLabelBackupTypeKey = "dataprotection.kubeblocks.io/backup-type" dataProtectionLabelAutoBackupKey = "dataprotection.kubeblocks.io/autobackup" dataProtectionLabelBackupNameKey = "backups.dataprotection.kubeblocks.io/name" dataProtectionLabelRestoreJobNameKey = "restorejobs.dataprotection.kubeblocks.io/name" - dataProtectionBackupTargetPodKey = "dataprotection.kubeblocks.io/target-pod-name" - // error status - errorJobFailed = "JobFailed" + dataProtectionBackupTargetPodKey = "dataprotection.kubeblocks.io/target-pod-name" + dataProtectionAnnotationCreateByPolicyKey = "dataprotection.kubeblocks.io/created-by-policy" + + // the key of persistentVolumeTemplate in the configmap. + persistentVolumeTemplateKey = "persistentVolume" + + hostNameLabelKey = "kubernetes.io/hostname" ) var reconcileInterval = time.Second @@ -45,3 +55,21 @@ var reconcileInterval = time.Second func init() { viper.SetDefault(maxConcurDataProtectionReconKey, runtime.NumCPU()*2) } + +var ( + //go:embed cue/* + cueTemplates embed.FS +) + +type backupPolicyOptions struct { + Name string `json:"name"` + BackupPolicyName string `json:"backupPolicyName"` + Namespace string `json:"namespace"` + MgrNamespace string `json:"mgrNamespace"` + Cluster string `json:"cluster"` + Schedule string `json:"schedule"` + BackupType string `json:"backupType"` + TTL metav1.Duration `json:"ttl,omitempty"` + ServiceAccount string `json:"serviceAccount"` + Image string `json:"image"` +} diff --git a/controllers/dataprotection/utils.go b/controllers/dataprotection/utils.go index f0c2cdbd6..ae72edbd8 100644 --- a/controllers/dataprotection/utils.go +++ b/controllers/dataprotection/utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dataprotection @@ -23,13 +26,13 @@ import ( // byBackupStartTime sorts a list of jobs by start timestamp, using their names as a tie breaker. type byBackupStartTime []dataprotectionv1alpha1.Backup -// Len return the length of byBackupStartTime, for the sort.Sort +// Len returns the length of byBackupStartTime, for the sort.Sort func (o byBackupStartTime) Len() int { return len(o) } // Swap the items, for the sort.Sort func (o byBackupStartTime) Swap(i, j int) { o[i], o[j] = o[j], o[i] } -// Less define how to compare items, for the sort.Sort +// Less defines how to compare items, for the sort.Sort func (o byBackupStartTime) Less(i, j int) bool { if o[i].Status.StartTimestamp == nil && o[j].Status.StartTimestamp != nil { return false diff --git a/controllers/extensions/addon_controller.go b/controllers/extensions/addon_controller.go index 62552e7d4..3c4461f9e 100644 --- a/controllers/extensions/addon_controller.go +++ b/controllers/extensions/addon_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package extensions @@ -25,12 +28,16 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/rest" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" + "sigs.k8s.io/controller-runtime/pkg/handler" "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/internal/constant" @@ -56,7 +63,8 @@ func init() { // +kubebuilder:rbac:groups=extensions.kubeblocks.io,resources=addons/finalizers,verbs=update // +kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete;deletecollection -// +kubebuilder:rbac:groups=core,resources=pods,verbs=delete;deletecollection +// +kubebuilder:rbac:groups=core,resources=pods,verbs=get;list;delete;deletecollection +// +kubebuilder:rbac:groups=core,resources=pods/log,verbs=get;list // +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims,verbs=get;update;patch;watch // +kubebuilder:rbac:groups=core,resources=persistentvolumeclaims/status,verbs=get @@ -106,6 +114,10 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl return ctrlerihandler.NewTypeHandler(&autoInstallCheckStage{stageCtx: buildStageCtx(next...)}) } + enabledAutoValuesStageBuilder := func(next ...ctrlerihandler.Handler) ctrlerihandler.Handler { + return ctrlerihandler.NewTypeHandler(&enabledWithDefaultValuesStage{stageCtx: buildStageCtx(next...)}) + } + progressingStageBuilder := func(next ...ctrlerihandler.Handler) ctrlerihandler.Handler { return ctrlerihandler.NewTypeHandler(&progressingHandler{stageCtx: buildStageCtx(next...)}) } @@ -119,6 +131,7 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl genIDProceedStageBuilder, installableCheckStageBuilder, autoInstallCheckStageBuilder, + enabledAutoValuesStageBuilder, progressingStageBuilder, terminalStateStageBuilder, ).Handler("") @@ -140,21 +153,30 @@ func (r *AddonReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl func (r *AddonReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&extensionsv1alpha1.Addon{}). - // TODO: replace with controller-idioms's adopt lib - // Watches(&source.Kind{Type: &batchv1.Job{}}, - // &handler.EnqueueRequestForObject{}, - // builder.WithPredicates(&jobCompletitionPredicate{reconciler: r, Log: log.FromContext(context.TODO())})). + Watches(&source.Kind{Type: &batchv1.Job{}}, handler.EnqueueRequestsFromMapFunc(r.findAddonJobs)). WithOptions(controller.Options{ MaxConcurrentReconciles: viper.GetInt(maxConcurrentReconcilesKey), }). Complete(r) } -// type jobCompletitionPredicate struct { -// predicate.Funcs -// reconciler *AddonReconciler -// Log logr.Logger -// } +func (r *AddonReconciler) findAddonJobs(job client.Object) []reconcile.Request { + labels := job.GetLabels() + if _, ok := labels[constant.AddonNameLabelKey]; !ok { + return []reconcile.Request{} + } + if v, ok := labels[constant.AppManagedByLabelKey]; !ok || v != constant.AppName { + return []reconcile.Request{} + } + return []reconcile.Request{ + { + NamespacedName: types.NamespacedName{ + Namespace: job.GetNamespace(), + Name: job.GetName(), + }, + }, + } +} func (r *AddonReconciler) cleanupJobPods(reqCtx intctrlutil.RequestCtx) error { if err := r.DeleteAllOf(reqCtx.Ctx, &corev1.Pod{}, @@ -170,7 +192,7 @@ func (r *AddonReconciler) cleanupJobPods(reqCtx intctrlutil.RequestCtx) error { } func (r *AddonReconciler) deleteExternalResources(reqCtx intctrlutil.RequestCtx, addon *extensionsv1alpha1.Addon) (*ctrl.Result, error) { - if addon.Annotations != nil && addon.Annotations[NoDeleteJobs] == "true" { + if addon.Annotations != nil && addon.Annotations[NoDeleteJobs] == trueVal { return nil, nil } deleteJobIfExist := func(jobName string) error { diff --git a/controllers/extensions/addon_controller_stages.go b/controllers/extensions/addon_controller_stages.go index bace5231c..33b18bcbe 100644 --- a/controllers/extensions/addon_controller_stages.go +++ b/controllers/extensions/addon_controller_stages.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package extensions @@ -25,6 +28,7 @@ import ( ctrlerihandler "github.com/authzed/controller-idioms/handler" "github.com/spf13/viper" + "golang.org/x/exp/slices" batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" storagev1 "k8s.io/api/storage/v1" @@ -32,6 +36,7 @@ import ( "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1client "k8s.io/client-go/kubernetes/typed/core/v1" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" @@ -50,10 +55,17 @@ const ( resultValueKey = "result" errorValueKey = "err" operandValueKey = "operand" + trueVal = "true" ) func init() { viper.SetDefault(addonSANameKey, "kubeblocks-addon-installer") + viper.SetDefault(addonHelmInstallOptKey, []string{ + "--atomic", + "--cleanup-on-fail", + "--wait", + }) + viper.SetDefault(addonHelmUninstallOptKey, []string{}) } func (r *stageCtx) setReconciled() { @@ -62,10 +74,15 @@ func (r *stageCtx) setReconciled() { } func (r *stageCtx) setRequeueAfter(duration time.Duration, msg string) { - res, err := intctrlutil.RequeueAfter(time.Second, r.reqCtx.Log, msg) + res, err := intctrlutil.RequeueAfter(duration, r.reqCtx.Log, msg) r.updateResultNErr(&res, err) } +// func (r *stageCtx) setRequeue(msg string) { +// res, err := intctrlutil.Requeue(r.reqCtx.Log, msg) +// r.updateResultNErr(&res, err) +// } + func (r *stageCtx) setRequeueWithErr(err error, msg string) { res, err := intctrlutil.CheckedRequeueWithError(err, r.reqCtx.Log, msg) r.updateResultNErr(&res, err) @@ -113,6 +130,10 @@ type autoInstallCheckStage struct { stageCtx } +type enabledWithDefaultValuesStage struct { + stageCtx +} + type progressingHandler struct { stageCtx enablingStage enablingStage @@ -254,7 +275,7 @@ func (r *installableCheckStage) Handle(ctx context.Context) { if addon.Spec.InstallSpec != nil { return } - if addon.Annotations != nil && addon.Annotations[SkipInstallableCheck] == "true" { + if addon.Annotations != nil && addon.Annotations[SkipInstallableCheck] == trueVal { r.reconciler.Event(addon, "Warning", InstallableCheckSkipped, "Installable check skipped.") return @@ -274,7 +295,7 @@ func (r *installableCheckStage) Handle(ctx context.Context) { Type: extensionsv1alpha1.ConditionTypeChecked, Status: metav1.ConditionFalse, ObservedGeneration: addon.Generation, - Reason: AddonSpecInstallableReqUnmatched, + Reason: InstallableRequirementUnmatched, Message: "spec.installable.selectors has no matching requirement.", LastTransitionTime: metav1.Now(), }) @@ -303,32 +324,22 @@ func (r *autoInstallCheckStage) Handle(ctx context.Context) { r.reqCtx.Log.V(1).Info("has specified addon.spec.installSpec") return } + enabledAddonWithDefaultValues(ctx, &r.stageCtx, addon, AddonAutoInstall, "Addon enabled auto-install") + }) + r.next.Handle(ctx) +} - setInstallSpec := func(di *extensionsv1alpha1.AddonDefaultInstallSpecItem) { - addon.Spec.InstallSpec = di.AddonInstallSpec.DeepCopy() - addon.Spec.InstallSpec.Enabled = true - if err := r.reconciler.Client.Update(ctx, addon); err != nil { - r.setRequeueWithErr(err, "") - return - } - r.reconciler.Event(addon, "Normal", AddonAutoInstall, - "Addon enabled auto-install") - r.setReconciled() +func (r *enabledWithDefaultValuesStage) Handle(ctx context.Context) { + r.process(func(addon *extensionsv1alpha1.Addon) { + r.reqCtx.Log.V(1).Info("enabledWithDefaultValuesStage", "phase", addon.Status.Phase) + if addon.Spec.InstallSpec.HasSetValues() || addon.Spec.InstallSpec.IsDisabled() { + r.reqCtx.Log.V(1).Info("has specified addon.spec.installSpec") + return } - - for _, di := range addon.Spec.GetSortedDefaultInstallValues() { - if len(di.Selectors) == 0 { - setInstallSpec(&di) - return - } - for _, s := range di.Selectors { - if !s.MatchesFromConfig() { - continue - } - setInstallSpec(&di) - return - } + if v, ok := addon.Annotations[AddonDefaultIsEmpty]; ok && v == trueVal { + return } + enabledAddonWithDefaultValues(ctx, &r.stageCtx, addon, AddonSetDefaultValues, "Addon enabled with default values") }) r.next.Handle(ctx) } @@ -508,25 +519,16 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { // info. from conditions. if helmInstallJob.Status.Failed > 0 { // job failed set terminal state phase - patch := client.MergeFrom(addon.DeepCopy()) - addon.Status.ObservedGeneration = addon.Generation - addon.Status.Phase = extensionsv1alpha1.AddonFailed - meta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ - Type: extensionsv1alpha1.ConditionTypeFailed, - Status: metav1.ConditionFalse, - ObservedGeneration: addon.Generation, - Reason: AddonSpecInstallFailed, - Message: "installation failed", - LastTransitionTime: metav1.Now(), - }) - - if err := r.reconciler.Status().Patch(ctx, addon, patch); err != nil { - r.setRequeueWithErr(err, "") - return - } - r.reconciler.Event(addon, "Warning", InstallationFailed, + setAddonErrorConditions(ctx, &r.stageCtx, addon, true, true, InstallationFailed, fmt.Sprintf("Installation failed, do inspect error from jobs.batch %s", key.String())) - r.setReconciled() + // only allow to do pod logs if max concurrent reconciles > 1, also considered that helm + // cmd error only has limited contents + if viper.GetInt(maxConcurrentReconcilesKey) > 1 { + if err := logFailedJobPodToCondError(ctx, &r.stageCtx, addon, key.Name, InstallationFailedLogs); err != nil { + r.setRequeueWithErr(err, "") + return + } + } return } r.setRequeueAfter(time.Second, "") @@ -542,49 +544,44 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { helmInstallJob.ObjectMeta.Namespace = key.Namespace helmJobPodSpec := &helmInstallJob.Spec.Template.Spec helmContainer := &helmInstallJob.Spec.Template.Spec.Containers[0] - helmContainer.Args = []string{ + helmContainer.Args = append([]string{ "upgrade", "--install", "$(RELEASE_NAME)", "$(CHART)", "--namespace", "$(RELEASE_NS)", - "--timeout", - "10m", "--create-namespace", - "--atomic", - "--cleanup-on-fail", - "--wait", - } - - // add extra helm install option flags - for k, v := range addon.Spec.Helm.InstallOptions { - helmContainer.Args = append(helmContainer.Args, fmt.Sprintf("--%s", k)) - if v != "" { - helmContainer.Args = append(helmContainer.Args, v) - } - } + }, viper.GetStringSlice(addonHelmInstallOptKey)...) installValues := addon.Spec.Helm.BuildMergedValues(addon.Spec.InstallSpec) - // set values from URL - for _, urlValue := range installValues.URLs { - helmContainer.Args = append(helmContainer.Args, "--values", urlValue) + if err = addon.Spec.Helm.BuildContainerArgs(helmContainer, installValues); err != nil { + r.setRequeueWithErr(err, "") + return } // set values from file for _, cmRef := range installValues.ConfigMapRefs { cm := &corev1.ConfigMap{} - if err := r.reconciler.Get(ctx, client.ObjectKey{ + key := client.ObjectKey{ Name: cmRef.Name, - Namespace: mgrNS}, cm); err != nil { + Namespace: mgrNS} + if err := r.reconciler.Get(ctx, key, cm); err != nil { if !apierrors.IsNotFound(err) { r.setRequeueWithErr(err, "") return } r.setRequeueAfter(time.Second, fmt.Sprintf("ConfigMap %s not found", cmRef.Name)) + setAddonErrorConditions(ctx, &r.stageCtx, addon, false, true, AddonRefObjError, + fmt.Sprintf("ConfigMap object %v not found", key)) + return + } + if !findDataKey(cm.Data, cmRef) { + setAddonErrorConditions(ctx, &r.stageCtx, addon, true, true, AddonRefObjError, + fmt.Sprintf("Attach ConfigMap %v volume source failed, key %s not found", key, cmRef.Key)) + r.setReconciled() return } - // TODO: validate cmRef.key exist in cm attachVolumeMount(helmJobPodSpec, cmRef, cm.Name, "cm", func() corev1.VolumeSource { return corev1.VolumeSource{ @@ -605,18 +602,25 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { for _, secretRef := range installValues.SecretRefs { secret := &corev1.Secret{} - if err := r.reconciler.Get(ctx, client.ObjectKey{ + key := client.ObjectKey{ Name: secretRef.Name, - Namespace: mgrNS}, secret); err != nil { + Namespace: mgrNS} + if err := r.reconciler.Get(ctx, key, secret); err != nil { if !apierrors.IsNotFound(err) { r.setRequeueWithErr(err, "") return } r.setRequeueAfter(time.Second, fmt.Sprintf("Secret %s not found", secret.Name)) + setAddonErrorConditions(ctx, &r.stageCtx, addon, false, true, AddonRefObjError, + fmt.Sprintf("Secret object %v not found", key)) + return + } + if !findDataKey(secret.Data, secretRef) { + setAddonErrorConditions(ctx, &r.stageCtx, addon, true, true, AddonRefObjError, + fmt.Sprintf("Attach Secret %v volume source failed, key %s not found", key, secretRef.Key)) + r.setReconciled() return } - // TODO: validate secretRef.key exist in secret - attachVolumeMount(helmJobPodSpec, secretRef, secret.Name, "secret", func() corev1.VolumeSource { return corev1.VolumeSource{ @@ -633,17 +637,6 @@ func (r *helmTypeInstallStage) Handle(ctx context.Context) { }) } - // set key1=val1,key2=val2 value - if len(installValues.SetValues) > 0 { - helmContainer.Args = append(helmContainer.Args, "--set", - strings.Join(installValues.SetValues, ",")) - } - - // set key1=jsonval1,key2=jsonval2 JSON value, applied multiple - for _, v := range installValues.SetJSONValues { - helmContainer.Args = append(helmContainer.Args, "--set-json", v) - } - if err := r.reconciler.Create(ctx, helmInstallJob); err != nil { r.setRequeueWithErr(err, "") return @@ -674,12 +667,12 @@ func (r *helmTypeUninstallStage) Handle(ctx context.Context) { } // Job controller has yet handling Job or job controller is not running, i.e., testenv - // only handle this situation when addon is at terminating state. + // only handles this situation when addon is at terminating state. if helmUninstallJob.Status.StartTime.IsZero() && !addon.GetDeletionTimestamp().IsZero() { return } - // requeue if uninstall job is active or deleting + // requeue if uninstall job is active or under deleting if !helmUninstallJob.GetDeletionTimestamp().IsZero() || helmUninstallJob.Status.Active > 0 { r.setRequeueAfter(time.Second, "") return @@ -692,6 +685,14 @@ func (r *helmTypeUninstallStage) Handle(ctx context.Context) { r.reconciler.Event(addon, "Warning", UninstallationFailed, fmt.Sprintf("Uninstallation failed, do inspect error from jobs.batch %s", key.String())) + // only allow to do pod logs if max concurrent reconciles > 1, also considered that helm + // cmd error only has limited contents + if viper.GetInt(maxConcurrentReconcilesKey) > 1 { + if err := logFailedJobPodToCondError(ctx, &r.stageCtx, addon, key.Name, UninstallationFailedLogs); err != nil { + r.setRequeueWithErr(err, "") + return + } + } if err := r.reconciler.Delete(ctx, helmUninstallJob); client.IgnoreNotFound(err) != nil { r.setRequeueWithErr(err, "") @@ -740,14 +741,12 @@ func (r *helmTypeUninstallStage) Handle(ctx context.Context) { } helmUninstallJob.ObjectMeta.Name = key.Name helmUninstallJob.ObjectMeta.Namespace = key.Namespace - helmUninstallJob.Spec.Template.Spec.Containers[0].Args = []string{ + helmUninstallJob.Spec.Template.Spec.Containers[0].Args = append([]string{ "delete", "$(RELEASE_NAME)", "--namespace", "$(RELEASE_NS)", - "--timeout", - "10m", - } + }, viper.GetStringSlice(addonHelmUninstallOptKey)...) r.reqCtx.Log.V(1).Info("create helm uninstall job", "job", key) if err := r.reconciler.Create(ctx, helmUninstallJob); err != nil { r.reqCtx.Log.V(1).Info("helmTypeUninstallStage", "job", key, "err", err) @@ -824,7 +823,7 @@ func (r *terminalStateStage) Handle(ctx context.Context) { r.next.Handle(ctx) } -// attachVolumeMount attach a volumes to pod and added container.VolumeMounts to a ConfigMap +// attachVolumeMount attaches a volumes to pod and added container.VolumeMounts to a ConfigMap // or Secret referenced key as file, and add --values={volumeMountPath}/{selector.Key} to // helm install/upgrade args func attachVolumeMount( @@ -849,7 +848,7 @@ func attachVolumeMount( fmt.Sprintf("%s/%s", mountPath, selector.Key)) } -// createHelmJobProto create a job.batch prototyped object +// createHelmJobProto creates a job.batch prototyped object func createHelmJobProto(addon *extensionsv1alpha1.Addon) (*batchv1.Job, error) { ttl := time.Minute * 5 if jobTTL := viper.GetString(constant.CfgKeyAddonJobTTL); jobTTL != "" { @@ -859,6 +858,7 @@ func createHelmJobProto(addon *extensionsv1alpha1.Addon) (*batchv1.Job, error) { } } ttlSec := int32(ttl.Seconds()) + backoffLimit := int32(3) helmProtoJob := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{ @@ -867,6 +867,7 @@ func createHelmJobProto(addon *extensionsv1alpha1.Addon) (*batchv1.Job, error) { }, }, Spec: batchv1.JobSpec{ + BackoffLimit: &backoffLimit, TTLSecondsAfterFinished: &ttlSec, Template: corev1.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ @@ -876,11 +877,11 @@ func createHelmJobProto(addon *extensionsv1alpha1.Addon) (*batchv1.Job, error) { }, }, Spec: corev1.PodSpec{ - RestartPolicy: corev1.RestartPolicyOnFailure, + RestartPolicy: corev1.RestartPolicyNever, ServiceAccountName: viper.GetString("KUBEBLOCKS_ADDON_SA_NAME"), Containers: []corev1.Container{ { - Name: strings.ToLower(string(addon.Spec.Type)), + Name: getJobMainContainerName(addon), Image: viper.GetString(constant.KBToolsImage), ImagePullPolicy: corev1.PullPolicy(viper.GetString(constant.CfgAddonJobImgPullPolicy)), // TODO: need have image that is capable of following settings, current settings @@ -960,3 +961,126 @@ func createHelmJobProto(addon *extensionsv1alpha1.Addon) (*batchv1.Job, error) { } return helmProtoJob, nil } + +func enabledAddonWithDefaultValues(ctx context.Context, stageCtx *stageCtx, + addon *extensionsv1alpha1.Addon, reason, message string) { + setInstallSpec := func(di *extensionsv1alpha1.AddonDefaultInstallSpecItem) { + addon.Spec.InstallSpec = di.AddonInstallSpec.DeepCopy() + addon.Spec.InstallSpec.Enabled = true + if addon.Annotations == nil { + addon.Annotations = map[string]string{} + } + if di.AddonInstallSpec.IsEmpty() { + addon.Annotations[AddonDefaultIsEmpty] = trueVal + } + if err := stageCtx.reconciler.Client.Update(ctx, addon); err != nil { + stageCtx.setRequeueWithErr(err, "") + return + } + stageCtx.reconciler.Event(addon, "Normal", reason, message) + stageCtx.setReconciled() + } + + for _, di := range addon.Spec.GetSortedDefaultInstallValues() { + if len(di.Selectors) == 0 { + setInstallSpec(&di) + return + } + for _, s := range di.Selectors { + if !s.MatchesFromConfig() { + continue + } + setInstallSpec(&di) + return + } + } +} + +func setAddonErrorConditions(ctx context.Context, + stageCtx *stageCtx, + addon *extensionsv1alpha1.Addon, + setFailedStatus, recordEvent bool, + reason, message string, + eventMessage ...string) { + patch := client.MergeFrom(addon.DeepCopy()) + addon.Status.ObservedGeneration = addon.Generation + if setFailedStatus { + addon.Status.Phase = extensionsv1alpha1.AddonFailed + } + meta.SetStatusCondition(&addon.Status.Conditions, metav1.Condition{ + Type: extensionsv1alpha1.ConditionTypeChecked, + Status: metav1.ConditionFalse, + ObservedGeneration: addon.Generation, + Reason: reason, + Message: message, + LastTransitionTime: metav1.Now(), + }) + + if err := stageCtx.reconciler.Status().Patch(ctx, addon, patch); err != nil { + stageCtx.setRequeueWithErr(err, "") + return + } + if !recordEvent { + return + } + if len(eventMessage) > 0 && eventMessage[0] != "" { + stageCtx.reconciler.Event(addon, "Warning", reason, eventMessage[0]) + } else { + stageCtx.reconciler.Event(addon, "Warning", reason, message) + } +} + +func getJobMainContainerName(addon *extensionsv1alpha1.Addon) string { + return strings.ToLower(string(addon.Spec.Type)) +} + +func logFailedJobPodToCondError(ctx context.Context, stageCtx *stageCtx, addon *extensionsv1alpha1.Addon, + jobName, reason string) error { + podList := &corev1.PodList{} + if err := stageCtx.reconciler.List(ctx, podList, + client.InNamespace(viper.GetString(constant.CfgKeyCtrlrMgrNS)), + client.MatchingLabels{ + constant.AddonNameLabelKey: stageCtx.reqCtx.Req.Name, + constant.AppManagedByLabelKey: constant.AppName, + "job-name": jobName, + }); err != nil { + return err + } + + // sort pod with latest creation place front + slices.SortFunc(podList.Items, func(a, b corev1.Pod) bool { + return b.CreationTimestamp.Before(&(a.CreationTimestamp)) + }) + +podsloop: + for _, pod := range podList.Items { + switch pod.Status.Phase { + case corev1.PodFailed: + clientset, err := corev1client.NewForConfig(stageCtx.reconciler.RestConfig) + if err != nil { + return err + } + currOpts := &corev1.PodLogOptions{ + Container: getJobMainContainerName(addon), + } + req := clientset.Pods(pod.Namespace).GetLogs(pod.Name, currOpts) + data, err := req.DoRaw(ctx) + if err != nil { + return err + } + setAddonErrorConditions(ctx, stageCtx, addon, false, true, reason, string(data)) + break podsloop + } + } + return nil +} + +func findDataKey[V string | []byte](data map[string]V, refObj extensionsv1alpha1.DataObjectKeySelector) bool { + for k := range data { + if k != refObj.Key { + continue + } + return true + } + return false +} diff --git a/controllers/extensions/addon_controller_test.go b/controllers/extensions/addon_controller_test.go index 295b0a0d4..04a37a1af 100644 --- a/controllers/extensions/addon_controller_test.go +++ b/controllers/extensions/addon_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package extensions @@ -41,7 +44,7 @@ import ( var _ = Describe("Addon controller", func() { cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -226,7 +229,7 @@ var _ = Describe("Addon controller", func() { Expect(addon.Spec.DefaultInstallValues).ShouldNot(BeEmpty()) } - progressingPhaseCheck := func(genID int, expectPhase extensionsv1alpha1.AddonPhase, handler func()) { + addonStatusPhaseCheck := func(genID int, expectPhase extensionsv1alpha1.AddonPhase, handler func()) { Eventually(func(g Gomega) { _, err := doReconcile() Expect(err).To(Not(HaveOccurred())) @@ -255,7 +258,7 @@ var _ = Describe("Addon controller", func() { } enablingPhaseCheck := func(genID int) { - progressingPhaseCheck(genID, extensionsv1alpha1.AddonEnabling, func() { + addonStatusPhaseCheck(genID, extensionsv1alpha1.AddonEnabling, func() { By("By fake active install job") jobKey := client.ObjectKey{ Namespace: viper.GetString(constant.CfgKeyCtrlrMgrNS), @@ -268,7 +271,7 @@ var _ = Describe("Addon controller", func() { } disablingPhaseCheck := func(genID int) { - progressingPhaseCheck(genID, extensionsv1alpha1.AddonDisabling, nil) + addonStatusPhaseCheck(genID, extensionsv1alpha1.AddonDisabling, nil) } checkAddonDeleted := func(g Gomega) { @@ -302,9 +305,16 @@ var _ = Describe("Addon controller", func() { Expect(testCtx.CreateObj(ctx, helmRelease)).Should(Succeed()) } - It("should successfully reconcile a custom resource for Addon", func() { + It("should successfully reconcile a custom resource for Addon with spec.type=Helm", func() { By("By create an addon") - createAddonSpecWithRequiredAttributes(nil) + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Spec.Type = extensionsv1alpha1.HelmType + newOjb.Spec.Helm = &extensionsv1alpha1.HelmTypeInstallSpec{ + InstallOptions: extensionsv1alpha1.HelmInstallOptions{ + "--debug": "true", + }, + } + }) By("By checking status.observedGeneration and status.phase=disabled") Eventually(func(g Gomega) { @@ -539,7 +549,7 @@ var _ = Describe("Addon controller", func() { // "extensions.kubeblocks.io/skip-installable-check" }) - It("should successfully reconcile a custom resource for Addon with CM and secret values", func() { + It("should successfully reconcile a custom resource for Addon with CM and secret ref values", func() { By("By create an addon with spec.helm.installValues.configMapRefs set") cm := testapps.CreateCustomizedObj(&testCtx, "addon/cm-values.yaml", &corev1.ConfigMap{}, func(newCM *corev1.ConfigMap) { @@ -549,6 +559,8 @@ var _ = Describe("Addon controller", func() { &corev1.Secret{}, func(newSecret *corev1.Secret) { newSecret.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS) }) + + By("By addon enabled via auto-install") createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { newOjb.Spec.Installable.AutoInstall = true for k := range cm.Data { @@ -566,12 +578,47 @@ var _ = Describe("Addon controller", func() { }) } }) - - By("By addon autoInstall auto added") enablingPhaseCheck(2) By("By enabled addon with fake completed install job status") fakeInstallationCompletedJob(2) }) + + It("should failed reconcile a custom resource for Addon with missing CM ref values", func() { + By("By create an addon with spec.helm.installValues.configMapRefs set") + cm := testapps.CreateCustomizedObj(&testCtx, "addon/cm-values.yaml", + &corev1.ConfigMap{}, func(newCM *corev1.ConfigMap) { + newCM.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS) + }) + + By("By addon enabled via auto-install") + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Spec.Installable.AutoInstall = true + newOjb.Spec.Helm.InstallValues.ConfigMapRefs = append(newOjb.Spec.Helm.InstallValues.ConfigMapRefs, + extensionsv1alpha1.DataObjectKeySelector{ + Name: cm.Name, + Key: "unknown", + }) + }) + addonStatusPhaseCheck(2, extensionsv1alpha1.AddonFailed, nil) + }) + + It("should failed reconcile a custom resource for Addon with missing secret ref values", func() { + By("By create an addon with spec.helm.installValues.configMapRefs set") + secret := testapps.CreateCustomizedObj(&testCtx, "addon/secret-values.yaml", + &corev1.Secret{}, func(newSecret *corev1.Secret) { + newSecret.Namespace = viper.GetString(constant.CfgKeyCtrlrMgrNS) + }) + By("By addon enabled via auto-install") + createAddonSpecWithRequiredAttributes(func(newOjb *extensionsv1alpha1.Addon) { + newOjb.Spec.Installable.AutoInstall = true + newOjb.Spec.Helm.InstallValues.SecretRefs = append(newOjb.Spec.Helm.InstallValues.SecretRefs, + extensionsv1alpha1.DataObjectKeySelector{ + Name: secret.Name, + Key: "unknown", + }) + }) + addonStatusPhaseCheck(2, extensionsv1alpha1.AddonFailed, nil) + }) }) }) diff --git a/controllers/extensions/const.go b/controllers/extensions/const.go index e4c56fe27..f04c97629 100644 --- a/controllers/extensions/const.go +++ b/controllers/extensions/const.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package extensions @@ -24,23 +27,28 @@ const ( ControllerPaused = "controller.kubeblocks.io/controller-paused" SkipInstallableCheck = "extensions.kubeblocks.io/skip-installable-check" NoDeleteJobs = "extensions.kubeblocks.io/no-delete-jobs" + AddonDefaultIsEmpty = "addons.extensions.kubeblocks.io/default-is-empty" // condition reasons - AddonDisabled = "AddonDisabled" - AddonEnabled = "AddonEnabled" - AddonSpecInstallFailed = "AddonSpecInstallFailed" - AddonSpecInstallableReqUnmatched = "AddonSpecInstallableRequirementUnmatched" + AddonDisabled = "AddonDisabled" + AddonEnabled = "AddonEnabled" // event reasons InstallableCheckSkipped = "InstallableCheckSkipped" InstallableRequirementUnmatched = "InstallableRequirementUnmatched" AddonAutoInstall = "AddonAutoInstall" + AddonSetDefaultValues = "AddonSetDefaultValues" DisablingAddon = "DisablingAddon" EnablingAddon = "EnablingAddon" InstallationFailed = "InstallationFailed" + InstallationFailedLogs = "InstallationFailedLogs" UninstallationFailed = "UninstallationFailed" + UninstallationFailedLogs = "UninstallationFailedLogs" + AddonRefObjError = "ReferenceObjectError" // config keys used in viper maxConcurrentReconcilesKey = "MAXCONCURRENTRECONCILES_ADDON" addonSANameKey = "KUBEBLOCKS_ADDON_SA_NAME" + addonHelmInstallOptKey = "KUBEBLOCKS_ADDON_HELM_INSTALL_OPTIONS" + addonHelmUninstallOptKey = "KUBEBLOCKS_ADDON_HELM_UNINSTALL_OPTIONS" ) diff --git a/controllers/extensions/suite_test.go b/controllers/extensions/suite_test.go index 1d3f78d25..b6fb96ae2 100644 --- a/controllers/extensions/suite_test.go +++ b/controllers/extensions/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package extensions diff --git a/controllers/k8score/const.go b/controllers/k8score/const.go index e6cc1fc94..31476e120 100644 --- a/controllers/k8score/const.go +++ b/controllers/k8score/const.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score diff --git a/controllers/k8score/event_controller.go b/controllers/k8score/event_controller.go index 3a35969fc..2f351f2f9 100644 --- a/controllers/k8score/event_controller.go +++ b/controllers/k8score/event_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score @@ -19,7 +22,6 @@ package k8score import ( "context" "encoding/json" - "regexp" "strings" corev1 "k8s.io/api/core/v1" @@ -32,11 +34,13 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/consensusset" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" + "github.com/apecloud/kubeblocks/controllers/apps/components/consensus" + "github.com/apecloud/kubeblocks/controllers/apps/components/replication" componentutil "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/consensusset" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + probeutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type EventHandler interface { @@ -72,6 +76,7 @@ var _ EventHandler = &RoleChangeEventHandler{} func init() { EventHandlerMap["role-change-handler"] = &RoleChangeEventHandler{} + EventHandlerMap["consensus-set-event-handler"] = &consensusset.PodRoleEventHandler{} } // Reconcile is part of the main kubernetes reconciliation loop which aims to @@ -112,7 +117,7 @@ func (r *EventReconciler) SetupWithManager(mgr ctrl.Manager) error { // Handle handles role changed event. func (r *RoleChangeEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) error { - if event.InvolvedObject.FieldPath != constant.ProbeCheckRolePath { + if event.Reason != string(probeutil.CheckRoleOperation) { return nil } var ( @@ -146,7 +151,7 @@ func handleRoleChangedEvent(cli client.Client, reqCtx intctrlutil.RequestCtx, re return "", nil } - // if probe event operation is not impl, check role failed or role invalid, ignore it + // if probe event operation is not implemented, check role failed or invalid, ignore it if message.Event == ProbeEventOperationNotImpl || message.Event == ProbeEventCheckRoleFailed || message.Event == ProbeEventRoleInvalid { reqCtx.Log.Info("probe event failed", "message", message.Message) return "", nil @@ -175,16 +180,16 @@ func handleRoleChangedEvent(cli client.Client, reqCtx intctrlutil.RequestCtx, re }, cluster); err != nil { return role, err } - reqCtx.Log.V(1).Info("handle role change event", "cluster", cluster.Name, "pod", pod.Name, "role", role, "originalRole", message.OriginalRole) + reqCtx.Log.V(1).Info("handle role changed event", "cluster", cluster.Name, "pod", pod.Name, "role", role, "originalRole", message.OriginalRole) compName, componentDef, err := componentutil.GetComponentInfoByPod(reqCtx.Ctx, cli, *cluster, pod) if err != nil { return role, err } switch componentDef.WorkloadType { case appsv1alpha1.Consensus: - return role, consensusset.UpdateConsensusSetRoleLabel(cli, reqCtx, componentDef, pod, role) + return role, consensus.UpdateConsensusSetRoleLabel(cli, reqCtx, componentDef, pod, role) case appsv1alpha1.Replication: - return role, replicationset.HandleReplicationSetRoleChangeEvent(cli, reqCtx, cluster, compName, pod, role) + return role, replication.HandleReplicationSetRoleChangeEvent(cli, reqCtx, cluster, compName, pod, role) } return role, nil } @@ -192,14 +197,7 @@ func handleRoleChangedEvent(cli client.Client, reqCtx intctrlutil.RequestCtx, re // ParseProbeEventMessage parses probe event message. func ParseProbeEventMessage(reqCtx intctrlutil.RequestCtx, event *corev1.Event) *ProbeMessage { message := &ProbeMessage{} - re := regexp.MustCompile(`Readiness probe failed: ({.*})`) - matches := re.FindStringSubmatch(event.Message) - if len(matches) != 2 { - reqCtx.Log.Info("parser Readiness probe event message failed", "message", event.Message) - return nil - } - msg := matches[1] - err := json.Unmarshal([]byte(msg), message) + err := json.Unmarshal([]byte(event.Message), message) if err != nil { // not role related message, ignore it reqCtx.Log.Info("not role message", "message", event.Message, "error", err) diff --git a/controllers/k8score/event_controller_test.go b/controllers/k8score/event_controller_test.go index d85c4b239..e12537cf0 100644 --- a/controllers/k8score/event_controller_test.go +++ b/controllers/k8score/event_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score @@ -35,6 +38,7 @@ import ( intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/generics" + probeutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) @@ -48,7 +52,7 @@ var _ = Describe("Event Controller", func() { var ctx = context.Background() cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -122,7 +126,7 @@ involvedObject: kind: Pod name: {{ .PodName }} namespace: default -message: "Readiness probe failed: {\"event\":\"roleChanged\",\"originalRole\":\"secondary\",\"role\":\"{{ .Role }}\"}" +message: "{\"event\":\"roleChanged\",\"originalRole\":\"secondary\",\"role\":\"{{ .Role }}\"}" reason: RoleChanged type: Normal ` @@ -146,12 +150,14 @@ type: Normal return nil, err } - event, _, err := scheme.Codecs.UniversalDeserializer().Decode(buf.Bytes(), nil, nil) + event := &corev1.Event{} + _, _, err = scheme.Codecs.UniversalDeserializer().Decode(buf.Bytes(), nil, event) if err != nil { return nil, err } + event.Reason = string(probeutil.CheckRoleOperation) - return event.(*corev1.Event), nil + return event, nil } func createInvolvedPod(name string) corev1.Pod { diff --git a/controllers/k8score/event_utils.go b/controllers/k8score/event_utils.go index 12443fd97..c7e39afb9 100644 --- a/controllers/k8score/event_utils.go +++ b/controllers/k8score/event_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score @@ -22,7 +25,7 @@ import ( corev1 "k8s.io/api/core/v1" ) -// IsOvertimeEvent check whether the duration of warning event reaches the threshold. +// IsOvertimeEvent checks whether the duration of warning event reaches the threshold. func IsOvertimeEvent(event *corev1.Event, timeout time.Duration) bool { if event.Series != nil { return event.Series.LastObservedTime.After(event.EventTime.Add(timeout)) diff --git a/controllers/k8score/pvc_controller.go b/controllers/k8score/pvc_controller.go index 86f775bed..96132e441 100644 --- a/controllers/k8score/pvc_controller.go +++ b/controllers/k8score/pvc_controller.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score @@ -64,6 +67,11 @@ func (r *PersistentVolumeClaimReconciler) Reconcile(ctx context.Context, req ctr return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "getPVCError") } + // skip if pvc is being deleted + if !pvc.DeletionTimestamp.IsZero() { + return intctrlutil.Reconciled() + } + for _, handlePVC := range PersistentVolumeClaimHandlerMap { // ignores the not found error. if err := handlePVC(reqCtx, r.Client, pvc); err != nil && !apierrors.IsNotFound(err) { diff --git a/controllers/k8score/pvc_controller_test.go b/controllers/k8score/pvc_controller_test.go index bfe078868..c4143fc74 100644 --- a/controllers/k8score/pvc_controller_test.go +++ b/controllers/k8score/pvc_controller_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score @@ -33,7 +36,7 @@ import ( var _ = Describe("PersistentVolumeClaim Controller", func() { cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. diff --git a/controllers/k8score/suite_test.go b/controllers/k8score/suite_test.go index a287832fc..954c4060e 100644 --- a/controllers/k8score/suite_test.go +++ b/controllers/k8score/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package k8score diff --git a/controllers/workloads/consensusset_controller.go b/controllers/workloads/consensusset_controller.go new file mode 100644 index 000000000..5e7a56af7 --- /dev/null +++ b/controllers/workloads/consensusset_controller.go @@ -0,0 +1,146 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package workloads + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/tools/record" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/source" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/consensusset" + "github.com/apecloud/kubeblocks/internal/controller/model" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// ConsensusSetReconciler reconciles a ConsensusSet object +type ConsensusSetReconciler struct { + client.Client + Scheme *runtime.Scheme + Recorder record.EventRecorder +} + +//+kubebuilder:rbac:groups=workloads.kubeblocks.io,resources=consensussets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=workloads.kubeblocks.io,resources=consensussets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=workloads.kubeblocks.io,resources=consensussets/finalizers,verbs=update + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the ConsensusSet object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.14.1/pkg/reconcile +func (r *ConsensusSetReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + reqCtx := intctrlutil.RequestCtx{ + Ctx: ctx, + Req: req, + Log: log.FromContext(ctx).WithValues("ConsensusSet", req.NamespacedName), + Recorder: r.Recorder, + } + + reqCtx.Log.V(1).Info("reconcile", "ConsensusSet", req.NamespacedName) + + requeueError := func(err error) (ctrl.Result, error) { + if re, ok := err.(model.RequeueError); ok { + return intctrlutil.RequeueAfter(re.RequeueAfter(), reqCtx.Log, re.Reason()) + } + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + // the consensus set reconciliation loop is a 3-stage model: plan Init, plan Build and plan Execute + // Init stage + planBuilder := consensusset.NewCSSetPlanBuilder(reqCtx, r.Client, req) + if err := planBuilder.Init(); err != nil { + return intctrlutil.CheckedRequeueWithError(err, reqCtx.Log, "") + } + + // Build stage + // what you should do in most cases is writing your transformer. + // + // here are the how-to tips: + // 1. one transformer for one scenario + // 2. try not to modify the current transformers, make a new one + // 3. transformers are independent with each-other, with some exceptions. + // Which means transformers' order is not important in most cases. + // If you don't know where to put your transformer, append it to the end and that would be ok. + // 4. don't use client.Client for object write, use client.ReadonlyClient for object read. + // If you do need to create/update/delete object, make your intent operation a model.ObjectVertex and put it into the DAG. + // + // TODO: transformers are vertices, theirs' dependencies are edges, make plan Build stage a DAG. + plan, err := planBuilder. + AddTransformer( + // fix meta + &consensusset.FixMetaTransformer{}, + // handle deletion + // handle cluster deletion first + &consensusset.CSSetDeletionTransformer{}, + // handle secondary objects generation + &consensusset.ObjectGenerationTransformer{}, + // handle status + &consensusset.CSSetStatusTransformer{}, + // handle UpdateStrategy + &consensusset.UpdateStrategyTransformer{}, + // handle member reconfiguration + &consensusset.MemberReconfigurationTransformer{}, + // always safe to put your transformer below + ). + Build() + if err != nil { + return requeueError(err) + } + // TODO: define error categories in Build stage and handle them here like this: + // switch errBuild.(type) { + // case NOTFOUND: + // case ALREADYEXISY: + // } + + // Execute stage + if err = plan.Execute(); err != nil { + return requeueError(err) + } + + return intctrlutil.Reconciled() +} + +// SetupWithManager sets up the controller with the Manager. +func (r *ConsensusSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&workloads.ConsensusSet{}). + Owns(&appsv1.StatefulSet{}). + Owns(&batchv1.Job{}). + Watches(&source.Kind{Type: &corev1.Pod{}}, + &consensusset.EnqueueRequestForAncestor{ + Client: r.Client, + OwnerType: &workloads.ConsensusSet{}, + UpToLevel: 2, + }). + Complete(r) +} diff --git a/controllers/workloads/consensusset_controller_test.go b/controllers/workloads/consensusset_controller_test.go new file mode 100644 index 000000000..5e545cff4 --- /dev/null +++ b/controllers/workloads/consensusset_controller_test.go @@ -0,0 +1,83 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package workloads + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/builder" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" +) + +var _ = Describe("ConsensusSet Controller", func() { + Context("reconciliation", func() { + It("should reconcile well", func() { + name := "text-consensus-set" + port := int32(12345) + service := corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + Port: port, + }, + }, + } + pod := builder.NewPodBuilder(testCtx.DefaultNamespace, "foo"). + AddContainer(corev1.Container{ + Name: "foo", + Image: "bar", + Ports: []corev1.ContainerPort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + }).GetObject() + template := corev1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + Spec: pod.Spec, + } + action := workloads.Action{ + Image: "foo", + Command: []string{"bar"}, + } + csSet := builder.NewConsensusSetBuilder(testCtx.DefaultNamespace, name). + SetService(service). + SetTemplate(template). + AddObservationAction(action). + GetObject() + Expect(k8sClient.Create(ctx, csSet)).Should(Succeed()) + Eventually(testapps.CheckObj(&testCtx, client.ObjectKeyFromObject(csSet), + func(g Gomega, set *workloads.ConsensusSet) { + g.Expect(set.Status.ObservedGeneration).Should(BeEquivalentTo(1)) + }), + ).Should(Succeed()) + Expect(k8sClient.Delete(ctx, csSet)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, client.ObjectKeyFromObject(csSet), &workloads.ConsensusSet{}, false)). + Should(Succeed()) + }) + }) +}) diff --git a/controllers/workloads/suite_test.go b/controllers/workloads/suite_test.go new file mode 100644 index 000000000..97b2f5155 --- /dev/null +++ b/controllers/workloads/suite_test.go @@ -0,0 +1,118 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package workloads + +import ( + "context" + "path/filepath" + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/envtest" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/log/zap" + + //+kubebuilder:scaffold:imports + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + workloadsv1alpha1 "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/testutil" +) + +// 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 +var ctx context.Context +var cancel context.CancelFunc +var testCtx testutil.TestContext + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Controller Suite") +} + +var _ = BeforeSuite(func() { + logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) + + ctx, cancel = context.WithCancel(context.TODO()) + + By("bootstrapping test environment") + testEnv = &envtest.Environment{ + CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + ErrorIfCRDPathMissing: true, + } + + var err error + // cfg is defined in this file globally. + cfg, err = testEnv.Start() + Expect(err).NotTo(HaveOccurred()) + Expect(cfg).NotTo(BeNil()) + + err = workloadsv1alpha1.AddToScheme(scheme.Scheme) + Expect(err).NotTo(HaveOccurred()) + + //+kubebuilder:scaffold:scheme + + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + Expect(err).NotTo(HaveOccurred()) + Expect(k8sClient).NotTo(BeNil()) + + testCtx = testutil.NewDefaultTestContext(ctx, k8sClient, testEnv) + + // run reconcile + k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ + Scheme: scheme.Scheme, + MetricsBindAddress: "0", + }) + Expect(err).ToNot(HaveOccurred()) + + recorder := k8sManager.GetEventRecorderFor("consensus-set-controller") + err = (&ConsensusSetReconciler{ + Client: k8sManager.GetClient(), + Scheme: k8sManager.GetScheme(), + Recorder: recorder, + }).SetupWithManager(k8sManager) + Expect(err).ToNot(HaveOccurred()) + + appsv1alpha1.RegisterWebhookManager(k8sManager) + + go func() { + defer GinkgoRecover() + err = k8sManager.Start(ctx) + Expect(err).ToNot(HaveOccurred(), "failed to run manager") + }() +}) + +var _ = AfterSuite(func() { + cancel() + By("tearing down the test environment") + err := testEnv.Stop() + Expect(err).NotTo(HaveOccurred()) +}) diff --git a/deploy/apecloud-mysql-cluster/Chart.yaml b/deploy/apecloud-mysql-cluster/Chart.yaml index 262d694e3..087607e7d 100644 --- a/deploy/apecloud-mysql-cluster/Chart.yaml +++ b/deploy/apecloud-mysql-cluster/Chart.yaml @@ -4,6 +4,6 @@ description: An ApeCloud MySQL Cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: "8.0.30" diff --git a/deploy/apecloud-mysql-cluster/templates/_helpers.tpl b/deploy/apecloud-mysql-cluster/templates/_helpers.tpl index 90137c9f9..0a1088741 100644 --- a/deploy/apecloud-mysql-cluster/templates/_helpers.tpl +++ b/deploy/apecloud-mysql-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "apecloud-mysql-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "apecloud-mysql-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "apecloud-mysql-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "apecloud-mysql-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/apecloud-mysql-cluster/templates/cluster.yaml b/deploy/apecloud-mysql-cluster/templates/cluster.yaml index aa6f5536a..d02f155af 100644 --- a/deploy/apecloud-mysql-cluster/templates/cluster.yaml +++ b/deploy/apecloud-mysql-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: apecloud-mysql # ref clusterdefinition.name @@ -19,6 +19,7 @@ spec: componentDefRef: mysql # ref clusterdefinition componentDefs.name monitor: {{ .Values.monitor.enabled | default false }} replicas: {{ .Values.replicaCount | default 3 }} + serviceAccountName: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} enabledLogs: {{ .Values.enabledLogs | toJson | indent 4 }} {{- with .Values.resources }} resources: diff --git a/deploy/apecloud-mysql-cluster/templates/role.yaml b/deploy/apecloud-mysql-cluster/templates/role.yaml new file mode 100644 index 000000000..39f74a5f5 --- /dev/null +++ b/deploy/apecloud-mysql-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/apecloud-mysql-cluster/templates/rolebinding.yaml b/deploy/apecloud-mysql-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..ec145c939 --- /dev/null +++ b/deploy/apecloud-mysql-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/apecloud-mysql-cluster/templates/serviceaccount.yaml b/deploy/apecloud-mysql-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..b29b17731 --- /dev/null +++ b/deploy/apecloud-mysql-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} diff --git a/deploy/apecloud-mysql-cluster/values.yaml b/deploy/apecloud-mysql-cluster/values.yaml index 414ec673d..e863abc89 100644 --- a/deploy/apecloud-mysql-cluster/values.yaml +++ b/deploy/apecloud-mysql-cluster/values.yaml @@ -7,6 +7,8 @@ replicaCount: 3 terminationPolicy: Delete clusterVersionOverride: "" +nameOverride: "" +fullnameOverride: "" monitor: enabled: false @@ -58,4 +60,8 @@ topologyKeys: ## @param tolerations ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## -tolerations: [ ] \ No newline at end of file +tolerations: [ ] + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/apecloud-mysql-scale-cluster/Chart.yaml b/deploy/apecloud-mysql-scale-cluster/Chart.yaml index 5634d4b3e..7bf2d56f1 100644 --- a/deploy/apecloud-mysql-scale-cluster/Chart.yaml +++ b/deploy/apecloud-mysql-scale-cluster/Chart.yaml @@ -4,7 +4,7 @@ description: An ApeCloud MySQL-Scale Cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 # This is the version number of the ApeCloud MySQL being deployed, # rather than the version number of ApeCloud MySQL-Scale itself. diff --git a/deploy/apecloud-mysql-scale-cluster/templates/_helpers.tpl b/deploy/apecloud-mysql-scale-cluster/templates/_helpers.tpl index 90137c9f9..0a1088741 100644 --- a/deploy/apecloud-mysql-scale-cluster/templates/_helpers.tpl +++ b/deploy/apecloud-mysql-scale-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "apecloud-mysql-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "apecloud-mysql-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "apecloud-mysql-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "apecloud-mysql-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/apecloud-mysql-scale-cluster/templates/cluster.yaml b/deploy/apecloud-mysql-scale-cluster/templates/cluster.yaml index accd02e77..57cd9d841 100644 --- a/deploy/apecloud-mysql-scale-cluster/templates/cluster.yaml +++ b/deploy/apecloud-mysql-scale-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: apecloud-mysql-scale # ref clusterdefinition.name @@ -20,6 +20,7 @@ spec: monitor: {{ .Values.monitor.enabled | default false }} replicas: {{ .Values.replicaCount | default 3 }} enabledLogs: {{ .Values.enabledLogs | toJson | indent 4 }} + serviceAccountName: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} {{- with .Values.resources }} resources: limits: diff --git a/deploy/apecloud-mysql-scale-cluster/templates/role.yaml b/deploy/apecloud-mysql-scale-cluster/templates/role.yaml new file mode 100644 index 000000000..39f74a5f5 --- /dev/null +++ b/deploy/apecloud-mysql-scale-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/apecloud-mysql-scale-cluster/templates/rolebinding.yaml b/deploy/apecloud-mysql-scale-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..ec145c939 --- /dev/null +++ b/deploy/apecloud-mysql-scale-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/apecloud-mysql-scale-cluster/templates/serviceaccount.yaml b/deploy/apecloud-mysql-scale-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..b29b17731 --- /dev/null +++ b/deploy/apecloud-mysql-scale-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "apecloud-mysql-cluster.serviceAccountName" . }} + labels: + {{ include "apecloud-mysql-cluster.labels" . | nindent 4 }} diff --git a/deploy/apecloud-mysql-scale-cluster/values.yaml b/deploy/apecloud-mysql-scale-cluster/values.yaml index 016b39146..378238ed2 100644 --- a/deploy/apecloud-mysql-scale-cluster/values.yaml +++ b/deploy/apecloud-mysql-scale-cluster/values.yaml @@ -7,6 +7,8 @@ replicaCount: 3 terminationPolicy: Delete clusterVersionOverride: "" +nameOverride: "" +fullnameOverride: "" monitor: enabled: false @@ -58,4 +60,8 @@ topologyKeys: ## @param tolerations ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ ## -tolerations: [ ] \ No newline at end of file +tolerations: [ ] + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/apecloud-mysql-scale/Chart.yaml b/deploy/apecloud-mysql-scale/Chart.yaml index 165ca3995..bf8c86c01 100644 --- a/deploy/apecloud-mysql-scale/Chart.yaml +++ b/deploy/apecloud-mysql-scale/Chart.yaml @@ -1,11 +1,11 @@ apiVersion: v2 name: apecloud-mysql-scale description: ApeCloud MySQL-Scale is ApeCloud MySQL proxy. ApeCloud MySQL-Scale can support - apecloud mysql failover automatically and read write seperation. + apecloud mysql failover automatically and read write separation. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 # This is the version number of the ApeCloud MySQL being deployed, # rather than the version number of ApeCloud MySQL-Scale itself. diff --git a/deploy/apecloud-mysql-scale/config/mysql8-config-constraint.cue b/deploy/apecloud-mysql-scale/config/mysql8-config-constraint.cue index 24108dc7f..19104e267 100644 --- a/deploy/apecloud-mysql-scale/config/mysql8-config-constraint.cue +++ b/deploy/apecloud-mysql-scale/config/mysql8-config-constraint.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #MysqlParameter: { @@ -83,7 +86,7 @@ // If this variable is enabled (the default), transactions are committed in the same order they are written to the binary log. If disabled, transactions may be committed in parallel. binlog_order_commits?: string & "0" | "1" | "OFF" | "ON" - // Whether the server logs full or minmal rows with row-based replication. + // Whether the server logs full or minimal rows with row-based replication. binlog_row_image?: string & "FULL" | "MINIMAL" | "NOBLOB" // Controls whether metadata is logged using FULL or MINIMAL format. FULL causes all metadata to be logged; MINIMAL means that only metadata actually required by slave is logged. Default: MINIMAL. @@ -1526,8 +1529,8 @@ // For SQL window functions, determines whether to enable inversion optimization for moving window frames also for floating values. windowing_use_high_precision: string & "0" | "1" | "OFF" | "ON" | *"1" - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } diff --git a/deploy/apecloud-mysql-scale/config/mysql8-config.tpl b/deploy/apecloud-mysql-scale/config/mysql8-config.tpl index a1b8a2a12..43a59d898 100644 --- a/deploy/apecloud-mysql-scale/config/mysql8-config.tpl +++ b/deploy/apecloud-mysql-scale/config/mysql8-config.tpl @@ -143,7 +143,7 @@ log_bin_index=mysql-bin.index max_binlog_size=134217728 log_replica_updates=1 # binlog_rows_query_log_events=ON #AWS not set -# binlog_transaction_dependency_tracking=WRITESET #Defautl Commit Order, Aws not set +# binlog_transaction_dependency_tracking=WRITESET #Default Commit Order, Aws not set # replay log # relay_log_info_repository=TABLE diff --git a/deploy/apecloud-mysql-scale/templates/backuppolicytemplate.yaml b/deploy/apecloud-mysql-scale/templates/backuppolicytemplate.yaml index 7c708ccc1..437548b7f 100644 --- a/deploy/apecloud-mysql-scale/templates/backuppolicytemplate.yaml +++ b/deploy/apecloud-mysql-scale/templates/backuppolicytemplate.yaml @@ -1,21 +1,33 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-mysql-scale + name: apecloud-mysql-scale-backup-policy-template labels: - clusterdefinition.kubeblocks.io/name: apecloud-mysql + clusterdefinition.kubeblocks.io/name: apecloud-mysql-scale {{- include "apecloud-mysql.labels" . | nindent 4 }} + annotations: + dataprotection.kubeblocks.io/is-default-policy-template: "true" spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: xtrabackup-mysql-scale - ttl: 168h0m0s - hooks: - containerName: mysql - preCommands: - - "touch /data/mysql/data/.restore_new_cluster; sync" - postCommands: - - "rm -f /data/mysql/data/.restore_new_cluster; sync" - - credentialKeyword: - userKeyword: username - passwordKeyword: password + clusterDefinitionRef: apecloud-mysql-scale + backupPolicies: + - componentDefRef: mysql + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * *" + datafile: + enable: false + cronExpression: "0 18 * * *" + snapshot: + hooks: + containerName: mysql + preCommands: + - "touch /data/mysql/data/.restore_new_cluster; sync" + postCommands: + - "rm -f /data/mysql/data/.restore_new_cluster; sync" + target: + role: leader + datafile: + backupToolName: xtrabackup-for-apecloud-mysql-scale \ No newline at end of file diff --git a/deploy/apecloud-mysql-scale/templates/backuppolicytemplateforhscale.yaml b/deploy/apecloud-mysql-scale/templates/backuppolicytemplateforhscale.yaml new file mode 100644 index 000000000..143abe1ec --- /dev/null +++ b/deploy/apecloud-mysql-scale/templates/backuppolicytemplateforhscale.yaml @@ -0,0 +1,21 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: BackupPolicyTemplate +metadata: + name: apecloud-mysql-scale-backup-policy-template-for-hscale + labels: + clusterdefinition.kubeblocks.io/name: apecloud-mysql + {{- include "apecloud-mysql.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: apecloud-mysql + identifier: hscale + backupPolicies: + - componentDefRef: mysql + snapshot: + hooks: + containerName: mysql + preCommands: + - "touch /data/mysql/data/.restore; sync" + postCommands: + - "rm -f /data/mysql/data/.restore; sync" + target: + role: leader \ No newline at end of file diff --git a/deploy/apecloud-mysql-scale/templates/backuptool.yaml b/deploy/apecloud-mysql-scale/templates/backuptool.yaml index 26266043f..0260a3a93 100644 --- a/deploy/apecloud-mysql-scale/templates/backuptool.yaml +++ b/deploy/apecloud-mysql-scale/templates/backuptool.yaml @@ -1,20 +1,13 @@ apiVersion: dataprotection.kubeblocks.io/v1alpha1 kind: BackupTool metadata: - name: xtrabackup-mysql-scale + name: xtrabackup-for-apecloud-mysql-scale labels: clusterdefinition.kubeblocks.io/name: apecloud-mysql {{- include "apecloud-mysql.labels" . | nindent 4 }} spec: image: registry.cn-hangzhou.aliyuncs.com/apecloud/percona-xtrabackup deployKind: job - resources: - limits: - cpu: "1" - memory: 2Gi - requests: - cpu: "1" - memory: 128Mi env: - name: DATA_DIR value: /data/mysql/data @@ -22,21 +15,28 @@ spec: restoreCommands: - > set -e; - mkdir -p /tmp/data/ && cd /tmp/data; - xbstream -x < /${BACKUP_DIR}/${BACKUP_NAME}.xbstream; - xtrabackup --decompress --target-dir=/tmp/data/; - xtrabackup --prepare --target-dir=/tmp/data/; - find . -name "*.qp"|xargs rm -f; - rm -rf ${DATA_DIR}/*; - rm -rf ${DATA_DIR}/.xtrabackup_restore_new_cluster; - xtrabackup --move-back --target-dir=/tmp/data/ --datadir=${DATA_DIR}/; - touch ${DATA_DIR}/.xtrabackup_restore_new_cluster; - rm -rf /tmp/data/; - chmod -R 0777 ${DATA_DIR}; + mkdir -p ${DATA_DIR} + res=`ls -A ${DATA_DIR}` + if [ ! -z ${res} ]; then + echo "${DATA_DIR} is not empty! Please make sure that the directory is empty before restoring the backup." + exit 1 + fi + mkdir -p /tmp/data/ && cd /tmp/data + xbstream -x < ${BACKUP_DIR}/${BACKUP_NAME}.xbstream + xtrabackup --decompress --target-dir=/tmp/data/ + xtrabackup --prepare --target-dir=/tmp/data/ + find . -name "*.qp"|xargs rm -f + xtrabackup --move-back --target-dir=/tmp/data/ --datadir=${DATA_DIR}/ + touch ${DATA_DIR}/.xtrabackup_restore_new_cluster + rm -rf /tmp/data/ + chmod -R 0777 ${DATA_DIR} incrementalRestoreCommands: [] logical: restoreCommands: [] incrementalRestoreCommands: [] backupCommands: - - xtrabackup --compress --backup --safe-slave-backup --slave-info --stream=xbstream --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} --datadir=${DATA_DIR} > /${BACKUP_DIR}/${BACKUP_NAME}.xbstream + - | + set -e + mkdir -p ${BACKUP_DIR} + xtrabackup --compress --backup --safe-slave-backup --slave-info --stream=xbstream --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} --datadir=${DATA_DIR} > ${BACKUP_DIR}/${BACKUP_NAME}.xbstream incrementalBackupCommands: [] diff --git a/deploy/apecloud-mysql-scale/templates/clusterdefinition.yaml b/deploy/apecloud-mysql-scale/templates/clusterdefinition.yaml index e76bb9f15..eac59be4c 100644 --- a/deploy/apecloud-mysql-scale/templates/clusterdefinition.yaml +++ b/deploy/apecloud-mysql-scale/templates/clusterdefinition.yaml @@ -20,10 +20,10 @@ spec: - name: mysql characterType: mysql probes: - roleChangedProbe: - failureThreshold: {{ .Values.roleChangedProbe.failureThreshold }} - periodSeconds: {{ .Values.roleChangedProbe.periodSeconds }} - timeoutSeconds: {{ .Values.roleChangedProbe.timeoutSeconds }} + roleProbe: + failureThreshold: {{ .Values.roleProbe.failureThreshold }} + periodSeconds: {{ .Values.roleProbe.periodSeconds }} + timeoutSeconds: {{ .Values.roleProbe.timeoutSeconds }} monitor: builtIn: false exporterConfig: @@ -73,8 +73,7 @@ spec: targetPort: delvedebug horizontalScalePolicy: type: Snapshot - backupTemplateSelector: - "clusterdefinition.kubeblocks.io/name": apecloud-mysql-scale + backupPolicyTemplateName: apecloud-mysql-scale-backup-policy-template-for-hscale podSpec: containers: - name: mysql @@ -99,11 +98,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: MYSQL_DATABASE value: {{- if .Values.auth.createDatabase }} {{ .Values.auth.database | quote }} {{- else }} "" {{- end }} - name: MYSQL_USER @@ -124,7 +125,9 @@ spec: value: {{ if .Values.cluster.customConfig }}{{ .Values.cluster.customConfig }}{{ end }} - name: MYSQL_DYNAMIC_CONFIG value: {{ if .Values.cluster.dynamicConfig }}{{ .Values.cluster.dynamicConfig }}{{ end }} - command: ["/scripts/setup.sh"] + - name: KB_EMBEDDED_WESQL + value: {{ .Values.cluster.kbWeSQLImage | default "1" | quote }} + command: ["/scripts/setup.sh"] - name: metrics image: {{ .Values.metrics.image.registry | default "docker.io" }}/{{ .Values.metrics.image.repository }}:{{ .Values.metrics.image.tag }} imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} @@ -137,11 +140,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: DATA_SOURCE_NAME value: "$(MYSQL_USER):$(MYSQL_PASSWORD)@(localhost:3306)/" command: @@ -200,11 +205,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) - key: password + key: password + optional: false command: - /bin/bash - -c @@ -220,7 +227,7 @@ spec: vtctld_web_port=${VTCTLD_WEB_PORT:-'15000'} printf -v alias '%s-%010d' $cell $uid printf -v tablet_dir 'vt_%010d' $uid - tablet_hostname=$(eval echo \$KB_MYSQL_"$uid"_HOSTNAME) + tablet_hostname=$(eval echo \$KB_"$uid"_HOSTNAME) printf -v tablet_logfile 'vttablet_%010d_querylog.txt' $uid tablet_type=replica @@ -278,11 +285,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false passwordConfig: length: 10 numDigits: 5 @@ -484,11 +493,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: VTCONSENSUS_PORT value: "16000" - name: ETCD_SERVER @@ -553,11 +564,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) - key: password + key: password + optional: false - name: CELL value: {{ .Values.wesqlscale.cell | default "zone1" | quote }} - name: VTGATE_MYSQL_PORT @@ -588,10 +601,10 @@ spec: $TOPOLOGY_FLAGS \ --alsologtostderr \ --gateway_initial_tablet_timeout 30s \ - --healthcheck_timeout 1s \ + --healthcheck_timeout 2s \ --srv_topo_timeout 1s \ - --grpc_keepalive_time 1s \ - --grpc_keepalive_timeout 2s \ + --grpc_keepalive_time 10s \ + --grpc_keepalive_timeout 10s \ --log_dir $VTDATAROOT \ --log_queries_to_file $VTDATAROOT/vtgate_querylog.txt \ --port $web_port \ @@ -604,4 +617,4 @@ spec: --service_map 'grpc-vtgateservice' \ --pid_file $VTDATAROOT/vtgate.pid \ --mysql_auth_server_impl none - EOF \ No newline at end of file + EOF diff --git a/deploy/apecloud-mysql-scale/templates/scripts.yaml b/deploy/apecloud-mysql-scale/templates/scripts.yaml index f11d52f24..d47e4eb63 100644 --- a/deploy/apecloud-mysql-scale/templates/scripts.yaml +++ b/deploy/apecloud-mysql-scale/templates/scripts.yaml @@ -7,133 +7,61 @@ metadata: data: setup.sh: | #!/bin/bash - leader=$KB_MYSQL_LEADER - followers=$KB_MYSQL_FOLLOWERS - echo $leader - echo $followers - sub_follower=`echo "$followers" | grep "$KB_POD_NAME"` - echo $KB_POD_NAME - echo $sub_follower - if [ -z "$leader" -o "$KB_POD_NAME" = "$leader" -o ! -z "$sub_follower" ]; then - echo "no need to call add" - else - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - echo "$host" - leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) - if [ ! -z $leader_host ]; then - host_flag="-h$leader_host" - fi - if [ ! -z $MYSQL_ROOT_PASSWORD ]; then - password_flag="-p$MYSQL_ROOT_PASSWORD" - fi - echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.add_learner('$host:13306');\" >> /tmp/setup_error.log 2>&1 " - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.add_learner('$host:13306');" >> /tmp/setup_error.log 2>&1 - code=$? - echo "exit code: $code" - if [ $code -ne 0 ]; then - cat /tmp/setup_error.log - already_exists=`cat /tmp/setup_error.log | grep "Target node already exists"` - if [ -z "$already_exists" ]; then - exit $code - fi - fi - /scripts/upgrade-learner.sh & - fi - cluster_info=""; for (( i=0; i< $KB_MYSQL_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_MYSQL_"$i"_HOSTNAME) - cluster_info="$cluster_info$host:13306"; - done; - idx=${KB_POD_NAME##*-} - echo $idx - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - cluster_info="$cluster_info@$(($idx+1))"; - echo $cluster_info; - mkdir -p /data/mysql/data; - mkdir -p /data/mysql/log; - chmod +777 -R /data/mysql; - leader=$KB_MYSQL_LEADER - echo $leader - echo "KB_MYSQL_RECREATE=$KB_MYSQL_RECREATE" - if [ "$KB_MYSQL_RECREATE" == "true" ]; then - echo "recreate from existing volumes, touch /data/mysql/data/.resetup_db" - touch /data/mysql/data/.resetup_db - fi - if [ -z $leader ] || [ ! -f "/data/mysql/data/.restore" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$cluster_info\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$cluster_info" --cluster-id=$CLUSTER_ID - elif [ "$KB_POD_NAME" != "$leader" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306" --cluster-id=$CLUSTER_ID - else - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306@1\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306@1" --cluster-id=$CLUSTER_ID - fi - upgrade-learner.sh: | + exec docker-entrypoint.sh + pre-stop.sh: | #!/bin/bash - leader=$KB_MYSQL_LEADER - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) + drop_followers() { + echo "leader=$leader" >> /data/mysql/.kb_pre_stop.log + echo "KB_POD_NAME=$KB_POD_NAME" >> /data/mysql/.kb_pre_stop.log + if [ -z "$leader" -o "$KB_POD_NAME" = "$leader" ]; then + echo "no leader or self is leader, exit" >> /data/mysql/.kb_pre_stop.log + exit 0 + fi + host=$(eval echo \$KB_"$idx"_HOSTNAME) + echo "host=$host" >> /data/mysql/.kb_pre_stop.log leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) + leader_host=$(eval echo \$KB_"$leader_idx"_HOSTNAME) if [ ! -z $leader_host ]; then host_flag="-h$leader_host" fi if [ ! -z $MYSQL_ROOT_PASSWORD ]; then password_flag="-p$MYSQL_ROOT_PASSWORD" fi - while true - do - sleep 5 - mysql -uroot $password_flag -e "select ROLE from information_schema.wesql_cluster_local" > /tmp/role.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/role.log >> /tmp/upgrade-learner.log - else - role=`cat /tmp/role.log` - echo "role: $role" >> /tmp/upgrade-learner.log - if [ -z "$role" ]; then - echo "cannot get role" >> /tmp/upgrade-learner.log - else - break - fi - fi - else - kill -9 $pid - echo "mysql timeout" >> /tmp/upgrade-learner.log + echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.downgrade_follower('$host:13306');\" 2>&1 " >> /data/mysql/.kb_pre_stop.log + mysql $host_flag -uroot $password_flag -e "call dbms_consensus.downgrade_follower('$host:13306');" 2>&1 + echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.drop_learner('$host:13306');\" 2>&1 " >> /data/mysql/.kb_pre_stop.log + mysql $host_flag -uroot $password_flag -e "call dbms_consensus.drop_learner('$host:13306');" 2>&1 + } + switchover() { + if [ ! -z $MYSQL_ROOT_PASSWORD ]; then + password_flag="-p$MYSQL_ROOT_PASSWORD" fi - done - grep_learner=`echo $role | grep "Learner"` - echo "grep learner: $grep_learner" >> /tmp/upgrade-learner.log - if [ -z "$grep_learner" ]; then - exit 0 - fi - while true - do - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.upgrade_learner('$host:13306');" >> /tmp/upgrade.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/upgrade.log >> /tmp/upgrade-learner.log - already_exists=`cat /tmp/upgrade.log | grep "Target node already exists"` - if [ ! -z "$already_exists" ]; then - break - fi - else - break + new_leader_host=$KB_0_HOSTNAME + if [ "$KB_POD_NAME" = "$leader" ]; then + echo "self is leader, need to switchover" >> /data/mysql/.kb_pre_stop.log + echo "mysql -uroot $password_flag -e \"call dbms_consensus.change_leader('$new_leader_host:13306');\" 2>&1" >> /data/mysql/.kb_pre_stop.log + mysql -uroot $password_flag -e "call dbms_consensus.change_leader('$new_leader_host:13306');" 2>&1 + sleep 1 + role_info=`mysql -uroot $password_flag -e "select * from information_schema.wesql_cluster_local;" 2>&1` + echo "role_info=$role_info" >> /data/mysql/.kb_pre_stop.log + is_follower=`echo $role_info | grep "Follower"` + if [ ! -z "$is_follower" ]; then + echo "new_leader=$new_leader_host" >> /data/mysql/.kb_pre_stop.log + leader=`echo "$new_leader_host" | cut -d "." -f 1` + idx=${KB_POD_NAME##*-} fi - else - kill -9 $pid - echo "mysql call leader timeout" >> /tmp/upgrade-learner.log fi - sleep 5 - done + } + leader=`cat /etc/annotations/leader` + idx=${KB_POD_NAME##*-} + current_component_replicas=`cat /etc/annotations/component-replicas` + echo "current replicas: $current_component_replicas" >> /data/mysql/.kb_pre_stop.log + if [ ! $idx -lt $current_component_replicas ] && [ $current_component_replicas -ne 0 ]; then + # if idx greater than or equal to current_component_replicas means the cluster's scaling in + # switch leader before leader scaling in itself + switchover + # only scaling in need to drop followers + drop_followers + else + echo "no need to drop followers" >> /data/mysql/.kb_pre_stop.log + fi diff --git a/deploy/apecloud-mysql-scale/values.yaml b/deploy/apecloud-mysql-scale/values.yaml index 9eab84d14..b644961a4 100644 --- a/deploy/apecloud-mysql-scale/values.yaml +++ b/deploy/apecloud-mysql-scale/values.yaml @@ -7,7 +7,7 @@ image: repository: apecloud/apecloud-mysql-server pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: 8.0.30-5.alpha2.20230105.gd6b8719.2 + tag: 8.0.30-5.alpha8.20230523.g3e93ae7.8 ## MySQL Cluster parameters cluster: @@ -23,6 +23,8 @@ cluster: customConfig: ## MYSQL_DYNAMIC_CONFIG dynamicConfig: + ## KB_EMBEDDED_WESQL + kbWeSQLImage: "1" ## MySQL Authentication parameters auth: @@ -61,7 +63,7 @@ logConfigs: slow: /data/mysql/log/mysqld-slowquery.log general: /data/mysql/log/mysqld.log -roleChangedProbe: +roleProbe: failureThreshold: 2 periodSeconds: 1 timeoutSeconds: 1 @@ -79,4 +81,4 @@ wesqlscale: registry: registry.cn-hangzhou.aliyuncs.com repository: apecloud/apecloud-mysql-scale tag: "latest" - pullPolicy: IfNotPresent \ No newline at end of file + pullPolicy: IfNotPresent diff --git a/deploy/apecloud-mysql/Chart.yaml b/deploy/apecloud-mysql/Chart.yaml index 736d7bd5b..4bba2d02f 100644 --- a/deploy/apecloud-mysql/Chart.yaml +++ b/deploy/apecloud-mysql/Chart.yaml @@ -1,15 +1,11 @@ apiVersion: v2 name: apecloud-mysql -description: ApeCloud MySQL is fully compatible with MySQL syntax and supports single-availability - zone deployment, double-availability zone deployment, and multiple-availability zone deployment. - Based on the Paxos consensus protocol, ApeCloud MySQL realizes automatic leader election, log - synchronization, and strict consistency. ApeCloud MySQL is the optimum choice for the production - environment since it can automatically perform a high-availability switch to maintain business continuity - when container exceptions, server exceptions, or availability zone exceptions occur. +description: ApeCloud MySQL is a database that is compatible with MySQL syntax and achieves high availability + through the utilization of the RAFT consensus protocol. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: "8.0.30" diff --git a/deploy/apecloud-mysql/config/mysql8-config-constraint.cue b/deploy/apecloud-mysql/config/mysql8-config-constraint.cue index 24108dc7f..528e3df1b 100644 --- a/deploy/apecloud-mysql/config/mysql8-config-constraint.cue +++ b/deploy/apecloud-mysql/config/mysql8-config-constraint.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #MysqlParameter: { @@ -66,7 +69,7 @@ binlog_expire_logs_seconds: int & >=0 & <=4294967295 | *2592000 // Row-based, Statement-based or Mixed replication - binlog_format?: string & "ROW" | "STATEMENT" | "MIXED" + binlog_format?: string & "ROW" | "STATEMENT" | "MIXED" | "row" | "statement" | "mixed" // Controls how many microseconds the binary log commit waits before synchronizing the binary log file to disk. binlog_group_commit_sync_delay?: int & >=0 & <=1000000 @@ -83,11 +86,11 @@ // If this variable is enabled (the default), transactions are committed in the same order they are written to the binary log. If disabled, transactions may be committed in parallel. binlog_order_commits?: string & "0" | "1" | "OFF" | "ON" - // Whether the server logs full or minmal rows with row-based replication. - binlog_row_image?: string & "FULL" | "MINIMAL" | "NOBLOB" + // Whether the server logs full or minimal rows with row-based replication. + binlog_row_image?: string & "FULL" | "MINIMAL" | "NOBLOB" | "full" | "minimal" | "noblob" // Controls whether metadata is logged using FULL or MINIMAL format. FULL causes all metadata to be logged; MINIMAL means that only metadata actually required by slave is logged. Default: MINIMAL. - binlog_row_metadata?: string & "FULL" | "MINIMAL" + binlog_row_metadata?: string & "FULL" | "MINIMAL" | "full" | "minimal" // When enabled, it causes a MySQL 5.6.2 or later server to write informational log events such as row query log events into its binary log. binlog_rows_query_log_events?: string & "0" | "1" | "OFF" | "ON" @@ -165,6 +168,9 @@ // Write a core file if mysqld dies. "core-file"?: string & "0" | "1" | "OFF" | "ON" + // xengine enable + "xengine"?: string & "0" | "1" | "OFF" | "ON" + // Abort a recursive common table expression if it does more than this number of iterations. cte_max_recursion_depth: int & >=0 & <=4294967295 | *1000 @@ -181,7 +187,7 @@ default_password_lifetime: int & >=0 & <=65535 | *0 // The default storage engine (table type). - default_storage_engine?: string & "InnoDB" | "MRG_MYISAM" | "BLACKHOLE" | "CSV" | "MEMORY" | "FEDERATED" | "ARCHIVE" | "MyISAM" + default_storage_engine?: string & "InnoDB" | "MRG_MYISAM" | "BLACKHOLE" | "CSV" | "MEMORY" | "FEDERATED" | "ARCHIVE" | "MyISAM" | "xengine" | "XENGINE" | "INNODB" | "innodb" // Server current time zone default_time_zone?: string @@ -1526,8 +1532,8 @@ // For SQL window functions, determines whether to enable inversion optimization for moving window frames also for floating values. windowing_use_high_precision: string & "0" | "1" | "OFF" | "ON" | *"1" - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } diff --git a/deploy/apecloud-mysql/config/mysql8-config-effect-scope.yaml b/deploy/apecloud-mysql/config/mysql8-config-effect-scope.yaml index 498cdaf09..d3bd9b7bd 100644 --- a/deploy/apecloud-mysql/config/mysql8-config-effect-scope.yaml +++ b/deploy/apecloud-mysql/config/mysql8-config-effect-scope.yaml @@ -5,6 +5,7 @@ ## if all the changed parameters are in the dynamicParameters list, this change executes reload without process restart. ## if the above two conditions are not met, by default, parameter change operation follow the rule for using staticParameters. staticParameters: + - xengine - allow-suspicious-udfs - auto_generate_certs - back_log diff --git a/deploy/apecloud-mysql/config/mysql8-config.tpl b/deploy/apecloud-mysql/config/mysql8-config.tpl index 2aa1d7ec1..8f8882753 100644 --- a/deploy/apecloud-mysql/config/mysql8-config.tpl +++ b/deploy/apecloud-mysql/config/mysql8-config.tpl @@ -6,6 +6,7 @@ {{- $mysql_port_info := getPortByName ( index $.podSpec.containers 0 ) "mysql" }} {{- $pool_buffer_size := ( callBufferSizeByResource ( index $.podSpec.containers 0 ) ) }} {{- $phy_memory := getContainerMemory ( index $.podSpec.containers 0 ) }} +{{- $phy_cpu := getContainerCPU ( index $.podSpec.containers 0 ) }} {{- if $pool_buffer_size }} innodb_buffer_pool_size={{ $pool_buffer_size }} @@ -89,28 +90,25 @@ mysqlx=0 datadir={{ $data_root }}/data +{{ block "logsBlock" . }} log_statements_unsafe_for_binlog=OFF log_error_verbosity=2 log_output=FILE {{- if hasKey $.component "enabledLogs" }} {{- if mustHas "error" $.component.enabledLogs }} -# Mysql error log -log_error={{ $data_root }}/log/mysqld-error.log +log_error=/data/mysql/log/mysqld-error.log {{- end }} - {{- if mustHas "slow" $.component.enabledLogs }} -# MySQL Slow log slow_query_log=ON long_query_time=5 -slow_query_log_file={{ $data_root }}/log/mysqld-slowquery.log +slow_query_log_file=/data/mysql/log/mysqld-slowquery.log {{- end }} - {{- if mustHas "general" $.component.enabledLogs }} -# SQL access log, default off general_log=ON -general_log_file={{ $data_root }}/log/mysqld.log +general_log_file=/data/mysql/log/mysqld.log {{- end }} {{- end }} +{{ end }} #innodb innodb_doublewrite_batch_size=16 @@ -143,7 +141,7 @@ log_bin_index=mysql-bin.index max_binlog_size=134217728 log_replica_updates=1 # binlog_rows_query_log_events=ON #AWS not set -# binlog_transaction_dependency_tracking=WRITESET #Defautl Commit Order, Aws not set +# binlog_transaction_dependency_tracking=WRITESET #Default Commit Order, Aws not set # replay log # relay_log_info_repository=TABLE @@ -166,6 +164,60 @@ ssl_cert={{ $cert_file }} ssl_key={{ $key_file }} {{- end }} +## xengine base config +#default_storage_engine=xengine +default_tmp_storage_engine=innodb +xengine=0 + +# log_error_verbosity=3 +# binlog_format=ROW + +## non classes config + +loose_xengine_datadir={{ $data_root }}/xengine +loose_xengine_wal_dir={{ $data_root }}/xengine +loose_xengine_flush_log_at_trx_commit=1 +loose_xengine_enable_2pc=1 +loose_xengine_batch_group_slot_array_size=5 +loose_xengine_batch_group_max_group_size=15 +loose_xengine_batch_group_max_leader_wait_time_us=50 +loose_xengine_block_size=16384 +loose_xengine_disable_auto_compactions=0 +loose_xengine_dump_memtable_limit_size=0 + +loose_xengine_min_write_buffer_number_to_merge=1 +loose_xengine_level0_file_num_compaction_trigger=64 +loose_xengine_level0_layer_num_compaction_trigger=2 +loose_xengine_level1_extents_major_compaction_trigger=1000 +loose_xengine_level2_usage_percent=70 +loose_xengine_flush_delete_percent=70 +loose_xengine_compaction_delete_percent=50 +loose_xengine_flush_delete_percent_trigger=700000 +loose_xengine_flush_delete_record_trigger=700000 +loose_xengine_scan_add_blocks_limit=100 + +loose_xengine_compression_per_level=kZSTD:kZSTD:kZSTD + + +## classes classes config + +{{- if gt $phy_memory 0 }} +{{- $phy_memory := div $phy_memory ( mul 1024 1024 ) }} +loose_xengine_write_buffer_size={{ min ( max 32 ( mulf $phy_memory 0.01 ) ) 256 | int | mul 1024 1024 }} +loose_xengine_db_write_buffer_size={{ mulf $phy_memory 0.3 | int | mul 1024 1024 }} +loose_xengine_db_total_write_buffer_size={{ mulf $phy_memory 0.3 | int | mul 1024 1024 }} +loose_xengine_block_cache_size={{ mulf $phy_memory 0.3 | int | mul 1024 1024 }} +loose_xengine_row_cache_size={{ mulf $phy_memory 0.1 | int | mul 1024 1024 }} +loose_xengine_max_total_wal_size={{ min ( mulf $phy_memory 0.3 ) ( mul 12 1024 ) | int | mul 1024 1024 }} +{{- end }} + +{{- if gt $phy_cpu 0 }} +loose_xengine_max_background_flushes={{ max 1 ( min ( div $phy_cpu 2 ) 8 ) | int }} +loose_xengine_base_background_compactions={{ max 1 ( min ( div $phy_cpu 2 ) 8 ) | int }} +loose_xengine_max_background_compactions={{ max 1 (min ( div $phy_cpu 2 ) 12 ) | int }} +{{- end }} + + [client] port={{ $mysql_port }} socket=/var/run/mysqld/mysqld.sock \ No newline at end of file diff --git a/deploy/apecloud-mysql/templates/backuppolicytemplate.yaml b/deploy/apecloud-mysql/templates/backuppolicytemplate.yaml index f23c990ff..8a21fb0d8 100644 --- a/deploy/apecloud-mysql/templates/backuppolicytemplate.yaml +++ b/deploy/apecloud-mysql/templates/backuppolicytemplate.yaml @@ -1,21 +1,33 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-mysql + name: apecloud-mysql-backup-policy-template labels: clusterdefinition.kubeblocks.io/name: apecloud-mysql {{- include "apecloud-mysql.labels" . | nindent 4 }} + annotations: + dataprotection.kubeblocks.io/is-default-policy-template: "true" spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: xtrabackup-mysql - ttl: 168h0m0s - hooks: - containerName: mysql - preCommands: - - "touch /data/mysql/data/.restore_new_cluster; sync" - postCommands: - - "rm -f /data/mysql/data/.restore_new_cluster; sync" - - credentialKeyword: - userKeyword: username - passwordKeyword: password + clusterDefinitionRef: apecloud-mysql + backupPolicies: + - componentDefRef: mysql + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * *" + datafile: + enable: false + cronExpression: "0 18 * * *" + snapshot: + hooks: + containerName: mysql + preCommands: + - "touch /data/mysql/data/.restore_new_cluster; sync" + postCommands: + - "rm -f /data/mysql/data/.restore_new_cluster; sync" + target: + role: leader + datafile: + backupToolName: xtrabackup-for-apecloud-mysql \ No newline at end of file diff --git a/deploy/apecloud-mysql/templates/backuppolicytemplateforhscale.yaml b/deploy/apecloud-mysql/templates/backuppolicytemplateforhscale.yaml new file mode 100644 index 000000000..b25685963 --- /dev/null +++ b/deploy/apecloud-mysql/templates/backuppolicytemplateforhscale.yaml @@ -0,0 +1,21 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: BackupPolicyTemplate +metadata: + name: apecloud-mysql-backup-policy-for-hscale + labels: + clusterdefinition.kubeblocks.io/name: apecloud-mysql + {{- include "apecloud-mysql.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: apecloud-mysql + identifier: hscale + backupPolicies: + - componentDefRef: mysql + snapshot: + hooks: + containerName: mysql + preCommands: + - "touch /data/mysql/data/.restore; sync" + postCommands: + - "rm -f /data/mysql/data/.restore; sync" + target: + role: leader \ No newline at end of file diff --git a/deploy/apecloud-mysql/templates/backuptool.yaml b/deploy/apecloud-mysql/templates/backuptool.yaml index 5b0cd01c4..8729c836a 100644 --- a/deploy/apecloud-mysql/templates/backuptool.yaml +++ b/deploy/apecloud-mysql/templates/backuptool.yaml @@ -1,20 +1,13 @@ apiVersion: dataprotection.kubeblocks.io/v1alpha1 kind: BackupTool metadata: - name: xtrabackup-mysql + name: xtrabackup-for-apecloud-mysql labels: clusterdefinition.kubeblocks.io/name: apecloud-mysql {{- include "apecloud-mysql.labels" . | nindent 4 }} spec: image: registry.cn-hangzhou.aliyuncs.com/apecloud/percona-xtrabackup deployKind: job - resources: - limits: - cpu: "1" - memory: 2Gi - requests: - cpu: "1" - memory: 128Mi env: - name: DATA_DIR value: /data/mysql/data @@ -29,7 +22,7 @@ spec: exit 1 fi mkdir -p /tmp/data/ && cd /tmp/data - xbstream -x < /${BACKUP_DIR}/${BACKUP_NAME}.xbstream + xbstream -x < ${BACKUP_DIR}/${BACKUP_NAME}.xbstream xtrabackup --decompress --target-dir=/tmp/data/ xtrabackup --prepare --target-dir=/tmp/data/ find . -name "*.qp"|xargs rm -f @@ -42,5 +35,8 @@ spec: restoreCommands: [] incrementalRestoreCommands: [] backupCommands: - - xtrabackup --compress --backup --safe-slave-backup --slave-info --stream=xbstream --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} --datadir=${DATA_DIR} > /${BACKUP_DIR}/${BACKUP_NAME}.xbstream + - | + set -e + mkdir -p ${BACKUP_DIR} + xtrabackup --compress --backup --safe-slave-backup --slave-info --stream=xbstream --host=${DB_HOST} --user=${DB_USER} --password=${DB_PASSWORD} --datadir=${DATA_DIR} > ${BACKUP_DIR}/${BACKUP_NAME}.xbstream incrementalBackupCommands: [] diff --git a/deploy/apecloud-mysql/templates/class.yaml b/deploy/apecloud-mysql/templates/class.yaml index af0db9a7c..301bc1f46 100644 --- a/deploy/apecloud-mysql/templates/class.yaml +++ b/deploy/apecloud-mysql/templates/class.yaml @@ -1,63 +1,53 @@ -apiVersion: v1 -kind: ConfigMap +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ComponentClassDefinition metadata: name: kb.classes.default.apecloud-mysql.mysql labels: - class.kubeblocks.io/level: component class.kubeblocks.io/provider: kubeblocks apps.kubeblocks.io/component-def-ref: mysql clusterdefinition.kubeblocks.io/name: apecloud-mysql -data: - families-20230223162700: | - - family: kb-class-family-general - template: | - cpu: {{ printf "{{ or .cpu 1 }}" }} - memory: {{ printf "{{ or .memory 4 }}Gi" }} - storage: - - name: data - size: {{ printf "{{ or .dataStorageSize 10 }}Gi" }} - - name: log - size: {{ printf "{{ or .logStorageSize 1 }}Gi" }} - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - name: {{ printf "general-{{ .cpu }}c{{ .memory }}g" }} - classes: - - args: [1, 1, 100, 10] - - args: [2, 2, 100, 10] - - args: [2, 4, 100, 10] - - args: [2, 8, 100, 10] - - args: [4, 16, 100, 10] - - args: [8, 32, 100, 10] - - args: [16, 64, 200, 10] - - args: [32, 128, 200, 10] - - args: [64, 256, 200, 10] - - args: [128, 512, 200, 10] +spec: + groups: + - resourceConstraintRef: kb-resource-constraint-general + template: | + cpu: {{ printf "{{ .cpu }}" }} + memory: {{ printf "{{ .memory }}Gi" }} + vars: [ cpu, memory] + series: + - namingTemplate: {{ printf "general-{{ .cpu }}c{{ .memory }}g" }} + classes: + - args: [ "0.5", "0.5"] + - args: [ "1", "1"] + - args: [ "2", "2"] + - args: [ "2", "4"] + - args: [ "2", "8"] + - args: [ "4", "16"] + - args: [ "8", "32"] + - args: [ "16", "64"] + - args: [ "32", "128"] + - args: [ "64", "256"] + - args: [ "128", "512"] - - family: kb-class-family-memory-optimized - template: | - cpu: {{ printf "{{ or .cpu 1 }}" }} - memory: {{ printf "{{ or .memory 8 }}Gi" }} - storage: - - name: data - size: {{ printf "{{ or .dataStorageSize 10 }}Gi" }} - - name: log - size: {{ printf "{{ or .logStorageSize 1 }}Gi" }} - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - name: {{ printf "mo-{{ .cpu }}c{{ .memory }}g" }} - classes: - # 1:8 - - args: [2, 16, 100, 10] - - args: [4, 32, 100, 10] - - args: [8, 64, 100, 10] - - args: [12, 96, 100, 10] - - args: [24, 192, 200, 10] - - args: [48, 384, 200, 10] - # 1:16 - - args: [2, 32, 100, 10] - - args: [4, 64, 100, 10] - - args: [8, 128, 100, 10] - - args: [16, 256, 100, 10] - - args: [32, 512, 200, 10] - - args: [48, 768, 200, 10] - - args: [64, 1024, 200, 10] \ No newline at end of file + - resourceConstraintRef: kb-resource-constraint-memory-optimized + template: | + cpu: {{ printf "{{ .cpu }}" }} + memory: {{ printf "{{ .memory }}Gi" }} + vars: [ cpu, memory] + series: + - namingTemplate: {{ printf "mo-{{ .cpu }}c{{ .memory }}g" }} + classes: + # 1:8 + - args: [ "2", "16"] + - args: [ "4", "32"] + - args: [ "8", "64"] + - args: [ "12", "96"] + - args: [ "24", "192"] + - args: [ "48", "384"] + # 1:16 + - args: [ "2", "32"] + - args: [ "4", "64"] + - args: [ "8", "128"] + - args: [ "16", "256"] + - args: [ "32", "512"] + - args: [ "48", "768"] + - args: [ "64", "1024"] diff --git a/deploy/apecloud-mysql/templates/clusterdefinition.yaml b/deploy/apecloud-mysql/templates/clusterdefinition.yaml index a7fca264e..791e196e3 100644 --- a/deploy/apecloud-mysql/templates/clusterdefinition.yaml +++ b/deploy/apecloud-mysql/templates/clusterdefinition.yaml @@ -16,10 +16,10 @@ spec: - name: mysql characterType: mysql probes: - roleChangedProbe: - failureThreshold: {{ .Values.roleChangedProbe.failureThreshold }} - periodSeconds: {{ .Values.roleChangedProbe.periodSeconds }} - timeoutSeconds: {{ .Values.roleChangedProbe.timeoutSeconds }} + roleProbe: + failureThreshold: {{ .Values.roleProbe.failureThreshold }} + periodSeconds: {{ .Values.roleProbe.periodSeconds }} + timeoutSeconds: {{ .Values.roleProbe.timeoutSeconds }} monitor: builtIn: false exporterConfig: @@ -60,8 +60,7 @@ spec: targetPort: mysql horizontalScalePolicy: type: Snapshot - backupTemplateSelector: - "clusterdefinition.kubeblocks.io/name": apecloud-mysql + backupPolicyTemplateName: apecloud-mysql-backup-policy-for-hscale volumeTypes: - name: data type: data @@ -91,11 +90,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: MYSQL_DATABASE value: {{- if .Values.auth.createDatabase }} {{ .Values.auth.database | quote }} {{- else }} "" {{- end }} - name: MYSQL_USER @@ -116,6 +117,33 @@ spec: value: {{ if .Values.cluster.customConfig }}{{ .Values.cluster.customConfig }}{{ end }} - name: MYSQL_DYNAMIC_CONFIG value: {{ if .Values.cluster.dynamicConfig }}{{ .Values.cluster.dynamicConfig }}{{ end }} + - name: KB_EMBEDDED_WESQL + value: {{ .Values.cluster.kbWeSQLImage | default "1" | quote }} + # - name: KB_MYSQL_LEADER + # valueFrom: + # configMapKeyRef: + # name: $(COMP_ENV_CM_NAME) + # key: KB_LEADER + # optional: false + # - name: KB_MYSQL_FOLLOWERS + # valueFrom: + # configMapKeyRef: + # name: $(COMP_ENV_CM_NAME) + # key: KB_FOLLOWERS + # optional: false + # - name: KB_MYSQL_N + # valueFrom: + # configMapKeyRef: + # name: $(COMP_ENV_CM_NAME) + # key: KB_REPLICA_COUNT + # optional: false + # - name: KB_MYSQL_CLUSTER_UID + # valueFrom: + # configMapKeyRef: + # name: $(COMP_ENV_CM_NAME) + # key: KB_CLUSTER_UID + # optional: false + command: ["/scripts/setup.sh"] lifecycle: preStop: @@ -133,11 +161,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: DATA_SOURCE_NAME value: "$(MYSQL_USER):$(MYSQL_PASSWORD)@(localhost:3306)/" command: @@ -171,6 +201,9 @@ spec: - path: "leader" fieldRef: fieldPath: metadata.annotations['cs.apps.kubeblocks.io/leader'] + - path: "component-replicas" + fieldRef: + fieldPath: metadata.annotations['apps.kubeblocks.io/component-replicas'] systemAccounts: cmdExecutorConfig: image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} @@ -188,11 +221,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: MYSQL_ROOT_PASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false passwordConfig: length: 10 numDigits: 5 @@ -204,33 +239,28 @@ spec: type: CreateByStmt scope: AnyPods statements: - creation: CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT ALL PRIVILEGES ON *.* TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + creation: CREATE USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT ALL PRIVILEGES ON *.* TO $(USERNAME); + update: ALTER USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; - name: kbdataprotection provisionPolicy: type: CreateByStmt scope: AnyPods statements: - creation: CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY '$(PASSWD)';GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO $(USERNAME); GRANT LOCK TABLES,RELOAD,PROCESS,REPLICATION CLIENT, SUPER,SELECT,EVENT,TRIGGER,SHOW VIEW ON *.* TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + creation: CREATE USER $(USERNAME) IDENTIFIED BY '$(PASSWD)';GRANT RELOAD, LOCK TABLES, PROCESS, REPLICATION CLIENT ON *.* TO $(USERNAME); GRANT LOCK TABLES,RELOAD,PROCESS,REPLICATION CLIENT, SUPER,SELECT,EVENT,TRIGGER,SHOW VIEW ON *.* TO $(USERNAME); + update: ALTER USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; - name: kbprobe - provisionPolicy: + provisionPolicy: &kbReadonlyAcctRef type: CreateByStmt scope: AnyPods - statements: - creation: CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT REPLICATION CLIENT, PROCESS ON *.* TO $(USERNAME); GRANT SELECT ON performance_schema.* TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + statements: + creation: CREATE USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT REPLICATION CLIENT, PROCESS ON *.* TO $(USERNAME); GRANT SELECT ON performance_schema.* TO $(USERNAME); + update: ALTER USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; - name: kbmonitoring - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT REPLICATION CLIENT, PROCESS ON *.* TO $(USERNAME); GRANT SELECT ON performance_schema.* TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + provisionPolicy: *kbReadonlyAcctRef - name: kbreplicator provisionPolicy: type: CreateByStmt scope: AnyPods - statements: - creation: CREATE USER IF NOT EXISTS $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT REPLICATION SLAVE ON *.* TO $(USERNAME) WITH GRANT OPTION; - deletion: DROP USER IF EXISTS $(USERNAME); \ No newline at end of file + statements: + creation: CREATE USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; GRANT REPLICATION SLAVE ON *.* TO $(USERNAME) WITH GRANT OPTION; + update: ALTER USER $(USERNAME) IDENTIFIED BY '$(PASSWD)'; diff --git a/deploy/apecloud-mysql/templates/clusterversion.yaml b/deploy/apecloud-mysql/templates/clusterversion.yaml index 22c92bc33..22b8b7f5c 100644 --- a/deploy/apecloud-mysql/templates/clusterversion.yaml +++ b/deploy/apecloud-mysql/templates/clusterversion.yaml @@ -13,3 +13,6 @@ spec: - name: mysql image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + systemAccountSpec: + cmdExecutorConfig: + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} \ No newline at end of file diff --git a/deploy/apecloud-mysql/templates/scripts.yaml b/deploy/apecloud-mysql/templates/scripts.yaml index 8c46f63a2..bd230e934 100644 --- a/deploy/apecloud-mysql/templates/scripts.yaml +++ b/deploy/apecloud-mysql/templates/scripts.yaml @@ -7,161 +7,113 @@ metadata: data: setup.sh: | #!/bin/bash - leader=$KB_MYSQL_LEADER - followers=$KB_MYSQL_FOLLOWERS - echo "leader=$leader" - echo "followers=$followers" - sub_follower=`echo "$followers" | grep "$KB_POD_NAME"` - echo "KB_POD_NAME=$KB_POD_NAME" - echo "sub_follower=$sub_follower" - if [ -z "$leader" -o "$KB_POD_NAME" = "$leader" -o ! -z "$sub_follower" ]; then - echo "no need to call add" - else - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - echo "host=$host" - leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) - if [ ! -z $leader_host ]; then - host_flag="-h$leader_host" - fi - if [ ! -z $MYSQL_ROOT_PASSWORD ]; then - password_flag="-p$MYSQL_ROOT_PASSWORD" - fi - echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.add_learner('$host:13306');\" >> /tmp/setup_error.log 2>&1 " - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.add_learner('$host:13306');" >> /tmp/setup_error.log 2>&1 - code=$? - echo "exit code: $code" - if [ $code -ne 0 ]; then - cat /tmp/setup_error.log - already_exists=`cat /tmp/setup_error.log | grep "Target node already exists"` - if [ -z "$already_exists" ]; then - exit $code - fi - fi - /scripts/upgrade-learner.sh & - fi - cluster_info=""; - for (( i=0; i< $KB_MYSQL_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_MYSQL_"$i"_HOSTNAME) - # setup pod weight, prefer pod 0 to be leader - if [ $i -eq 0 ]; then - cluster_info="$cluster_info$host:13306#9N"; - else - cluster_info="$cluster_info$host:13306#1N"; - fi - done; - idx=${KB_POD_NAME##*-} - echo $idx - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - cluster_info="$cluster_info@$(($idx+1))"; - echo "cluster_info=$cluster_info"; - mkdir -p /data/mysql/data; - mkdir -p /data/mysql/log; - chmod +777 -R /data/mysql; - echo "KB_MYSQL_RECREATE=$KB_MYSQL_RECREATE" - if [ "$KB_MYSQL_RECREATE" == "true" ]; then - echo "recreate from existing volumes, touch /data/mysql/data/.resetup_db" - touch /data/mysql/data/.resetup_db - fi - if [ -z $leader ] || [ ! -f "/data/mysql/data/.restore" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$cluster_info\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$cluster_info" --cluster-id=$CLUSTER_ID - elif [ "$KB_POD_NAME" != "$leader" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306" --cluster-id=$CLUSTER_ID - else - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306@1\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306@1" --cluster-id=$CLUSTER_ID - fi - upgrade-learner.sh: | - #!/bin/bash - leader=$KB_MYSQL_LEADER - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) - if [ ! -z $leader_host ]; then - host_flag="-h$leader_host" - fi - if [ ! -z $MYSQL_ROOT_PASSWORD ]; then - password_flag="-p$MYSQL_ROOT_PASSWORD" - fi - while true - do - sleep 5 - mysql -uroot $password_flag -e "select ROLE from information_schema.wesql_cluster_local" > /tmp/role.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/role.log >> /tmp/upgrade-learner.log - else - role=`cat /tmp/role.log` - echo "role: $role" >> /tmp/upgrade-learner.log - if [ -z "$role" ]; then - echo "cannot get role" >> /tmp/upgrade-learner.log - else - break - fi - fi - else - kill -9 $pid - echo "mysql timeout" >> /tmp/upgrade-learner.log - fi - done - grep_learner=`echo $role | grep "Learner"` - echo "grep learner: $grep_learner" >> /tmp/upgrade-learner.log - if [ -z "$grep_learner" ]; then - exit 0 - fi - while true - do - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.upgrade_learner('$host:13306');" >> /tmp/upgrade.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/upgrade.log >> /tmp/upgrade-learner.log - already_exists=`cat /tmp/upgrade.log | grep "Target node already exists"` - if [ ! -z "$already_exists" ]; then - break - fi - else - break - fi - else - kill -9 $pid - echo "mysql call leader timeout" >> /tmp/upgrade-learner.log - fi - sleep 5 - done + exec docker-entrypoint.sh pre-stop.sh: | #!/bin/bash - leader=`cat /etc/annotations/leader` - echo "leader=$leader" - echo "KB_POD_NAME=$KB_POD_NAME" + drop_followers() { + echo "leader=$leader" >> /data/mysql/.kb_pre_stop.log + echo "KB_POD_NAME=$KB_POD_NAME" >> /data/mysql/.kb_pre_stop.log if [ -z "$leader" -o "$KB_POD_NAME" = "$leader" ]; then - echo "no leader or self is leader, exit" + echo "no leader or self is leader, exit" >> /data/mysql/.kb_pre_stop.log exit 0 fi - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - echo "host=$host" + host=$(eval echo \$KB_"$idx"_HOSTNAME) + echo "host=$host" >> /data/mysql/.kb_pre_stop.log leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) + leader_host=$(eval echo \$KB_"$leader_idx"_HOSTNAME) if [ ! -z $leader_host ]; then host_flag="-h$leader_host" fi if [ ! -z $MYSQL_ROOT_PASSWORD ]; then password_flag="-p$MYSQL_ROOT_PASSWORD" fi - echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.downgrade_follower('$host:13306');\" 2>&1 " + echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.downgrade_follower('$host:13306');\" 2>&1 " >> /data/mysql/.kb_pre_stop.log mysql $host_flag -uroot $password_flag -e "call dbms_consensus.downgrade_follower('$host:13306');" 2>&1 - echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.drop_learner('$host:13306');\" 2>&1 " + echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.drop_learner('$host:13306');\" 2>&1 " >> /data/mysql/.kb_pre_stop.log mysql $host_flag -uroot $password_flag -e "call dbms_consensus.drop_learner('$host:13306');" 2>&1 + } + + set_my_weight_to_zero() { + if [ ! -z $MYSQL_ROOT_PASSWORD ]; then + password_flag="-p$MYSQL_ROOT_PASSWORD" + fi + + if [ "$KB_POD_NAME" = "$leader" ]; then + echo "self is leader, before scale in, need to set my election weight to 0." >> /data/mysql/.kb_pre_stop.log + host=$(eval echo \$KB_"$idx"_HOSTNAME) + echo "set weight to 0. mysql -uroot $password_flag -e \"call dbms_consensus.configure_follower('$host:13306',0 ,false);\" 2>&1" >> /data/mysql/.kb_pre_stop.log + mysql -uroot $password_flag -e "call dbms_consensus.configure_follower('$host:13306',0 ,false);" 2>&1 + fi + } + + switchover() { + if [ ! -z $MYSQL_ROOT_PASSWORD ]; then + password_flag="-p$MYSQL_ROOT_PASSWORD" + fi + #new_leader_host=$KB_0_HOSTNAME + if [ "$KB_POD_NAME" = "$leader" ]; then + echo "self is leader, need to switchover" >> /data/mysql/.kb_pre_stop.log + echo "try to get global cluster info" >> /data/mysql/.kb_pre_stop.log + global_info=`mysql -uroot $password_flag 2>/dev/null -e "select IP_PORT from information_schema.wesql_cluster_global order by MATCH_INDEX desc;"` + echo "all nodes: $global_info" >> /data/mysql/.kb_pre_stop.log + global_info_arr=($global_info) + echo "all nodes arrary: ${global_info_arr[0]},${global_info_arr[1]},${global_info_arr[2]},${global_info_arr[3]}" >> /data/mysql/.kb_pre_stop.log + echo "array size: ${#global_info_arr[@]}, the first one is not real address,just the field name IP_PORT" >> /data/mysql/.kb_pre_stop.log + + host=$(eval echo \$KB_"$idx"_HOSTNAME) + host_ip_port=$host:13306 + try_times=10 + for((i=1;i<${#global_info_arr[@]};i++)) do + if [ "$host_ip_port" == "${global_info_arr[i]}" ];then + echo "do not transfer to leader, leader:${global_info_arr[i]}" >> /data/mysql/.kb_pre_stop.log; + else + echo "try to transfer to:${global_info_arr[i]}" >> /data/mysql/.kb_pre_stop.log; + echo "mysql -uroot $password_flag -e \"call dbms_consensus.change_leader('${global_info_arr[i]}');\" 2>&1" >> /data/mysql/.kb_pre_stop.log + mysql -uroot $password_flag -e "call dbms_consensus.change_leader('${global_info_arr[i]}');" 2>&1 + sleep 1 + role_info=`mysql -uroot $password_flag 2>/dev/null -e "select ROLE from information_schema.wesql_cluster_local;"` + role_info_arr=($role_info) + real_role=${role_info_arr[1]} + echo "this node's current role info:$real_role" >> /data/mysql/.kb_pre_stop.log + if [ "$real_role" == "Follower" ];then + echo "transfer successfully" >> /data/mysql/.kb_pre_stop.log + new_leader_host_and_port=${global_info_arr[i]} + # get rid of port + new_leader_host=${new_leader_host_and_port%%:*} + echo "new_leader_host=$new_leader_host" >> /data/mysql/.kb_pre_stop.log + leader=`echo "$new_leader_host" | cut -d "." -f 1` + echo "leader_host: $leader" >> /data/mysql/.kb_pre_stop.log + idx=${KB_POD_NAME##*-} + break + fi + fi + ((try_times--)) + if [ $try_times -le 0 ];then + echo "try too many times" >> /data/mysql/.kb_pre_stop.log + break + fi + done + fi + } + leader=`cat /etc/annotations/leader` + idx=${KB_POD_NAME##*-} + current_component_replicas=`cat /etc/annotations/component-replicas` + echo "current replicas: $current_component_replicas" >> /data/mysql/.kb_pre_stop.log + if [ ! $idx -lt $current_component_replicas ] && [ $current_component_replicas -ne 0 ]; then + # if idx greater than or equal to current_component_replicas means the cluster's scaling in + # put .restore on pvc for next scaling out, if pvc not deleted + touch /data/mysql/data/.restore; sync + # set wegiht to 0 and switch leader before leader scaling in itself + set_my_weight_to_zero + switchover + # only scaling in need to drop followers + drop_followers + elif [ $current_component_replicas -eq 0 ]; then + # stop, do nothing. + echo "stop, do nothing" >> /data/mysql/.kb_pre_stop.log + else + # restart, switchover first. + echo "Also try to switchover just before restart" >> /data/mysql/.kb_pre_stop.log + switchover + echo "no need to drop followers" >> /data/mysql/.kb_pre_stop.log + fi diff --git a/deploy/apecloud-mysql/values.yaml b/deploy/apecloud-mysql/values.yaml index 1e1e28e9b..cc492dcc5 100644 --- a/deploy/apecloud-mysql/values.yaml +++ b/deploy/apecloud-mysql/values.yaml @@ -7,7 +7,7 @@ image: repository: apecloud/apecloud-mysql-server pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: 8.0.30-5.alpha5.20230319.g28f261a.5 + tag: 8.0.30-5.alpha8.20230523.g3e93ae7.8 ## MySQL Cluster parameters cluster: @@ -23,6 +23,8 @@ cluster: customConfig: ## MYSQL_DYNAMIC_CONFIG dynamicConfig: + ## KB_EMBEDDED_WESQL + kbWeSQLImage: "1" ## MySQL Authentication parameters auth: @@ -61,7 +63,7 @@ logConfigs: slow: /data/mysql/log/mysqld-slowquery.log general: /data/mysql/log/mysqld.log -roleChangedProbe: +roleProbe: failureThreshold: 2 periodSeconds: 1 timeoutSeconds: 1 @@ -71,4 +73,4 @@ metrics: registry: registry.cn-hangzhou.aliyuncs.com repository: apecloud/mysqld-exporter tag: 0.14.1 - pullPolicy: IfNotPresent \ No newline at end of file + pullPolicy: IfNotPresent diff --git a/deploy/chatgpt-retrieval-plugin/Chart.yaml b/deploy/chatgpt-retrieval-plugin/Chart.yaml index 347c5625d..e281e6f3e 100644 --- a/deploy/chatgpt-retrieval-plugin/Chart.yaml +++ b/deploy/chatgpt-retrieval-plugin/Chart.yaml @@ -5,7 +5,7 @@ description: A demo application for ChatGPT plugin. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: 0.1.0 diff --git a/deploy/chatgpt-retrieval-plugin/templates/clusterrole.yaml b/deploy/chatgpt-retrieval-plugin/templates/clusterrole.yaml index 83b941682..d51e76abf 100644 --- a/deploy/chatgpt-retrieval-plugin/templates/clusterrole.yaml +++ b/deploy/chatgpt-retrieval-plugin/templates/clusterrole.yaml @@ -2,6 +2,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "gptplugin.fullname" . }} + labels: + {{- include "gptplugin.labels" . | nindent 4 }} rules: - apiGroups: [""] resources: ["services", "pods", "secrets"] diff --git a/deploy/chatgpt-retrieval-plugin/templates/clusterrolebinding.yaml b/deploy/chatgpt-retrieval-plugin/templates/clusterrolebinding.yaml index e0eca0c92..ea56af650 100644 --- a/deploy/chatgpt-retrieval-plugin/templates/clusterrolebinding.yaml +++ b/deploy/chatgpt-retrieval-plugin/templates/clusterrolebinding.yaml @@ -2,6 +2,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "gptplugin.fullname" . }} + labels: + {{- include "gptplugin.labels" . | nindent 4 }} subjects: - kind: ServiceAccount name: {{ include "gptplugin.serviceAccountName" . }} diff --git a/deploy/chatgpt-retrieval-plugin/templates/configmap.yaml b/deploy/chatgpt-retrieval-plugin/templates/configmap.yaml index aa8bb62a0..618647163 100644 --- a/deploy/chatgpt-retrieval-plugin/templates/configmap.yaml +++ b/deploy/chatgpt-retrieval-plugin/templates/configmap.yaml @@ -24,8 +24,8 @@ data: "has_user_authentication": false }, "logo_url": "{{- .Values.website.logo_url | default "https://your-app-url.com/.well-known/logo.png"}}", - "contact_email": "{{- .Values.website.contact_email | default "hello@contact.com"}}", - "legal_info_url": "{{- .Values.website.legal_info_url | default "hello@legal.com"}}" + "contact_email": "{{- .Values.website.contact_email | default "admin@kubeblocks.io"}}", + "legal_info_url": "{{- .Values.website.legal_info_url | default "admin@kubeblocks.io"}}" } openapi.yaml: |- openapi: 3.0.2 diff --git a/deploy/chatgpt-retrieval-plugin/templates/deployment.yaml b/deploy/chatgpt-retrieval-plugin/templates/deployment.yaml index f3e250980..62aef4c49 100644 --- a/deploy/chatgpt-retrieval-plugin/templates/deployment.yaml +++ b/deploy/chatgpt-retrieval-plugin/templates/deployment.yaml @@ -44,14 +44,6 @@ spec: - name: http containerPort: 8080 protocol: TCP - livenessProbe: - httpGet: - path: /docs - port: http - readinessProbe: - httpGet: - path: /docs - port: http resources: {{- toYaml .Values.resources | nindent 12 }} env: @@ -129,6 +121,54 @@ spec: value: {{.Values.datastore.REDIS_DISTANCE_METRIC | default "COSINE" | quote}} - name: REDIS_INDEX_TYPE value: {{.Values.datastore.REDIS_INDEX_TYPE | default "FLAT" | quote}} + - name: LLAMA_INDEX_TYPE + value: {{.Values.datastore.LLAMA_INDEX_TYPE | default "simple_dict" | quote}} + - name: LLAMA_INDEX_JSON_PATH + value: {{.Values.datastore.LLAMA_INDEX_JSON_PATH | default | quote}} + - name: LLAMA_QUERY_KWARGS_JSON_PATH + value: {{.Values.datastore.LLAMA_QUERY_KWARGS_JSON_PATH | default | quote}} + - name: LLAMA_RESPONSE_MODE + value: {{.Values.datastore.LLAMA_RESPONSE_MODE | default "no_text" | quote}} + - name: CHROMA_COLLECTION + value: {{.Values.datastore.CHROMA_COLLECTION | default "openaiembeddings" | quote}} + - name: CHROMA_IN_MEMORY + value: {{.Values.datastore.CHROMA_IN_MEMORY | default "True" | quote}} + - name: CHROMA_PERSISTENCE_DIR + value: {{.Values.datastore.CHROMA_PERSISTENCE_DIR | default "openai" | quote}} + - name: CHROMA_HOST + value: {{.Values.datastore.CHROMA_HOST | default "http://127.0.0.1" | quote}} + - name: CHROMA_PORT + value: {{.Values.datastore.CHROMA_PORT | default "8080" | quote}} + - name: AZURESEARCH_SERVICE + value: {{.Values.datastore.AZURESEARCH_SERVICE | default | quote}} + - name: AZURESEARCH_INDEX + value: {{.Values.datastore.AZURESEARCH_INDEX | default | quote}} + - name: AZURESEARCH_API_KEY + value: {{.Values.datastore.AZURESEARCH_API_KEY | default | quote}} + - name: AZURESEARCH_DISABLE_HYBRID + value: {{.Values.datastore.AZURESEARCH_DISABLE_HYBRID | default | quote}} + - name: AZURESEARCH_SEMANTIC_CONFIG + value: {{.Values.datastore.AZURESEARCH_SEMANTIC_CONFIG | default | quote}} + - name: AZURESEARCH_LANGUAGE + value: {{.Values.datastore.AZURESEARCH_LANGUAGE | default "en-us" | quote}} + - name: AZURESEARCH_DIMENSIONS + value: {{.Values.datastore.AZURESEARCH_DIMENSIONS | default "1536" | quote}} + - name: SUPABASE_URL + value: {{.Values.datastore.SUPABASE_URL | default | quote}} + - name: SUPABASE_ANON_KEY + value: {{.Values.datastore.SUPABASE_ANON_KEY | default | quote}} + - name: SUPABASE_SERVICE_ROLE_KEY + value: {{.Values.datastore.SUPABASE_SERVICE_ROLE_KEY | default | quote}} + - name: PG_HOST + value: {{.Values.datastore.PG_HOST | default "localhost" | quote}} + - name: PG_PORT + value: {{.Values.datastore.PG_PORT | default "5432" | quote}} + - name: PG_PASSWORD + value: {{.Values.datastore.PG_PASSWORD | default "postgres" | quote}} + - name: PG_USER + value: {{.Values.datastore.PG_USER | default "postgres" | quote}} + - name: PG_DB + value: {{.Values.datastore.PG_DB | default "postgres" | quote}} {{- with .Values.nodeSelector }} nodeSelector: {{- toYaml . | nindent 8 }} diff --git a/deploy/chatgpt-retrieval-plugin/values.yaml b/deploy/chatgpt-retrieval-plugin/values.yaml index 282dbf940..cb049fe5a 100644 --- a/deploy/chatgpt-retrieval-plugin/values.yaml +++ b/deploy/chatgpt-retrieval-plugin/values.yaml @@ -9,7 +9,7 @@ image: repository: apecloud/chatgpt-retrieval-plugin pullPolicy: IfNotPresent # Overrides the image tag whose default is the chart appVersion. - tag: "arm64-latest" + tag: "0.1.0" imagePullSecrets: [] nameOverride: "" @@ -47,11 +47,11 @@ servers: website: url: https://your-app-url.com/.well-known/openapi.yaml logo_url: https://your-app-url.com/.well-known/logo.png - contact_email: hello@contact.com - legal_info_url: hello@legal.com + contact_email: admin@kubeblocks.io + legal_info_url: admin@kubeblocks.io datastore: - # in list of (pinecone, weaviate, zilliz, milvus, qdrant, redis) + # in list of (pinecone, weaviate, zilliz, milvus, qdrant, redis, llama, chroma, azuresearch, supabase, postgres) DATASTORE: # Yes Your secret token to protect the local plugin API BEARER_TOKEN: @@ -132,6 +132,59 @@ datastore: # Optional Vector index algorithm type default:FLAT REDIS_INDEX_TYPE: FLAT + # Optional Index type (see below for details) + LLAMA_INDEX_TYPE: simple_dict + # Optional Path to saved Index json file + LLAMA_INDEX_JSON_PATH: + # Optional Path to saved query kwargs json file + LLAMA_QUERY_KWARGS_JSON_PATH: + # Optional Response mode for query + LLAMA_RESPONSE_MODE: no_text + + # Optional Your chosen Chroma collection name to store your embeddings + CHROMA_COLLECTION: openaiembeddings + # Optional If set to True, ignore CHROMA_HOST and CHROMA_PORT and just use an in-memory Chroma instance + CHROMA_IN_MEMORY: True + # Optional If set, and CHROMA_IN_MEMORY is set, persist to and load from this directory. + CHROMA_PERSISTENCE_DIR: openai + # Optional Your Chroma instance host address (see notes below) + CHROMA_HOST: http://127.0.0.1 + # Optional Your Chroma port number + CHROMA_PORT: 8000 + + # Required Name of your search service + AZURESEARCH_SERVICE: + # Required Name of your search index + AZURESEARCH_INDEX: + # Optional Your API key, if using key-based auth instead of Azure managed identity + AZURESEARCH_API_KEY: + # Optional Disable hybrid search and only use vector similarity + AZURESEARCH_DISABLE_HYBRID: + # Optional Enable L2 re-ranking with this configuration name see re-ranking below + AZURESEARCH_SEMANTIC_CONFIG: + # Optional If using L2 re-ranking, language for queries/documents (valid values listed here) + AZURESEARCH_LANGUAGE: en-us + # Optional Vector size for embeddings + AZURESEARCH_DIMENSIONS: 1536 + + # Required Supabase Project URL + SUPABASE_URL: + # Optional Supabase Project API anon key + SUPABASE_ANON_KEY: + # Optional Supabase Project API service key, will be used if provided instead of anon key + SUPABASE_SERVICE_ROLE_KEY: + + # Optional Postgres host + PG_HOST: localhost + # Optional Postgres port + PG_PORT: 5432 + # Optional Postgres password + PG_PASSWORD: postgres + # Optional Postgres username + PG_USER: postgres + # Optional Postgres database + PG_DB: postgres + resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little diff --git a/deploy/clickhouse-cluster/Chart.yaml b/deploy/clickhouse-cluster/Chart.yaml index 5e3e0cdba..801000972 100644 --- a/deploy/clickhouse-cluster/Chart.yaml +++ b/deploy/clickhouse-cluster/Chart.yaml @@ -4,7 +4,7 @@ description: A ClickHouse cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: 22.9.4 diff --git a/deploy/clickhouse-cluster/templates/_helpers.tpl b/deploy/clickhouse-cluster/templates/_helpers.tpl index ff4117fb0..dacc31da8 100644 --- a/deploy/clickhouse-cluster/templates/_helpers.tpl +++ b/deploy/clickhouse-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "clickhouse-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "clickhouse-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "clickhouse-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "clickhouse-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/clickhouse-cluster/templates/cluster.yaml b/deploy/clickhouse-cluster/templates/cluster.yaml index 43c9dca96..8b74a0ca6 100644 --- a/deploy/clickhouse-cluster/templates/cluster.yaml +++ b/deploy/clickhouse-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "clickhouse-cluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: clickhouse # ref clusterdefinition.name @@ -22,6 +22,7 @@ spec: monitor: {{ $.Values.monitor.enabled }} serviceType: {{ $.Values.service.type | default "ClusterIP" }} replicas: {{ .replicaCount | default 2 }} + serviceAccountName: {{ include "clickhouse-cluster.serviceAccountName" $ }} {{- with .tolerations }} tolerations: {{ .| toYaml | nindent 8 }} {{- end }} diff --git a/deploy/clickhouse-cluster/templates/role.yaml b/deploy/clickhouse-cluster/templates/role.yaml new file mode 100644 index 000000000..a8e7080b8 --- /dev/null +++ b/deploy/clickhouse-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "clickhouse-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/clickhouse-cluster/templates/rolebinding.yaml b/deploy/clickhouse-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..86036ac66 --- /dev/null +++ b/deploy/clickhouse-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "clickhouse-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "clickhouse-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/clickhouse-cluster/templates/serviceaccount.yaml b/deploy/clickhouse-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..fd0953eb3 --- /dev/null +++ b/deploy/clickhouse-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "clickhouse-cluster.serviceAccountName" . }} + labels: + {{ include "clickhouse-cluster.labels" . | nindent 4 }} diff --git a/deploy/clickhouse-cluster/templates/tests/test-connection.yaml b/deploy/clickhouse-cluster/templates/tests/test-connection.yaml index c437bd44f..c08d4e47e 100644 --- a/deploy/clickhouse-cluster/templates/tests/test-connection.yaml +++ b/deploy/clickhouse-cluster/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "clickhouse-cluster.fullname" . }}-test-connection" + name: "{{ include "clustername" . }}-test-connection" labels: {{- include "clickhouse-cluster.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "clickhouse-cluster.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "clustername" . }}:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/deploy/clickhouse-cluster/values.yaml b/deploy/clickhouse-cluster/values.yaml index c00617590..4c8c57057 100644 --- a/deploy/clickhouse-cluster/values.yaml +++ b/deploy/clickhouse-cluster/values.yaml @@ -275,4 +275,11 @@ ingress: ## port: ## name: http ## - extraRules: [] \ No newline at end of file + extraRules: [] + +nameOverride: "" +fullnameOverride: "" + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/clickhouse/Chart.yaml b/deploy/clickhouse/Chart.yaml index d7c59ce9d..77018e4c2 100644 --- a/deploy/clickhouse/Chart.yaml +++ b/deploy/clickhouse/Chart.yaml @@ -9,7 +9,7 @@ annotations: type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: 22.9.4 diff --git a/deploy/clickhouse/configs/00_default_overrides.xml.tpl b/deploy/clickhouse/configs/00_default_overrides.xml.tpl new file mode 100644 index 000000000..6c6861071 --- /dev/null +++ b/deploy/clickhouse/configs/00_default_overrides.xml.tpl @@ -0,0 +1,58 @@ +{{- $clusterName := $.cluster.metadata.name }} +{{- $namespace := $.cluster.metadata.namespace }} + + + + + + {{ $clusterName }} + + + + information + + + + +{{- range $.cluster.spec.componentSpecs }} + {{ $compIter := . }} + {{- if eq $compIter.componentDefRef "clickhouse" }} + + {{- $replicas := $compIter.replicas | int }} + {{- range $i, $_e := until $replicas }} + + {{ $clusterName }}-{{ $compIter.name }}-{{ $i }}.{{ $clusterName }}-{{ $compIter.name }}-headless.{{ $namespace }}.svc + 9000 + + {{- end }} + + {{- end }} +{{- end }} + + +{{- range $.cluster.spec.componentSpecs }} + {{ $compIter := . }} + {{- if or (eq $compIter.componentDefRef "zookeeper") (eq $compIter.componentDefRef "ch-keeper") }} + + + {{- $replicas := $compIter.replicas | int }} + {{- range $i, $_e := until $replicas }} + + {{ $clusterName }}-{{ $compIter.name }}-{{ $i }}.{{ $clusterName }}-{{ $compIter.name }}-headless.{{ $namespace }}.svc + 2181 + + {{- end }} + + {{- end }} +{{- end }} +{{- if $.component.monitor.enable }} + + + /metrics + + true + true + true + +{{- end }} + diff --git a/deploy/clickhouse/configs/ch-keeper_00_default_overrides.xml.tpl b/deploy/clickhouse/configs/ch-keeper_00_default_overrides.xml.tpl new file mode 100644 index 000000000..3bc150f35 --- /dev/null +++ b/deploy/clickhouse/configs/ch-keeper_00_default_overrides.xml.tpl @@ -0,0 +1,36 @@ +{{- $clusterName := $.cluster.metadata.name }} +{{- $namespace := $.cluster.metadata.namespace }} + + 0.0.0.0 + + + 1 + /var/lib/clickhouse/coordination/log + /var/lib/clickhouse/coordination/snapshots + + 10000 + 30000 + warning + + +{{- $replicas := $.component.replicas | int }} +{{- range $i, $e := until $replicas }} + + {{ $i | int | add1 }} + {{ $clusterName }}-{{ $.component.name }}-{{ $i }}.{{ $clusterName }}-{{ $.component.name }}-headless.{{ $namespace }}.svc + + +{{- end }} + + +{{- if $.component.monitor.enable }} + + + /metrics + + true + true + true + +{{- end }} + \ No newline at end of file diff --git a/deploy/clickhouse/templates/clusterdefinition.yaml b/deploy/clickhouse/templates/clusterdefinition.yaml index 4fab723dc..0f2e2fa7d 100644 --- a/deploy/clickhouse/templates/clusterdefinition.yaml +++ b/deploy/clickhouse/templates/clusterdefinition.yaml @@ -87,6 +87,7 @@ spec: # just keeping the same secret keys as bitnami Clickhouse chart name: $(CONN_CREDENTIAL_SECRET_NAME) key: admin-password + optional: false - name: BITNAMI_DEBUG value: "false" - name: CLICKHOUSE_HTTP_PORT @@ -193,6 +194,7 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: admin-password + optional: false - name: BITNAMI_DEBUG value: "false" - name: CLICKHOUSE_KEEPER_TCP_PORT @@ -234,11 +236,11 @@ spec: workloadType: Stateful #Consensus characterType: zookeeper # probes: - # roleChangedProbe: + # roleProbe: # cmd: "stat | grep 'Leader'" - # failureThreshold: {{ .Values.zookeeper.roleChangedProbe.failureThreshold }} - # periodSeconds: {{ .Values.zookeeper.roleChangedProbe.periodSeconds }} - # successThreshold: {{ .Values.zookeeper.roleChangedProbe.successThreshold }} + # failureThreshold: {{ .Values.zookeeper.roleProbe.failureThreshold }} + # periodSeconds: {{ .Values.zookeeper.roleProbe.periodSeconds }} + # successThreshold: {{ .Values.zookeeper.roleProbe.successThreshold }} monitor: builtIn: false exporterConfig: diff --git a/deploy/clickhouse/templates/configmap.yaml b/deploy/clickhouse/templates/configmap.yaml index d075e14e6..6b2334c6c 100644 --- a/deploy/clickhouse/templates/configmap.yaml +++ b/deploy/clickhouse/templates/configmap.yaml @@ -11,7 +11,7 @@ metadata: {{- end }} data: 00_default_overrides.xml: | - {{- .Values.defaultConfigurationOverrides | nindent 4 }} + {{- .Files.Get "configs/00_default_overrides.xml.tpl" | nindent 4 }} --- apiVersion: v1 kind: ConfigMap @@ -26,7 +26,7 @@ metadata: {{- end }} data: 00_default_overrides.xml: | - {{- .Values.clickHouseKeeper.configuration | nindent 4 }} + {{- .Files.Get "configs/ch-keeper_00_default_overrides.xml.tpl" | nindent 4 }} --- {{- if .Values.zookeeper.configuration }} apiVersion: v1 diff --git a/deploy/clickhouse/values.yaml b/deploy/clickhouse/values.yaml index c610e4a0d..d7422c615 100644 --- a/deploy/clickhouse/values.yaml +++ b/deploy/clickhouse/values.yaml @@ -14,108 +14,6 @@ commonLabels: {} logConfigs: {} -## @param defaultConfigurationOverrides [string] Default configuration overrides (evaluated as a template) -## -defaultConfigurationOverrides: | - {{- $clusterName := $.cluster.metadata.name }} - {{- $namespace := $.cluster.metadata.namespace }} - - - - - - {{ $clusterName }} - - - - information - - - - - {{- range $.cluster.spec.components }} - {{ $compIter := . }} - {{- if eq $compIter.type "clickhouse" }} - - {{- $replicas := $compIter.replicas | int }} - {{- range $i, $_e := until $replicas }} - - {{ $clusterName }}-{{ $compIter.name }}-{{ $i }}.{{ $clusterName }}-{{ $compIter.name }}-headless.{{ $namespace }}.svc - 9000 - - {{- end }} - - {{- end }} - {{- end }} - - - {{- range $.cluster.spec.components }} - {{ $compIter := . }} - {{- if or (eq $compIter.type "zookeeper") (eq $compIter.type "ch-keeper") }} - - - {{- $replicas := $compIter.replicas | int }} - {{- range $i, $_e := until $replicas }} - - {{ $clusterName }}-{{ $compIter.name }}-{{ $i }}.{{ $clusterName }}-{{ $compIter.name }}-headless.{{ $namespace }}.svc - 2181 - - {{- end }} - - {{- end }} - {{- end }} - {{- if $.component.monitor.enable }} - - - /metrics - - true - true - true - - {{- end }} - - - -clickHouseKeeper: - configuration: | - {{- $clusterName := $.cluster.metadata.name }} - {{- $namespace := $.cluster.metadata.namespace }} - - 0.0.0.0 - - - 1 - /var/lib/clickhouse/coordination/log - /var/lib/clickhouse/coordination/snapshots - - 10000 - 30000 - warning - - - {{- $replicas := $.component.replicas | int }} - {{- range $i, $e := until $replicas }} - - {{ $i | int | add1 }} - {{ $clusterName }}-{{ $.component.name }}-{{ $i }}.{{ $clusterName }}-{{ $.component.name }}-headless.{{ $namespace }}.svc - - - {{- end }} - - - {{- if $.component.monitor.enable }} - - - /metrics - - true - true - true - - {{- end }} - - image: repository: docker.io/bitnami/clickhouse @@ -131,7 +29,7 @@ zookeeper: tag: 3.8.0-debian-11-r47 logConfigs: {} - roleChangedProbe: + roleProbe: failureThreshold: 2 periodSeconds: 1 successThreshold: 1 diff --git a/deploy/csi-s3/README.md b/deploy/csi-s3/README.md index 514bd5a23..505c32f43 100644 --- a/deploy/csi-s3/README.md +++ b/deploy/csi-s3/README.md @@ -21,17 +21,19 @@ to your [Yandex Object Storage](https://cloud.yandex.com/en-ru/services/storage) The following table lists all configuration parameters and their default values. -| Parameter | Description | Default | -| ---------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------ | -| `storageClass.create` | Specifies whether the storage class should be created | true | -| `storageClass.name` | Storage class name | csi-s3 | -| `storageClass.singleBucket` | Use a single bucket for all dynamically provisioned persistent volumes | | -| `storageClass.mounter` | Mounter to use. Either geesefs, s3fs or rclone. geesefs recommended | geesefs | -| `storageClass.mountOptions` | GeeseFS mount options | `--memory-limit 1000 --dir-mode 0777 --file-mode 0666` | -| `storageClass.reclaimPolicy` | Volume reclaim policy | Delete | -| `storageClass.annotations` | Annotations for the storage class | | -| `secret.create` | Specifies whether the secret should be created | true | -| `secret.name` | Name of the secret | csi-s3-secret | -| `secret.accessKey` | S3 Access Key | | -| `secret.secretKey` | S3 Secret Key | | -| `secret.endpoint` | Endpoint | https://storage.yandexcloud.net | +| Parameter | Description | Default | +|------------------------------|-----------------------------------------------------------------------------------------------------|--------------------------------------------------------| +| `storageClass.name` | Storage Class Name | csi-s3 | +| `storageClass.create` | Specifies whether the storage class should be created | true | +| `storageClass.bucket` | Use a single bucket for all dynamically provisioned persistent volumes | | +| `storageClass.mounter` | Mounter to use. Either geesefs, s3fs or rclone. geesefs recommended | geesefs | +| `storageClass.mountOptions` | GeeseFS mount options | `--memory-limit 1000 --dir-mode 0777 --file-mode 0666` | +| `storageClass.reclaimPolicy` | Volume reclaim policy | Delete | +| `storageClass.annotations` | Annotations for the storage class | | +| `secret.create` | Specifies whether the secret should be created | true | +| `secret.accessKey` | S3 Access Key | | +| `secret.secretKey` | S3 Secret Key | | +| `secret.endpoint` | Endpoint | https://storage.yandexcloud.net | +| `secret.region` | Region | eu-central-1 | +| `secret.cloudProvider` | cloud provider: [aws,aliyun] | | +| `multiCSI` | Check if this CSI has been installed multiple times. if true, only install storageClass and secret. | https://storage.yandexcloud.net | \ No newline at end of file diff --git a/deploy/csi-s3/manifest.yaml b/deploy/csi-s3/manifest.yaml index e186263c1..95a2e9b89 100644 --- a/deploy/csi-s3/manifest.yaml +++ b/deploy/csi-s3/manifest.yaml @@ -45,7 +45,7 @@ user_values: ru: Секретный ключ S3 string_value: default_value: "" - - name: storageClass.singleBucket + - name: storageClass.bucket title: en: Single S3 bucket for volumes ru: Общий S3 бакет для томов @@ -84,21 +84,25 @@ user_values: values: - Delete - Retain - - name: storageClass.name + - name: csiName title: - en: Storage class name - ru: Название класса хранения + en: csi name description: - en: Name of the storage class that will be created - ru: Название класса хранения, который будет создан при установке + en: Name of the csi driver string_value: - default_value: csi-s3 - - name: secret.name + default_value: s3 + - name: multiCSI title: - en: Name of the secret - ru: Название секрета + en: Check if this CSI has been installed multiple times. description: - en: Name of the secret to create or use for the storage class - ru: Название секрета, который будет создан или использован для класса хранения + en: Check if this CSI has been installed multiple times. if true only install storageClass and secret. string_value: - default_value: csi-s3-secret + default_value: false + - name: secret.cloudProvider + title: + en: cloud provider: [aws,aliyun]. + description: + en: cloud provider: [aws,aliyun]. + string_value: + default_value: "" + diff --git a/deploy/csi-s3/templates/_helpers.tpl b/deploy/csi-s3/templates/_helpers.tpl new file mode 100644 index 000000000..a4fd78ade --- /dev/null +++ b/deploy/csi-s3/templates/_helpers.tpl @@ -0,0 +1,42 @@ +{{/* +Expand the endpoint of the secret. +*/}} +{{- define "secret.endpoint" -}} +{{- if eq .Values.secret.cloudProvider "aws" }} + {{- if hasPrefix "cn-" .Values.secret.region }} + {{- printf "https://s3.%s.amazonaws.com.cn" .Values.secret.region }} + {{- else }} + {{- printf "https://s3.%s.amazonaws.com" .Values.secret.region }} + {{- end }} +{{- else if eq .Values.secret.cloudProvider "aliyun" }} + {{- printf "https://oss-%s.aliyuncs.com" .Values.secret.region }} +{{- else if .Values.secret.cloudProvider }} + fail "cloudProvider {{ .Values.secret.cloudProvider }} not supported" +{{- else }} + {{- .Values.secret.endpoint }} +{{- end }} +{{- end }} + + +{{/* +Expand the mountOptions of the storageClass. +*/}} +{{- define "storageClass.mountOptions" -}} +{{- if eq .Values.storageClass.mounter "geesefs" }} + {{- if hasSuffix ".aliyuncs.com" (include "secret.endpoint" .) }} + {{- printf "--memory-limit 1000 --dir-mode 0777 --file-mode 0666 --subdomain %s" .Values.storageClass.mountOptions }} + {{- else if .Values.secret.region }} + {{- printf "--memory-limit 1000 --dir-mode 0777 --file-mode 0666 --region %s %s" .Values.secret.region .Values.storageClass.mountOptions }} + {{- else }} + {{- printf "--memory-limit 1000 --dir-mode 0777 --file-mode 0666 %s" .Values.storageClass.mountOptions }} + {{- end }} +{{- else if eq .Values.storageClass.mounter "s3fs" }} + {{- if hasSuffix ".aliyuncs.com" (include "secret.endpoint" .) }} + {{- .Values.storageClass.mountOptions }} + {{- else }} + {{- printf "-o use_path_request_style %s" .Values.storageClass.mountOptions }} + {{- end }} +{{- else }} + {{- .Values.storageClass.mountOptions }} +{{- end }} +{{- end }} \ No newline at end of file diff --git a/deploy/csi-s3/templates/attacher.yaml b/deploy/csi-s3/templates/attacher.yaml index b849393e0..c87b5a66e 100644 --- a/deploy/csi-s3/templates/attacher.yaml +++ b/deploy/csi-s3/templates/attacher.yaml @@ -1,13 +1,14 @@ +{{- if not .Values.multiCSI -}} apiVersion: v1 kind: ServiceAccount metadata: - name: csi-attacher-sa + name: csi-attacher-sa-{{ .Values.csiName }} namespace: {{ .Release.Namespace }} --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: external-attacher-runner + name: external-attacher-runner-{{ .Values.csiName }} rules: - apiGroups: [""] resources: ["secrets"] @@ -34,14 +35,14 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: csi-attacher-role + name: csi-attacher-role-{{ .Values.csiName }} subjects: - kind: ServiceAccount - name: csi-attacher-sa + name: csi-attacher-sa-{{ .Values.csiName }} namespace: {{ .Release.Namespace }} roleRef: kind: ClusterRole - name: external-attacher-runner + name: external-attacher-runner-{{ .Values.csiName }} apiGroup: rbac.authorization.k8s.io --- # needed for StatefulSet @@ -75,7 +76,7 @@ spec: labels: app: csi-attacher-s3 spec: - serviceAccount: csi-attacher-sa + serviceAccount: csi-attacher-sa-{{ .Values.csiName }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} @@ -102,3 +103,4 @@ spec: hostPath: path: /var/lib/kubelet/plugins/ru.yandex.s3.csi type: DirectoryOrCreate +{{- end -}} \ No newline at end of file diff --git a/deploy/csi-s3/templates/csi-s3.yaml b/deploy/csi-s3/templates/csi-s3.yaml index 1b02cc003..eb3adf48e 100644 --- a/deploy/csi-s3/templates/csi-s3.yaml +++ b/deploy/csi-s3/templates/csi-s3.yaml @@ -1,3 +1,4 @@ +{{- if not .Values.multiCSI -}} apiVersion: v1 kind: ServiceAccount metadata: @@ -53,7 +54,12 @@ spec: app: csi-s3 spec: serviceAccount: csi-s3 + {{- with .Values.daemonsetTolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet containers: - name: driver-registrar image: {{ .Values.images.registrar }} @@ -118,3 +124,4 @@ spec: - name: fuse-device hostPath: path: /dev/fuse +{{- end -}} \ No newline at end of file diff --git a/deploy/csi-s3/templates/provisioner.yaml b/deploy/csi-s3/templates/provisioner.yaml index db0b9b585..add8a995e 100644 --- a/deploy/csi-s3/templates/provisioner.yaml +++ b/deploy/csi-s3/templates/provisioner.yaml @@ -1,13 +1,14 @@ +{{- if not .Values.multiCSI -}} apiVersion: v1 kind: ServiceAccount metadata: - name: csi-provisioner-sa + name: csi-provisioner-sa-{{ .Values.csiName }} namespace: {{ .Release.Namespace }} --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: external-provisioner-runner + name: external-provisioner-runner-{{ .Values.csiName }} rules: - apiGroups: [""] resources: ["secrets"] @@ -28,14 +29,14 @@ rules: kind: ClusterRoleBinding apiVersion: rbac.authorization.k8s.io/v1 metadata: - name: csi-provisioner-role + name: csi-provisioner-role-{{ .Values.csiName }} subjects: - kind: ServiceAccount - name: csi-provisioner-sa + name: csi-provisioner-sa-{{ .Values.csiName }} namespace: {{ .Release.Namespace }} roleRef: kind: ClusterRole - name: external-provisioner-runner + name: external-provisioner-runner-{{ .Values.csiName }} apiGroup: rbac.authorization.k8s.io --- kind: Service @@ -68,7 +69,7 @@ spec: labels: app: csi-provisioner-s3 spec: - serviceAccount: csi-provisioner-sa + serviceAccount: csi-provisioner-sa-{{ .Values.csiName }} {{- with .Values.affinity }} affinity: {{- toYaml . | nindent 8 }} @@ -110,3 +111,4 @@ spec: volumes: - name: socket-dir emptyDir: {} +{{- end -}} \ No newline at end of file diff --git a/deploy/csi-s3/templates/secret.yaml b/deploy/csi-s3/templates/secret.yaml index be7ac7d47..6bad0cb30 100644 --- a/deploy/csi-s3/templates/secret.yaml +++ b/deploy/csi-s3/templates/secret.yaml @@ -3,7 +3,7 @@ apiVersion: v1 kind: Secret metadata: namespace: {{ .Release.Namespace }} - name: {{ .Values.secret.name }} + name: {{ .Values.storageClass.name }}-secret stringData: {{- if .Values.secret.accessKey }} accessKeyID: {{ .Values.secret.accessKey }} @@ -11,5 +11,8 @@ stringData: {{- if .Values.secret.secretKey }} secretAccessKey: {{ .Values.secret.secretKey }} {{- end }} - endpoint: {{ .Values.secret.endpoint }} + endpoint: {{ include "secret.endpoint" . }} +{{- if .Values.secret.cloudProvider }} + cloudProvider: {{ .Values.secret.cloudProvider }} +{{- end }} {{- end -}} diff --git a/deploy/csi-s3/templates/storageclass.yaml b/deploy/csi-s3/templates/storageclass.yaml index e40d69939..1c220d5c3 100644 --- a/deploy/csi-s3/templates/storageclass.yaml +++ b/deploy/csi-s3/templates/storageclass.yaml @@ -10,17 +10,17 @@ metadata: provisioner: ru.yandex.s3.csi parameters: mounter: "{{ .Values.storageClass.mounter }}" - options: "{{ .Values.storageClass.mountOptions }}" -{{- if .Values.storageClass.singleBucket }} - bucket: "{{ .Values.storageClass.singleBucket }}" + options: "{{ include "storageClass.mountOptions" . }}" +{{- if .Values.storageClass.bucket }} + bucket: "{{ .Values.storageClass.bucket }}" {{- end }} - csi.storage.k8s.io/provisioner-secret-name: {{ .Values.secret.name }} + csi.storage.k8s.io/provisioner-secret-name: {{ .Values.storageClass.name }}-secret csi.storage.k8s.io/provisioner-secret-namespace: {{ .Release.Namespace }} - csi.storage.k8s.io/controller-publish-secret-name: {{ .Values.secret.name }} + csi.storage.k8s.io/controller-publish-secret-name: {{ .Values.storageClass.name }}-secret csi.storage.k8s.io/controller-publish-secret-namespace: {{ .Release.Namespace }} - csi.storage.k8s.io/node-stage-secret-name: {{ .Values.secret.name }} + csi.storage.k8s.io/node-stage-secret-name: {{ .Values.storageClass.name }}-secret csi.storage.k8s.io/node-stage-secret-namespace: {{ .Release.Namespace }} - csi.storage.k8s.io/node-publish-secret-name: {{ .Values.secret.name }} + csi.storage.k8s.io/node-publish-secret-name: {{ .Values.storageClass.name }}-secret csi.storage.k8s.io/node-publish-secret-namespace: {{ .Release.Namespace }} reclaimPolicy: {{ .Values.storageClass.reclaimPolicy }} {{- end -}} diff --git a/deploy/csi-s3/values.yaml b/deploy/csi-s3/values.yaml index 91e589777..07dc65502 100644 --- a/deploy/csi-s3/values.yaml +++ b/deploy/csi-s3/values.yaml @@ -1,27 +1,38 @@ --- images: # Source: quay.io/k8scsi/csi-attacher:v3.0.1 - attacher: cr.yandex/crp9ftr22d26age3hulg/yandex-cloud/csi-s3/csi-attacher:v3.0.1 + attacher: registry.cn-hangzhou.aliyuncs.com/apecloud/csi-attacher:v3.4.0 # Source: quay.io/k8scsi/csi-node-driver-registrar:v1.2.0 - registrar: cr.yandex/crp9ftr22d26age3hulg/yandex-cloud/csi-s3/csi-node-driver-registrar:v1.2.0 + registrar: registry.cn-hangzhou.aliyuncs.com/apecloud/csi-node-driver-registrar:v2.5.1 # Source: quay.io/k8scsi/csi-provisioner:v2.1.0 - provisioner: cr.yandex/crp9ftr22d26age3hulg/yandex-cloud/csi-s3/csi-provisioner:v2.1.0 + provisioner: registry.cn-hangzhou.aliyuncs.com/apecloud/csi-provisioner:v3.1.0 # Main image - csi: cr.yandex/crp9ftr22d26age3hulg/yandex-cloud/csi-s3/csi-s3-driver:0.31.3 + csi: registry.cn-hangzhou.aliyuncs.com/apecloud/csi-s3-driver:0.31.3 storageClass: # Specifies whether the storage class should be created create: true - # Name - name: csi-s3 + # storage class name + name: "csi-s3" # Use a single bucket for all dynamically provisioned persistent volumes - singleBucket: "" + bucket: "" # mounter to use - either geesefs, s3fs or rclone (default geesefs) mounter: geesefs # GeeseFS mount options - mountOptions: "--memory-limit 1000 --dir-mode 0777 --file-mode 0666" + # mounter: geesefs + # mountOptions: "--memory-limit 1000 --dir-mode 0777 --file-mode 0666" + + # S3FS mount options + # mounter: s3fs + # use legacy API calling style which do not support the virtual-host request style: + # mountOptions: "-o use_path_request_style" + # NOTE: + # aliyun OSS only support s3fs, and DO NOT set "-o use_path_request_style": + # mounter: s3fs + # mountOptions: "" + mountOptions: "" # Volume reclaim policy - reclaimPolicy: Delete + reclaimPolicy: Retain # Annotations for the storage class # Example: # annotations: @@ -31,22 +42,23 @@ storageClass: secret: # Specifies whether the secret should be created create: true - # Name of the secret - name: csi-s3-secret # S3 Access Key accessKey: "" # S3 Secret Key secretKey: "" # Endpoint + # For AWS set it to "https://s3..amazonaws.com", for example https://s3.eu-central-1.amazonaws.com + # In China set it to "https://s3..amazonaws.com.cn", for example https://s3.cn-north-1.amazonaws.com.cn endpoint: https://storage.yandexcloud.net + region: "" + # cloud name: [aws, aliyun] + cloudProvider: "" tolerations: - - key: kb-controller - operator: Equal - value: "true" - effect: NoSchedule - - key: node-role.kubernetes.io/master - operator: "Exists" + - operator: Exists + +daemonsetTolerations: + - operator: Exists affinity: nodeAffinity: @@ -58,3 +70,9 @@ affinity: operator: In values: - "true" + +# csi name +csiName: s3 + +# Check if this CSI has been installed multiple times. if true, only install storageClass and secret. +multiCSI: false diff --git a/deploy/delphic/.helmignore b/deploy/delphic/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deploy/delphic/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/delphic/Chart.yaml b/deploy/delphic/Chart.yaml new file mode 100644 index 000000000..4c2b53f88 --- /dev/null +++ b/deploy/delphic/Chart.yaml @@ -0,0 +1,20 @@ +apiVersion: v2 +name: delphic +description: A simple framework to use LlamaIndex to build and deploy LLM agents that can be used to analyze and manipulate text data from documents. + +type: application + +version: 0.1.0 + +appVersion: "1.16.0" + + +dependencies: + - name: pgcluster +# repository: https://jihulab.com/api/v4/projects/85949/packages/helm/stable + repository: file://../postgresql-cluster + version: ~0.5.0-0 + - name: redis-cluster +# repository: https://jihulab.com/api/v4/projects/85949/packages/helm/stable + repository: file://../redis-cluster + version: ~0.5.0-0 \ No newline at end of file diff --git a/deploy/delphic/README.md b/deploy/delphic/README.md new file mode 100644 index 000000000..b8784bd55 --- /dev/null +++ b/deploy/delphic/README.md @@ -0,0 +1,50 @@ +1. Enable the addon postgresql and redis. + ```shell + kbcli addon enable postgresql + kbcli addon enable redis + ``` +2. When enabled successfully, you can check it with ```kbcli addon list``` and ```kubectl get clusterdefinition```. + ```shell + kbcli addon list + kubectl get clusterdefinition + NAME MAIN-COMPONENT-NAME STATUS AGE + redis redis Available 6h43m + postgresql postgresql Available 6h42m + ``` +3. Install the delphic with helm. + ```shell + # TODO publish the delphic to public helm repository + helm install delphic ./deploy/delphic + ``` +4. Check whether the plugin is installed successfully. + ``` + kubectl get pods + NAME READY STATUS RESTARTS AGE + delphic-redis-redis-0 3/3 Running 0 42m + delphic-redis-redis-1 3/3 Running 0 42m + delphic-redis-redis-sentinel-0 1/1 Running 1 (41m ago) 42m + delphic-redis-redis-sentinel-2 1/1 Running 1 (41m ago) 42m + delphic-redis-redis-sentinel-1 1/1 Running 1 (41m ago) 42m + delphic-6f747fb8f7-4hdh5 5/5 Running 0 43m + delphic-create-django-user-lmpnq 1/1 Completed 1 43m + delphic-postgres-postgresql-0 4/4 Running 0 42m + delphic-postgres-postgresql-1 4/4 Running 0 42m + ``` +5. Find the username and password of web console. + On Mac OS X: + ``` + kubectl get secret delphic-django-secret -o jsonpath='{.data.username}' | base64 -D + kubectl get secret delphic-django-secret -o jsonpath='{.data.password}' | base64 -D + ``` + + On Linux: + ``` + kubectl get secret delphic-django-secret -o jsonpath='{.data.username}' | base64 -d + kubectl get secret delphic-django-secret -o jsonpath='{.data.password}' | base64 -d + ``` + +6. Port-forward the Plugin Portal to access it. + ```shell + kubectl port-forward deployment/delphic 3000:3000 8000:8000 + ``` +7. In your web browser, open the plugin portal with the address ```http://127.0.0.1:3000``` \ No newline at end of file diff --git a/deploy/delphic/templates/NOTES.txt b/deploy/delphic/templates/NOTES.txt new file mode 100644 index 000000000..c917929a9 --- /dev/null +++ b/deploy/delphic/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "delphic.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "delphic.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "delphic.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "delphic.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/delphic/templates/_helpers.tpl b/deploy/delphic/templates/_helpers.tpl new file mode 100644 index 000000000..eff808411 --- /dev/null +++ b/deploy/delphic/templates/_helpers.tpl @@ -0,0 +1,95 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "delphic.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "delphic.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "delphic.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "delphic.labels" -}} +helm.sh/chart: {{ include "delphic.chart" . }} +{{ include "delphic.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "delphic.selectorLabels" -}} +app.kubernetes.io/name: {{ include "delphic.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "delphic.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "delphic.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} + +{{- define "delphic.common.envs" }} +- name: REDIS_URL + value: redis://{{ .Release.Name }}-{{ index .Values "redis-cluster" "nameOverride" }}-redis:6379 +- name: MODEL_NAME + value: text-davinci-003 +- name: MAX_TOKENS + value: "512" +- name: USE_DOCKER + value: "yes" +- name: POSTGRES_HOST + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-{{ .Values.pgcluster.nameOverride }}-conn-credential + key: host +- name: POSTGRES_PORT + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-{{ .Values.pgcluster.nameOverride }}-conn-credential + key: port +- name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-{{ .Values.pgcluster.nameOverride }}-conn-credential + key: username +- name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Release.Name }}-{{ .Values.pgcluster.nameOverride }}-conn-credential + key: password +- name: POSTGRES_DB + value: delphic +{{- end }} diff --git a/deploy/delphic/templates/deployment.yaml b/deploy/delphic/templates/deployment.yaml new file mode 100644 index 000000000..d31eb704e --- /dev/null +++ b/deploy/delphic/templates/deployment.yaml @@ -0,0 +1,113 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "delphic.fullname" . }} + labels: + {{- include "delphic.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + {{- include "delphic.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include "delphic.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include "delphic.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + initContainers: + - name: postgres-init + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.postgres.repository }}:{{ .Values.image.postgres.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + PGPASSWORD=${POSTGRES_PASSWORD} psql -h${POSTGRES_HOST} -U ${POSTGRES_USER} -p ${POSTGRES_PORT} -tc "SELECT 1 FROM pg_database WHERE datname = '${POSTGRES_DB}'" + if [ $? != 0 ]; then + PGPASSWORD=${POSTGRES_PASSWORD} psql -h${POSTGRES_HOST} -U ${POSTGRES_USER} -p ${POSTGRES_PORT} -c "CREATE DATABASE ${POSTGRES_DB}" + fi + env: + {{ include "delphic.common.envs" . | nindent 12}} + containers: + - name: django + command: + - /entrypoint + - /start + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.resources | nindent 12 }} + env: + - name: OPENAI_API_KEY + value: {{ .Values.openai_api_key }} + {{ include "delphic.common.envs" . | nindent 12}} + - name: frontend + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.frontend.repository }}:{{ .Values.image.frontend.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.frontend.resources | nindent 12 }} + - name: celeryworker + command: + - /entrypoint + - /start-celeryworker + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.celeryWorker.resources | nindent 12 }} + env: + {{ include "delphic.common.envs" . | nindent 12}} + - name: celerybeat + command: + - /entrypoint + - /start-celerybeat + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.celeryBeat.resources | nindent 12 }} + env: + {{ include "delphic.common.envs" . | nindent 12}} + - name: flower + command: + - /entrypoint + - /start-flower + image: "{{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: + {{- toYaml .Values.flower.resources | nindent 12 }} + env: + - name: CELERY_FLOWER_USER + valueFrom: + secretKeyRef: + name: {{ include "delphic.fullname" . }}-celery-flower-secret + key: username + - name: CELERY_FLOWER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "delphic.fullname" . }}-celery-flower-secret + key: password + {{ include "delphic.common.envs" . | nindent 12}} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/deploy/delphic/templates/ingress.yaml b/deploy/delphic/templates/ingress.yaml new file mode 100644 index 000000000..118372551 --- /dev/null +++ b/deploy/delphic/templates/ingress.yaml @@ -0,0 +1,61 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include "delphic.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if and .Values.ingress.className (not (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion)) }} + {{- if not (hasKey .Values.ingress.annotations "kubernetes.io/ingress.class") }} + {{- $_ := set .Values.ingress.annotations "kubernetes.io/ingress.class" .Values.ingress.className}} + {{- end }} +{{- end }} +{{- if semverCompare ">=1.19-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1 +{{- else if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include "delphic.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if and .Values.ingress.className (semverCompare ">=1.18-0" .Capabilities.KubeVersion.GitVersion) }} + ingressClassName: {{ .Values.ingress.className }} + {{- end }} + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ .path }} + {{- if and .pathType (semverCompare ">=1.18-0" $.Capabilities.KubeVersion.GitVersion) }} + pathType: {{ .pathType }} + {{- end }} + backend: + {{- if semverCompare ">=1.19-0" $.Capabilities.KubeVersion.GitVersion }} + service: + name: {{ $fullName }} + port: + number: {{ $svcPort }} + {{- else }} + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} +{{- end }} diff --git a/deploy/delphic/templates/job.yaml b/deploy/delphic/templates/job.yaml new file mode 100644 index 000000000..d025e7ea2 --- /dev/null +++ b/deploy/delphic/templates/job.yaml @@ -0,0 +1,53 @@ +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ include "delphic.fullname" . }}-create-django-user + labels: + {{- include "delphic.labels" . | nindent 4 }} +spec: + ttlSecondsAfterFinished: 3600 + template: + metadata: + name: {{ include "delphic.fullname" . }}-create-django-user + labels: + {{- include "delphic.labels" . | nindent 8 }} + spec: + serviceAccountName: {{ include "delphic.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + restartPolicy: OnFailure + containers: + - name: post-install-job + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + command: + - /bin/sh + - -c + - | + /entrypoint python manage.py createsuperuser --noinput + env: + - name: DJANGO_SUPERUSER_USERNAME + valueFrom: + secretKeyRef: + name: {{ include "delphic.fullname" . }}-django-secret + key: username + - name: DJANGO_SUPERUSER_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "delphic.fullname" . }}-django-secret + key: password + - name: DJANGO_SUPERUSER_EMAIL + value: admin@kubeblocks.io + {{ include "delphic.common.envs" . | nindent 10}} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} \ No newline at end of file diff --git a/deploy/delphic/templates/secret.yaml b/deploy/delphic/templates/secret.yaml new file mode 100644 index 000000000..7fa049627 --- /dev/null +++ b/deploy/delphic/templates/secret.yaml @@ -0,0 +1,19 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "delphic.fullname" . }}-celery-flower-secret +type: Opaque +data: + # generate 32 chars long random string, base64 encode it and then double-quote the result string. + username: {{ randAlphaNum 32 | b64enc | quote }} + password: {{ randAlphaNum 64 | b64enc | quote }} + +--- +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "delphic.fullname" . }}-django-secret +type: Opaque +data: + username: {{ randAlphaNum 8 | b64enc | quote }} + password: {{ randAlphaNum 16 | b64enc | quote }} diff --git a/deploy/delphic/templates/service.yaml b/deploy/delphic/templates/service.yaml new file mode 100644 index 000000000..555711c67 --- /dev/null +++ b/deploy/delphic/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include "delphic.fullname" . }} + labels: + {{- include "delphic.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include "delphic.selectorLabels" . | nindent 4 }} diff --git a/deploy/delphic/templates/serviceaccount.yaml b/deploy/delphic/templates/serviceaccount.yaml new file mode 100644 index 000000000..365325816 --- /dev/null +++ b/deploy/delphic/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "delphic.serviceAccountName" . }} + labels: + {{- include "delphic.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/deploy/delphic/templates/tests/test-connection.yaml b/deploy/delphic/templates/tests/test-connection.yaml new file mode 100644 index 000000000..c1cf8c8f0 --- /dev/null +++ b/deploy/delphic/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include "delphic.fullname" . }}-test-connection" + labels: + {{- include "delphic.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include "delphic.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/deploy/delphic/values.yaml b/deploy/delphic/values.yaml new file mode 100644 index 000000000..a79d6228d --- /dev/null +++ b/deploy/delphic/values.yaml @@ -0,0 +1,107 @@ +# Default values for delphic. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + registry: registry.cn-hangzhou.aliyuncs.com + repository: apecloud/delphic + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: v1.0.0 + frontend: + repository: apecloud/delphic-frontend + tag: v1.0.0 + + postgres: + repository: apecloud/spilo + tag: 14.7.1 + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + className: "" + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: + - path: / + pathType: ImplementationSpecific + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +pgcluster: + enabled: true + nameOverride: postgres + +redis-cluster: + enabled: true + nameOverride: redis + +celeryWorker: + resources: {} + +celeryBeat: + resources: {} + +flower: + resources: {} + +frontend: + resources: {} + +openai_api_key: "" + + diff --git a/deploy/etcd-cluster/templates/_helpers.tpl b/deploy/etcd-cluster/templates/_helpers.tpl index b7c399f57..9e0ca6931 100644 --- a/deploy/etcd-cluster/templates/_helpers.tpl +++ b/deploy/etcd-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "etcd-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "etcd-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "etcd-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "etcd-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/etcd-cluster/templates/cluster.yaml b/deploy/etcd-cluster/templates/cluster.yaml index c27b9b0a9..336e4c958 100644 --- a/deploy/etcd-cluster/templates/cluster.yaml +++ b/deploy/etcd-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{- include "etcd-cluster.labels" . | nindent 4 }} spec: @@ -21,6 +21,7 @@ spec: monitor: {{ .Values.monitor.enabled }} serviceType: {{ $.Values.service.type | default "ClusterIP" }} replicas: {{ .Values.replicaCount | default "3" }} + serviceAccountName: {{ include "etcd-cluster.serviceAccountName" . }} {{- with .Values.resources }} resources: limits: @@ -40,4 +41,4 @@ spec: resources: requests: storage: {{ .Values.persistence.data.size }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/deploy/etcd-cluster/templates/role.yaml b/deploy/etcd-cluster/templates/role.yaml new file mode 100644 index 000000000..b15f87c85 --- /dev/null +++ b/deploy/etcd-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "etcd-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/etcd-cluster/templates/rolebinding.yaml b/deploy/etcd-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..d53659546 --- /dev/null +++ b/deploy/etcd-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "etcd-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "etcd-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/etcd-cluster/templates/serviceaccount.yaml b/deploy/etcd-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..2d9e1cf10 --- /dev/null +++ b/deploy/etcd-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "etcd-cluster.serviceAccountName" . }} + labels: + {{ include "etcd-cluster.labels" . | nindent 4 }} diff --git a/deploy/etcd-cluster/templates/tests/test-connection.yaml b/deploy/etcd-cluster/templates/tests/test-connection.yaml index 04cbdb9f8..f6bbd83c2 100644 --- a/deploy/etcd-cluster/templates/tests/test-connection.yaml +++ b/deploy/etcd-cluster/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "etcd-cluster.fullname" . }}-test-connection" + name: "{{ include "clustername" . }}-test-connection" labels: {{- include "etcd-cluster.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "etcd-cluster.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "clustername" . }}:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/deploy/etcd-cluster/values.yaml b/deploy/etcd-cluster/values.yaml index 8704e3a62..7fe5da0b7 100644 --- a/deploy/etcd-cluster/values.yaml +++ b/deploy/etcd-cluster/values.yaml @@ -174,3 +174,7 @@ ingress: ## name: http ## extraRules: [] + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/etcd/templates/clusterdefinition.yaml b/deploy/etcd/templates/clusterdefinition.yaml index 445f6cac9..7192449e5 100644 --- a/deploy/etcd/templates/clusterdefinition.yaml +++ b/deploy/etcd/templates/clusterdefinition.yaml @@ -19,7 +19,7 @@ spec: accessMode: ReadWrite updateStrategy: BestEffortParallel probes: - roleChangedProbe: + roleProbe: periodSeconds: 1 failureThreshold: 3 service: @@ -47,11 +47,11 @@ spec: PEERS="" DOMAIN=$KB_NAMESPACE".svc.cluster.local" i=0 - while [ $i -lt $KB_ETCD_N ]; do + while [ $i -lt $KB_REPLICA_COUNT ]; do if [ $i -ne 0 ]; then PEERS="$PEERS,"; fi; - host=$(eval echo \$KB_ETCD_"$i"_HOSTNAME) + host=$(eval echo \$KB_"$i"_HOSTNAME) host=$host"."$DOMAIN PEERS="$PEERS$host=http://$host:2380" i=$(( i + 1)) diff --git a/deploy/etcd/values.yaml b/deploy/etcd/values.yaml index e55273d44..cd7441ff1 100644 --- a/deploy/etcd/values.yaml +++ b/deploy/etcd/values.yaml @@ -8,7 +8,7 @@ clusterVersionOverride: "" nameOverride: "" fullnameOverride: "" -roleChangedProbe: +roleProbe: failureThreshold: 2 periodSeconds: 1 timeoutSeconds: 1 diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml index b3bb0da0f..37baad3a9 100644 --- a/deploy/helm/Chart.yaml +++ b/deploy/helm/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: 0.5.0-alpha.3 +appVersion: 0.5.1-beta.0 kubeVersion: '>=1.22.0-0' diff --git a/deploy/helm/config/rbac/role.yaml b/deploy/helm/config/rbac/role.yaml index e2747ca6a..5352a83ea 100644 --- a/deploy/helm/config/rbac/role.yaml +++ b/deploy/helm/config/rbac/role.yaml @@ -30,30 +30,6 @@ rules: - deployments/status verbs: - get -- apiGroups: - - apps - resources: - - pods - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - apps - resources: - - pods/finalizers - verbs: - - update -- apiGroups: - - apps - resources: - - pods/status - verbs: - - get - apiGroups: - apps resources: @@ -106,11 +82,10 @@ rules: - apiGroups: - apps.kubeblocks.io resources: - - classfamilies + - backuppolicytemplates verbs: - get - list - - watch - apiGroups: - apps.kubeblocks.io resources: @@ -189,6 +164,40 @@ rules: - get - patch - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions/finalizers + verbs: + - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentclassdefinitions/status + verbs: + - get + - patch + - update +- apiGroups: + - apps.kubeblocks.io + resources: + - componentresourceconstraints + verbs: + - get + - list + - watch - apiGroups: - apps.kubeblocks.io resources: @@ -383,6 +392,16 @@ rules: - persistentvolumeclaims/status verbs: - get +- apiGroups: + - "" + resources: + - persistentvolumes + verbs: + - get + - list + - patch + - update + - watch - apiGroups: - "" resources: @@ -401,6 +420,19 @@ rules: - pods/exec verbs: - create +- apiGroups: + - "" + resources: + - pods/finalizers + verbs: + - update +- apiGroups: + - "" + resources: + - pods/log + verbs: + - get + - list - apiGroups: - "" resources: @@ -496,32 +528,6 @@ rules: - get - patch - update -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates - verbs: - - create - - delete - - get - - list - - patch - - update - - watch -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates/finalizers - verbs: - - update -- apiGroups: - - dataprotection.kubeblocks.io - resources: - - backuppolicytemplates/status - verbs: - - get - - patch - - update - apiGroups: - dataprotection.kubeblocks.io resources: @@ -673,3 +679,29 @@ rules: - get - list - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/finalizers + verbs: + - update +- apiGroups: + - workloads.kubeblocks.io + resources: + - consensussets/status + verbs: + - get + - patch + - update diff --git a/deploy/helm/crds/apps.kubeblocks.io_backuppolicytemplates.yaml b/deploy/helm/crds/apps.kubeblocks.io_backuppolicytemplates.yaml new file mode 100644 index 000000000..51ba57b1c --- /dev/null +++ b/deploy/helm/crds/apps.kubeblocks.io_backuppolicytemplates.yaml @@ -0,0 +1,430 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: backuppolicytemplates.apps.kubeblocks.io +spec: + group: apps.kubeblocks.io + names: + categories: + - kubeblocks + kind: BackupPolicyTemplate + listKind: BackupPolicyTemplateList + plural: backuppolicytemplates + shortNames: + - bpt + singular: backuppolicytemplate + scope: Cluster + versions: + - additionalPrinterColumns: + - description: ClusterDefinition referenced by cluster. + jsonPath: .spec.clusterDefinitionRef + name: CLUSTER-DEFINITION + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: BackupPolicyTemplate is the Schema for the BackupPolicyTemplates + API (defined by provider) + 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: BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate + properties: + backupPolicies: + description: backupPolicies is a list of backup policy template for + the specified componentDefinition. + items: + properties: + componentDefRef: + description: componentDefRef references componentDef defined + in ClusterDefinition spec. + maxLength: 63 + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + datafile: + description: the policy for datafile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, + only support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + logfile: + description: the policy for logfile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, + only support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + retention: + description: retention describe how long the Backup should be + retained. if not set, will be retained forever. + properties: + ttl: + description: ttl is a time string ending with the 'd'|'D'|'h'|'H' + character to describe how long the Backup should be retained. + if not set, will be retained forever. + pattern: ^\d+[d|D|h|H]$ + type: string + type: object + schedule: + description: schedule policy for backup. + properties: + datafile: + description: schedule policy for datafile backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + logfile: + description: schedule policy for logfile backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + snapshot: + description: schedule policy for snapshot backup. + properties: + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. + type: string + enable: + description: enable or disable the schedule. + type: boolean + required: + - cronExpression + - enable + type: object + type: object + snapshot: + description: the policy for snapshot backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can + execute. + type: string + path: + description: 'specify the json path of backup object + for patch. example: manifests.backupLog -- means + patch the backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect + backup status metadata. The script must exist in + the container of ContainerName and the output format + must be set to JSON. Note that outputting to stderr + may cause the result format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: + before backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. + Value must be non-negative integer. 0 means NO limit on + the number of backups. + format: int32 + type: integer + hooks: + description: execute hook commands for backup. + properties: + containerName: + description: which container can exec command + type: string + image: + description: exec command with image + type: string + postCommands: + description: post backup to perform commands + items: + type: string + type: array + preCommands: + description: pre backup to perform commands + items: + type: string + type: array + type: object + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target instance for backup. + properties: + account: + description: refer to spec.componentDef.systemAccounts.accounts[*].name + in ClusterDefinition. the secret created by this account + will be used to connect the database. if not set, + the secret created by spec.ConnectionCredential of + the ClusterDefinition will be used. it will be transformed + to a secret for BackupPolicy's target secret. + type: string + connectionCredentialKey: + description: connectionCredentialKey defines connection + credential key in secret which created by spec.ConnectionCredential + of the ClusterDefinition. it will be ignored when + "account" is set. + properties: + passwordKey: + description: the key of password in the ConnectionCredential + secret. if not set, the default key is "password". + type: string + usernameKey: + description: the key of username in the ConnectionCredential + secret. if not set, the default key is "username". + type: string + type: object + role: + description: 'select instance of corresponding role + for backup, role are: - the name of Leader/Follower/Leaner + for Consensus component. - primary or secondary for + Replication component. finally, invalid role of the + component will be ignored. such as if workload type + is Replication and component''s replicas is 1, the + secondary role is invalid. and it also will be ignored + when component is Stateful/Stateless. the role will + be transformed to a role LabelSelector for BackupPolicy''s + target attribute.' + type: string + type: object + type: object + required: + - componentDefRef + type: object + minItems: 1 + type: array + x-kubernetes-list-map-keys: + - componentDefRef + x-kubernetes-list-type: map + clusterDefinitionRef: + description: clusterDefinitionRef references ClusterDefinition name, + this is an immutable attribute. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + identifier: + description: Identifier is a unique identifier for this BackupPolicyTemplate. + this identifier will be the suffix of the automatically generated + backupPolicy name. and must be added when multiple BackupPolicyTemplates + exist, otherwise the generated backupPolicy override will occur. + maxLength: 20 + type: string + required: + - backupPolicies + - clusterDefinitionRef + type: object + status: + description: BackupPolicyTemplateStatus defines the observed state of + BackupPolicyTemplate + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml index 2916d11b2..60423c646 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusterdefinitions.yaml @@ -94,6 +94,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: set name: description: Specify the name of configuration template. maxLength: 63 @@ -219,17 +220,72 @@ spec: - accessMode - name type: object + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object updateStrategy: default: Serial - description: 'updateStrategy, Pods update strategy. serial: - update Pods one by one that guarantee minimum component - unavailable time. Learner -> Follower(with AccessMode=none) - -> Follower(with AccessMode=readonly) -> Follower(with - AccessMode=readWrite) -> Leader bestEffortParallel: update - Pods in parallel that guarantee minimum component un-writable - time. Learner, Follower(minority) in parallel -> Follower(majority) - -> Leader, keep majority online all the time. parallel: - force parallel' + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" enum: - Serial - BestEffortParallel @@ -286,12 +342,10 @@ spec: description: horizontalScalePolicy controls the behavior of horizontal scale. properties: - backupTemplateSelector: - additionalProperties: - type: string - description: backupTemplateSelector defines the label selector - for finding associated BackupTemplate API object. - type: object + backupPolicyTemplateName: + description: BackupPolicyTemplateName reference the backup + policy template. + type: string type: default: None description: 'type controls what kind of data synchronization @@ -299,10 +353,10 @@ spec: Snapshot}. The default policy is `None`. None: Default policy, do nothing. Snapshot: Do native volume snapshot before scaling and restore to newly scaled pods. Prefer - backup job to create snapshot if `BackupTemplateSelector` - can find a template. Notice that ''Snapshot'' policy will - only take snapshot on one volumeMount, default is the - first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]), + backup job to create snapshot if can find a backupPolicy + from ''BackupPolicyTemplateName''. Notice that ''Snapshot'' + policy will only take snapshot on one volumeMount, default + is the first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]), since take multiple snapshots at one time might cause consistency problem.' enum: @@ -340,16 +394,6 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map - maxUnavailable: - anyOf: - - type: integer - - type: string - description: 'The maximum number of pods that can be unavailable - during scaling. Value can be an absolute number (ex: 5) or - a percentage of desired pods (ex: 10%). Absolute number is - calculated from percentage by rounding down. This value is - ignored if workloadType is Consensus.' - x-kubernetes-int-or-string: true monitor: description: monitor is monitoring config which provided by provider. @@ -357,9 +401,10 @@ spec: builtIn: default: false description: builtIn is a switch to enable KubeBlocks builtIn - monitoring. If BuiltIn is set to false, the provider should - set ExporterConfig and Sidecar container own. BuiltIn - set to true is not currently supported but will be soon. + monitoring. If BuiltIn is set to true, monitor metrics + will be scraped automatically. If BuiltIn is set to false, + the provider should set ExporterConfig and Sidecar container + own. type: boolean exporterConfig: description: exporterConfig provided by provider, which @@ -373,12 +418,12 @@ spec: maxLength: 128 type: string scrapePort: + anyOf: + - type: integer + - type: string description: scrapePort is exporter port for Time Series Database to scrape metrics. - format: int32 - maximum: 65535 - minimum: 0 - type: integer + x-kubernetes-int-or-string: true required: - scrapePort type: object @@ -7916,7 +7961,7 @@ spec: probes: description: probes setting for healthy checks. properties: - roleChangedProbe: + roleProbe: description: Probe for DB role changed check. properties: commands: @@ -7963,9 +8008,8 @@ spec: exceed the InitializationTimeoutSeconds time without a role label, this component will enter the Failed/Abnormal phase. Note that this configuration will only take effect - if the component supports RoleChangedProbe and will not - affect the life cycle of the pod. default values are 60 - seconds. + if the component supports RoleProbe and will not affect + the life cycle of the pod. default values are 60 seconds. format: int32 minimum: 30 type: integer @@ -8052,9 +8096,62 @@ spec: type: object replicationSpec: description: replicationSpec defines replication related spec - if workloadType is Replication, required if workloadType is - Replication. + if workloadType is Replication. properties: + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object switchCmdExecutorConfig: description: switchCmdExecutorConfig configs how to get client SDK and perform switch statements. @@ -8286,6 +8383,23 @@ spec: type: object minItems: 1 type: array + updateStrategy: + default: Serial + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" + enum: + - Serial + - BestEffortParallel + - Parallel + type: string required: - switchCmdExecutorConfig type: object @@ -8403,6 +8517,141 @@ spec: - protocol x-kubernetes-list-type: map type: object + statefulSpec: + description: statefulSpec defines stateful related spec if workloadType + is Stateful. + properties: + llPodManagementPolicy: + description: llPodManagementPolicy is the low-level controls + how pods are created during initial scale up, when replacing + pods on nodes, or when scaling down. `OrderedReady` policy + specify where pods are created in increasing order (pod-0, + then pod-1, etc) and the controller will wait until each + pod is ready before continuing. When scaling down, the + pods are removed in the opposite order. `Parallel` policy + specify create pods in parallel to match the desired scale + without waiting, and on scale down will delete all pods + at once. + type: string + llUpdateStrategy: + description: llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy + that will be employed to update Pods in the StatefulSet + when a revision is made to Template. Will ignore `updateStrategy` + attribute if provided. + properties: + rollingUpdate: + description: RollingUpdate is used to communicate parameters + when Type is RollingUpdateStatefulSetStrategyType. + properties: + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding up. This can not be + 0. Defaults to 1. This field is alpha-level and + is only honored by servers that enable the MaxUnavailableStatefulSet + feature. The field applies to all pods in the + range 0 to Replicas-1. That means if there is + any unavailable pod in the range 0 to Replicas-1, + it will be counted towards MaxUnavailable.' + x-kubernetes-int-or-string: true + partition: + description: Partition indicates the ordinal at + which the StatefulSet should be partitioned for + updates. During a rolling update, all pods from + ordinal Replicas-1 to Partition are updated. All + pods from ordinal Partition-1 to 0 remain untouched. + This is helpful in being able to do a canary based + deployment. The default value is 0. + format: int32 + type: integer + type: object + type: + description: Type indicates the type of the StatefulSetUpdateStrategy. + Default is RollingUpdate. + type: string + type: object + updateStrategy: + default: Serial + description: "updateStrategy, Pods update strategy. In case + of workloadType=Consensus the update strategy will be + following: \n serial: update Pods one by one that guarantee + minimum component unavailable time. Learner -> Follower(with + AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component + un-writable time. Learner, Follower(minority) in parallel + -> Follower(majority) -> Leader, keep majority online + all the time. parallel: force parallel" + enum: + - Serial + - BestEffortParallel + - Parallel + type: string + type: object + statelessSpec: + description: statelessSpec defines stateless related spec if + workloadType is Stateless. + properties: + updateStrategy: + description: updateStrategy defines the underlying deployment + strategy to use to replace existing pods with new ones. + properties: + rollingUpdate: + description: 'Rolling update config params. Present + only if DeploymentStrategyType = RollingUpdate. --- + TODO: Update this to follow our convention for oneOf, + whatever we decide it to be.' + properties: + maxSurge: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be scheduled above the desired number of pods. + Value can be an absolute number (ex: 5) or a percentage + of desired pods (ex: 10%). This can not be 0 if + MaxUnavailable is 0. Absolute number is calculated + from percentage by rounding up. Defaults to 25%. + Example: when this is set to 30%, the new ReplicaSet + can be scaled up immediately when the rolling + update starts, such that the total number of old + and new pods do not exceed 130% of desired pods. + Once old pods have been killed, new ReplicaSet + can be scaled up further, ensuring that total + number of pods running at any time during the + update is at most 130% of desired pods.' + x-kubernetes-int-or-string: true + maxUnavailable: + anyOf: + - type: integer + - type: string + description: 'The maximum number of pods that can + be unavailable during the update. Value can be + an absolute number (ex: 5) or a percentage of + desired pods (ex: 10%). Absolute number is calculated + from percentage by rounding down. This can not + be 0 if MaxSurge is 0. Defaults to 25%. Example: + when this is set to 30%, the old ReplicaSet can + be scaled down to 70% of desired pods immediately + when the rolling update starts. Once new pods + are ready, old ReplicaSet can be scaled down further, + followed by scaling up the new ReplicaSet, ensuring + that the total number of pods available at all + times during the update is at least 70% of desired + pods.' + x-kubernetes-int-or-string: true + type: object + type: + description: Type of deployment. Can be "Recreate" or + "RollingUpdate". Default is RollingUpdate. + type: string + type: object + type: object systemAccounts: description: Statement to create system account. properties: @@ -8457,7 +8706,16 @@ spec: type: string deletion: description: deletion specifies statement - how to delete this account. + how to delete this account. Used in combination + with `CreateionStatement` to delete the + account before create it. For instance, + one usually uses `drop user if exists` statement + followed by `create user` statement to create + an account. + type: string + update: + description: update specifies statement how + to update account's password. type: string required: - creation @@ -8662,9 +8920,9 @@ spec: the volumes mapping the name of the VolumeMounts in the PodSpec.Container field, such as data volume, log volume, etc. When backing up the volume, the volume can be correctly backed up according - to the volumeType. \n For example: `{name: data, type: data}` + to the volumeType. \n For example: `name: data, type: data` means that the volume named `data` is used to store `data`. - `{name: binlog, type: log}` means that the volume named `binlog` + `name: binlog, type: log` means that the volume named `binlog` is used to store `log`. \n NOTE: When volumeTypes is not defined, the backup function will not be supported, even if a persistent volume has been specified." @@ -8685,8 +8943,13 @@ spec: - data - log type: string + required: + - name type: object type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map workloadType: description: workloadType defines type of the workload. Stateless is a stateless workload type used to describe stateless applications. @@ -8706,6 +8969,11 @@ spec: - name - workloadType type: object + x-kubernetes-validations: + - message: componentDefs.consensusSpec is required when componentDefs.workloadType + is Consensus, and forbidden otherwise + rule: 'has(self.workloadType) && self.workloadType == ''Consensus'' + ? has(self.consensusSpec) : !has(self.consensusSpec)' minItems: 1 type: array x-kubernetes-list-map-keys: @@ -8715,17 +8983,20 @@ spec: additionalProperties: type: string description: 'Connection credential template used for creating a connection - credential secret for cluster.apps.kubeblock.io object. Built-in + credential secret for cluster.apps.kubeblocks.io object. Built-in objects are: `$(RANDOM_PASSWD)` - random 8 characters. `$(UUID)` - generate a random UUID v4 string. `$(UUID_B64)` - generate a random - UUID v4 BASE64 encoded string``. `$(UUID_STR_B64)` - generate a - random UUID v4 string then BASE64 encoded``. `$(UUID_HEX)` - generate - a random UUID v4 wth HEX representation``. `$(SVC_FQDN)` - service - FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc, + UUID v4 BASE64 encoded string. `$(UUID_STR_B64)` - generate a random + UUID v4 string then BASE64 encoded. `$(UUID_HEX)` - generate a random + UUID v4 HEX representation. `$(HEADLESS_SVC_FQDN)` - headless service + FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc, where 1ST_COMP_NAME is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` - attribute; `$(SVC_PORT_)` - a ServicePort''s port value - with specified port name, i.e, a servicePort JSON struct: { "name": - "mysql", "targetPort": "mysqlContainerPort", "port": 3306 }, and + attribute; `$(SVC_FQDN)` - service FQDN placeholder, value pattern + - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc, where 1ST_COMP_NAME + is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` + attribute; `$(SVC_PORT_{PORT-NAME})` - a ServicePort''s port value + with specified port name, i.e, a servicePort JSON struct: `"name": + "mysql", "targetPort": "mysqlContainerPort", "port": 3306`, and "$(SVC_PORT_mysql)" in the connection credential value is 3306.' type: object type: @@ -8750,9 +9021,9 @@ spec: format: int64 type: integer phase: - description: ClusterDefinition phase, valid values are , Available. - Available is ClusterDefinition become available, and can be referenced - for co-related objects. + description: ClusterDefinition phase, valid values are `empty`, `Available`, + 'Unavailable`. Available is ClusterDefinition become available, + and can be referenced for co-related objects. enum: - Available - Unavailable diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml index 68e463d91..8f4d20db2 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusters.yaml @@ -41,7 +41,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Cluster is the Schema for the clusters API + description: Cluster is the Schema for the clusters API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -56,7 +56,7 @@ spec: metadata: type: object spec: - description: ClusterSpec defines the desired state of Cluster + description: ClusterSpec defines the desired state of Cluster. properties: affinity: description: affinity is a group of affinity scheduling rules. @@ -99,12 +99,12 @@ spec: x-kubernetes-list-type: set type: object clusterDefinitionRef: - description: Cluster referenced ClusterDefinition name, this is an + description: Cluster referencing ClusterDefinition name. This is an immutable attribute. pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string clusterVersionRef: - description: Cluster referenced ClusterVersion name. + description: Cluster referencing ClusterVersion name. pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string componentSpecs: @@ -114,8 +114,7 @@ spec: items: properties: affinity: - description: affinity describes affinities which specific by - users. + description: affinity describes affinities specified by users. properties: nodeLabels: additionalProperties: @@ -156,28 +155,41 @@ spec: type: array x-kubernetes-list-type: set type: object + classDefRef: + description: classDefRef references the class defined in ComponentClassDefinition. + properties: + class: + description: Class refers to the name of the class that + is defined in the ComponentClassDefinition. + type: string + name: + description: Name refers to the name of the ComponentClassDefinition. + type: string + required: + - class + type: object componentDefRef: - description: componentDefRef reference componentDef defined + description: componentDefRef references the componentDef defined in ClusterDefinition spec. maxLength: 63 pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string enabledLogs: - description: enabledLogs indicate which log file takes effect - in database cluster element is the log type which defined - in cluster definition logConfig.name, and will set relative - variables about this log type in database kernel. + description: enabledLogs indicates which log file takes effect + in the database cluster. element is the log type which is + defined in cluster definition logConfig.name, and will set + relative variables about this log type in database kernel. items: type: string type: array x-kubernetes-list-type: set issuer: - description: issuer who provides tls certs required when TLS - enabled + description: issuer defines provider context for TLS certs. + required when TLS enabled properties: name: default: KubeBlocks - description: 'name of issuer options supported: - KubeBlocks + description: 'Name of issuer. Options supported: - KubeBlocks - Certificates signed by KubeBlocks Operator. - UserProvided - User provided own CA-signed certificates.' enum: @@ -185,20 +197,20 @@ spec: - UserProvided type: string secretRef: - description: secretRef, Tls certs Secret reference required + description: secretRef. TLS certs Secret reference required when from is UserProvided properties: ca: - description: ca cert key in Secret + description: CA cert key in Secret type: string cert: - description: cert key in Secret + description: Cert key in Secret type: string key: - description: key of TLS private key in Secret + description: Key of TLS private key in Secret type: string name: - description: name of the Secret + description: Name of the Secret type: string required: - ca @@ -211,33 +223,39 @@ spec: type: object monitor: default: false - description: monitor which is a switch to enable monitoring, - default is false KubeBlocks provides an extension mechanism - to support component level monitoring, which will scrape metrics - auto or manually from servers in component and export metrics - to Time Series Database. + description: monitor is a switch to enable monitoring and is + set as false by default. KubeBlocks provides an extension + mechanism to support component level monitoring, which will + scrape metrics auto or manually from servers in component + and export metrics to Time Series Database. type: boolean name: description: name defines cluster's component name. maxLength: 15 pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string + noCreatePDB: + default: false + description: noCreatePDB defines the PodDistruptionBudget creation + behavior and is set to true if creation of PodDistruptionBudget + for this component is not needed. It defaults to false. + type: boolean primaryIndex: description: primaryIndex determines which index is primary - when workloadType is Replication, index number starts from + when workloadType is Replication. Index number starts from zero. format: int32 minimum: 0 type: integer replicas: default: 1 - description: Component replicas, use default value in ClusterDefinition - spec. if not specified. + description: Component replicas. The default value is used in + ClusterDefinition spec if not specified. format: int32 minimum: 0 type: integer resources: - description: resources requests and limits of workload. + description: Resources requests and limits of workload. properties: claims: description: "Claims lists the names of resources, defined @@ -285,18 +303,24 @@ spec: type: object type: object x-kubernetes-preserve-unknown-fields: true + serviceAccountName: + description: serviceAccountName is the name of the ServiceAccount + that running component depends on. + type: string services: - description: services expose endpoints can be accessed by clients + description: Services expose endpoints that can be accessed + by clients. items: properties: annotations: additionalProperties: type: string description: 'If ServiceType is LoadBalancer, cloud provider - related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -305,15 +329,16 @@ spec: LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not specified, - by manual construction of an Endpoints object or EndpointSlice - objects. If clusterIP is "None", no virtual IP is allocated - and the endpoints are published as a set of endpoints - rather than a virtual IP. "NodePort" builds on ClusterIP - and allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" builds - on NodePort and creates an external load-balancer (if - supported in the current cloud) which routes to the - same endpoints as the clusterIP. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + they are determined by manual construction of an Endpoints + object or EndpointSlice objects. If clusterIP is "None", + no virtual IP is allocated and the endpoints are published + as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node + which routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external + load-balancer (if supported in the current cloud) which + routes to the same endpoints as the clusterIP. More + info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -339,7 +364,7 @@ spec: type: string type: object tls: - description: tls should be enabled or not + description: Enables or disables TLS certs. type: boolean tolerations: description: Component tolerations will override ClusterSpec.Tolerations @@ -390,7 +415,7 @@ spec: items: properties: name: - description: Ref ClusterVersion.spec.components.containers.volumeMounts.name + description: Reference `ClusterDefinition.spec.componentDefs.containers.volumeMounts.name`. type: string spec: description: spec defines the desired characteristics @@ -398,17 +423,18 @@ spec: properties: accessModes: description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1.' items: type: string type: array + x-kubernetes-preserve-unknown-fields: true resources: description: 'resources represents the minimum resources the volume should have. If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements that are lower than previous value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources.' properties: claims: description: "Claims lists the names of resources, @@ -458,12 +484,12 @@ spec: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' type: object type: object + x-kubernetes-preserve-unknown-fields: true storageClassName: description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1.' type: string type: object - x-kubernetes-preserve-unknown-fields: true required: - name type: object @@ -479,12 +505,12 @@ spec: - name x-kubernetes-list-type: map terminationPolicy: - description: Cluster termination policy. One of DoNotTerminate, Halt, - Delete, WipeOut. DoNotTerminate will block delete operation. Halt - will delete workload resources such as statefulset, deployment workloads - but keep PVCs. Delete is based on Halt and deletes PVCs. WipeOut - is based on Delete and wipe out all volume snapshots and snapshot - data from backup storage location. + description: Cluster termination policy. Valid values are DoNotTerminate, + Halt, Delete, WipeOut. DoNotTerminate will block delete operation. + Halt will delete workload resources such as statefulset, deployment + workloads but keep PVCs. Delete is based on Halt and deletes PVCs. + WipeOut is based on Delete and wipe out all volume snapshots and + snapshot data from backup storage location. enum: - DoNotTerminate - Halt @@ -493,7 +519,7 @@ spec: type: string tolerations: description: tolerations are attached to tolerate any taint that matches - the triple using the matching operator . + the triple `key,value,effect` using the matching operator `operator`. items: description: The pod this Toleration is attached to tolerates any taint that matches the triple using the matching @@ -538,7 +564,7 @@ spec: - terminationPolicy type: object status: - description: ClusterStatus defines the observed state of Cluster + description: ClusterStatus defines the observed state of Cluster. properties: clusterDefGeneration: description: clusterDefGeneration represents the generation number @@ -547,18 +573,20 @@ spec: type: integer components: additionalProperties: - description: ClusterComponentStatus record components status information + description: ClusterComponentStatus records components status. properties: consensusSetStatus: - description: consensusSetStatus role and pod name mapping. + description: consensusSetStatus specifies the mapping of role + and pod name. properties: followers: - description: followers status. + description: Followers status. items: properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this + pod provides. enum: - None - Readonly @@ -566,11 +594,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -579,11 +607,12 @@ spec: type: object type: array leader: - description: leader status. + description: Leader status. properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this pod + provides. enum: - None - Readonly @@ -591,11 +620,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -603,11 +632,12 @@ spec: - pod type: object learner: - description: learner status. + description: Learner status. properties: accessMode: default: ReadWrite - description: accessMode, what service this pod provides. + description: accessMode defines what service this pod + provides. enum: - None - Readonly @@ -615,11 +645,11 @@ spec: type: string name: default: leader - description: name role name. + description: Defines the role name. type: string pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - accessMode @@ -633,22 +663,22 @@ spec: additionalProperties: type: string description: message records the component details message in - current phase. keys are podName or deployName or statefulSetName, - the format is `/`. + current phase. Keys are podName or deployName or statefulSetName. + The format is `ObjectKind/Name`. type: object phase: - description: 'phase describes the phase of the component, the - detail information of the phases are as following: Running: - component is running. [terminal state] Stopped: component - is stopped, as no running pod. [terminal state] Failed: component - is unavailable. i.e, all pods are not ready for Stateless/Stateful - component, Leader/Primary pod is not ready for Consensus/Replication - component. [terminal state] Abnormal: component is running - but part of its pods are not ready. Leader/Primary pod is + description: 'phase describes the phase of the component and + the detail information of the phases are as following: Running: + the component is running. [terminal state] Stopped: the component + is stopped, as no running pod. [terminal state] Failed: the + component is unavailable, i.e. all pods are not ready for + Stateless/Stateful component and Leader/Primary pod is not ready for Consensus/Replication component. [terminal state] - Creating: component has entered creating process. Updating: - component has entered updating process, triggered by Spec. - updated.' + Abnormal: the component is running but part of its pods are + not ready. Leader/Primary pod is ready for Consensus/Replication + component. [terminal state] Creating: the component has entered + creating process. Updating: the component has entered updating + process, triggered by Spec. updated.' enum: - Running - Stopped @@ -668,25 +698,26 @@ spec: format: date-time type: string replicationSetStatus: - description: replicationSetStatus role and pod name mapping. + description: replicationSetStatus specifies the mapping of role + and pod name. properties: primary: - description: primary status. + description: Primary status. properties: pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - pod type: object secondaries: - description: secondaries status. + description: Secondaries status. items: properties: pod: default: Unknown - description: pod name. + description: Pod name. type: string required: - pod diff --git a/deploy/helm/crds/apps.kubeblocks.io_clusterversions.yaml b/deploy/helm/crds/apps.kubeblocks.io_clusterversions.yaml index 517450462..8d04ae87c 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_clusterversions.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_clusterversions.yaml @@ -100,6 +100,7 @@ spec: items: type: string type: array + x-kubernetes-list-type: set name: description: Specify the name of configuration template. maxLength: 63 @@ -134,6 +135,146 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + systemAccountSpec: + description: systemAccountSpec define image for the component + to connect database or engines. It overrides `image` and `env` + attributes defined in ClusterDefinition.spec.componentDefs.systemAccountSpec.cmdExecutorConfig. + To clean default envs settings, set `SystemAccountSpec.CmdExecutorConfig.Env` + to empty list. + properties: + cmdExecutorConfig: + description: cmdExecutorConfig configs how to get client + SDK and perform statements. + properties: + env: + description: envs is a list of environment variables. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + x-kubernetes-preserve-unknown-fields: true + image: + description: image for Connector when executing the + command. + type: string + required: + - image + type: object + required: + - cmdExecutorConfig + type: object versionsContext: description: versionContext defines containers images' context for component versions, this value replaces ClusterDefinition.spec.componentDefs.podSpec.[initContainers diff --git a/deploy/helm/crds/apps.kubeblocks.io_componentclassdefinitions.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentclassdefinitions.yaml new file mode 100644 index 000000000..9c8b10c7c --- /dev/null +++ b/deploy/helm/crds/apps.kubeblocks.io_componentclassdefinitions.yaml @@ -0,0 +1,170 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: componentclassdefinitions.apps.kubeblocks.io +spec: + group: apps.kubeblocks.io + names: + categories: + - kubeblocks + kind: ComponentClassDefinition + listKind: ComponentClassDefinitionList + plural: componentclassdefinitions + shortNames: + - ccd + singular: componentclassdefinition + scope: Cluster + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: ComponentClassDefinition is the Schema for the componentclassdefinitions + 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: ComponentClassDefinitionSpec defines the desired state of + ComponentClassDefinition + properties: + groups: + description: group defines a list of class series that conform to + the same constraint. + items: + properties: + resourceConstraintRef: + description: resourceConstraintRef reference to the resource + constraint object name, indicates that the series defined + below all conform to the constraint. + type: string + series: + description: series is a series of class definitions. + items: + properties: + classes: + description: classes are definitions of classes that come + in two forms. In the first form, only ComponentClass.Args + need to be defined, and the complete class definition + is generated by rendering the ComponentClassGroup.Template + and Name. In the second form, the Name, CPU and Memory + must be defined. + items: + properties: + args: + description: args are variable's value + items: + type: string + type: array + cpu: + anyOf: + - type: integer + - type: string + description: the CPU of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + description: the memory of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: name is the class name + type: string + type: object + type: array + namingTemplate: + description: 'namingTemplate is a template that uses the + Go template syntax and allows for referencing variables + defined in ComponentClassGroup.Template. This enables + dynamic generation of class names. For example: name: + "general-{{ .cpu }}c{{ .memory }}g"' + type: string + type: object + type: array + template: + description: "template is a class definition template that uses + the Go template syntax and allows for variable declaration. + When defining a class in Series, specifying the variable's + value is sufficient, as the complete class definition will + be generated through rendering the template. \n For example: + template: | cpu: \"{{ or .cpu 1 }}\" memory: \"{{ or .memory + 4 }}Gi\"" + type: string + vars: + description: vars defines the variables declared in the template + and will be used to generating the complete class definition + by render the template. + items: + type: string + type: array + x-kubernetes-list-type: set + required: + - resourceConstraintRef + type: object + type: array + type: object + status: + description: ComponentClassDefinitionStatus defines the observed state + of ComponentClassDefinition + properties: + classes: + description: classes is the list of classes that have been observed + for this ComponentClassDefinition + items: + properties: + args: + description: args are variable's value + items: + type: string + type: array + cpu: + anyOf: + - type: integer + - type: string + description: the CPU of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + memory: + anyOf: + - type: integer + - type: string + description: the memory of the class + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: name is the class name + type: string + resourceConstraintRef: + description: resourceConstraintRef reference to the resource + constraint object name. + type: string + type: object + type: array + observedGeneration: + description: observedGeneration is the most recent generation observed + for this ComponentClassDefinition. It corresponds to the ComponentClassDefinition's + generation, which is updated on mutation by the API Server. + format: int64 + type: integer + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/crds/apps.kubeblocks.io_classfamilies.yaml b/deploy/helm/crds/apps.kubeblocks.io_componentresourceconstraints.yaml similarity index 92% rename from deploy/helm/crds/apps.kubeblocks.io_classfamilies.yaml rename to deploy/helm/crds/apps.kubeblocks.io_componentresourceconstraints.yaml index 1c4f66d32..5f00d7070 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_classfamilies.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_componentresourceconstraints.yaml @@ -5,25 +5,26 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.9.0 creationTimestamp: null - name: classfamilies.apps.kubeblocks.io + name: componentresourceconstraints.apps.kubeblocks.io spec: group: apps.kubeblocks.io names: categories: - kubeblocks - all - kind: ClassFamily - listKind: ClassFamilyList - plural: classfamilies + kind: ComponentResourceConstraint + listKind: ComponentResourceConstraintList + plural: componentresourceconstraints shortNames: - - cf - singular: classfamily + - crc + singular: componentresourceconstraint scope: Cluster versions: - name: v1alpha1 schema: openAPIV3Schema: - description: ClassFamily is the Schema for the classfamilies API + description: ComponentResourceConstraint is the Schema for the componentresourceconstraints + API properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -38,11 +39,11 @@ spec: metadata: type: object spec: - description: ClassFamilySpec defines the desired state of ClassFamily + description: ComponentResourceConstraintSpec defines the desired state + of ComponentResourceConstraint properties: - models: - description: Class family models, generally, a model is a specific - constraint for CPU, memory and their relation. + constraints: + description: Component resource constraints items: properties: cpu: @@ -133,6 +134,9 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true type: object + required: + - cpu + - memory type: object type: array type: object diff --git a/deploy/helm/crds/apps.kubeblocks.io_configconstraints.yaml b/deploy/helm/crds/apps.kubeblocks.io_configconstraints.yaml index 5a6a5d3b0..c73a6b1d2 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_configconstraints.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_configconstraints.yaml @@ -284,16 +284,12 @@ spec: format: int64 type: integer phase: - allOf: - - enum: - - Available - - Unavailable - - enum: - - Available - - Unavailable - - Deleting description: phase is status of configuration template, when set to - AvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. + CCAvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion. + enum: + - Available + - Unavailable + - Deleting type: string type: object type: object diff --git a/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml b/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml index 0ab01c7f7..119abe42a 100644 --- a/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml +++ b/deploy/helm/crds/apps.kubeblocks.io_opsrequests.yaml @@ -60,9 +60,18 @@ spec: spec: description: OpsRequestSpec defines the desired state of OpsRequest properties: + cancel: + description: 'cancel defines the action to cancel the Pending/Creating/Running + opsRequest, supported types: [VerticalScaling, HorizontalScaling]. + once cancel is set to true, this opsRequest will be canceled and + modifying this property again will not take effect.' + type: boolean clusterRef: description: clusterRef references clusterDefinition. type: string + x-kubernetes-validations: + - message: forbidden to update spec.clusterRef + rule: self == oldSelf expose: description: expose defines services the component needs to expose. items: @@ -78,10 +87,11 @@ spec: additionalProperties: type: string description: 'If ServiceType is LoadBalancer, cloud provider - related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + related parameters can be put here More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -90,15 +100,16 @@ spec: LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not specified, - by manual construction of an Endpoints object or EndpointSlice - objects. If clusterIP is "None", no virtual IP is allocated - and the endpoints are published as a set of endpoints - rather than a virtual IP. "NodePort" builds on ClusterIP - and allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" builds - on NodePort and creates an external load-balancer (if - supported in the current cloud) which routes to the - same endpoints as the clusterIP. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + they are determined by manual construction of an Endpoints + object or EndpointSlice objects. If clusterIP is "None", + no virtual IP is allocated and the endpoints are published + as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node + which routes to the same endpoints as the clusterIP. + "LoadBalancer" builds on NodePort and creates an external + load-balancer (if supported in the current cloud) which + routes to the same endpoints as the clusterIP. More + info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -117,6 +128,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.expose + rule: self == oldSelf horizontalScaling: description: horizontalScaling defines what component need to horizontal scale the specified replicas. @@ -140,6 +154,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.horizontalScaling + rule: self == oldSelf reconfigure: description: reconfigure defines the variables that need to input when updating configuration. @@ -207,6 +224,9 @@ spec: - componentName - configurations type: object + x-kubernetes-validations: + - message: forbidden to update spec.reconfigure + rule: self == oldSelf restart: description: restart the specified component. items: @@ -223,6 +243,52 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.restart + rule: self == oldSelf + restoreFrom: + description: cluster RestoreFrom backup or point in time + properties: + backup: + description: use the backup name and component name for restore, + support for multiple components' recovery. + items: + properties: + ref: + description: specify a reference backup to restore + properties: + name: + description: specified the name + type: string + namespace: + description: specified the namespace + type: string + type: object + type: object + type: array + pointInTime: + description: specified the point in time to recovery + properties: + ref: + description: specify a reference source cluster to restore + properties: + name: + description: specified the name + type: string + namespace: + description: specified the namespace + type: string + type: object + time: + description: specify the time point to restore, with UTC as + the time zone. + format: date-time + type: string + type: object + type: object + x-kubernetes-validations: + - message: forbidden to update spec.restoreFrom + rule: self == oldSelf ttlSecondsAfterSucceed: description: ttlSecondsAfterSucceed OpsRequest will be deleted after TTLSecondsAfterSucceed second when OpsRequest.status.phase is Succeed. @@ -241,6 +307,9 @@ spec: - Stop - Expose type: string + x-kubernetes-validations: + - message: forbidden to update spec.type + rule: self == oldSelf upgrade: description: upgrade specifies the cluster version by specifying clusterVersionRef. properties: @@ -250,6 +319,9 @@ spec: required: - clusterVersionRef type: object + x-kubernetes-validations: + - message: forbidden to update spec.upgrade + rule: self == oldSelf verticalScaling: description: verticalScaling defines what component need to vertical scale the specified compute resources. @@ -277,6 +349,9 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + class: + description: class specifies the class name of the component + type: string componentName: description: componentName cluster component name. type: string @@ -310,6 +385,9 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.verticalScaling + rule: self == oldSelf volumeExpansion: description: volumeExpansion defines what component and volumeClaimTemplate need to expand the specified storage. @@ -352,13 +430,24 @@ spec: x-kubernetes-list-map-keys: - componentName x-kubernetes-list-type: map + x-kubernetes-validations: + - message: forbidden to update spec.volumeExpansion + rule: self == oldSelf required: - clusterRef - type type: object + x-kubernetes-validations: + - message: forbidden to cancel the opsRequest which type not in ['VerticalScaling','HorizontalScaling'] + rule: 'has(self.cancel) && self.cancel ? (self.type in [''VerticalScaling'', + ''HorizontalScaling'']) : true' status: description: OpsRequestStatus defines the observed state of OpsRequest properties: + cancelTimestamp: + description: CancelTimestamp defines cancel time. + format: date-time + type: string clusterGeneration: description: ClusterGeneration records the cluster generation after handling the opsRequest action. @@ -539,6 +628,9 @@ spec: x-kubernetes-list-map-keys: - name x-kubernetes-list-type: map + class: + description: the last class name of the component. + type: string limits: additionalProperties: anyOf: @@ -575,10 +667,11 @@ spec: type: string description: 'If ServiceType is LoadBalancer, cloud provider related parameters can be put here More - info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer' + info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.' type: object name: description: Service name + maxLength: 15 type: string serviceType: default: ClusterIP @@ -587,17 +680,17 @@ spec: and LoadBalancer. "ClusterIP" allocates a cluster-internal IP address for load-balancing to endpoints. Endpoints are determined by the selector or if that is not - specified, by manual construction of an Endpoints - object or EndpointSlice objects. If clusterIP is - "None", no virtual IP is allocated and the endpoints - are published as a set of endpoints rather than - a virtual IP. "NodePort" builds on ClusterIP and - allocates a port on every node which routes to the - same endpoints as the clusterIP. "LoadBalancer" + specified, they are determined by manual construction + of an Endpoints object or EndpointSlice objects. + If clusterIP is "None", no virtual IP is allocated + and the endpoints are published as a set of endpoints + rather than a virtual IP. "NodePort" builds on ClusterIP + and allocates a port on every node which routes + to the same endpoints as the clusterIP. "LoadBalancer" builds on NodePort and creates an external load-balancer (if supported in the current cloud) which routes to the same endpoints as the clusterIP. More info: - https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.' enum: - ClusterIP - NodePort @@ -649,6 +742,8 @@ spec: - Pending - Creating - Running + - Cancelling + - Cancelled - Failed - Succeed type: string diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicies.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicies.yaml index 6e359243d..b15056397 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicies.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicies.yaml @@ -14,6 +14,8 @@ spec: kind: BackupPolicy listKind: BackupPolicyList plural: backuppolicies + shortNames: + - bp singular: backuppolicy scope: Namespaced versions: @@ -21,9 +23,6 @@ spec: - jsonPath: .status.phase name: STATUS type: string - - jsonPath: .spec.schedule - name: SCHEDULE - type: string - jsonPath: .status.lastScheduleTime name: LAST SCHEDULE type: string @@ -51,1696 +50,569 @@ spec: spec: description: BackupPolicySpec defines the desired state of BackupPolicy properties: - backupPolicyTemplateName: - description: policy can inherit from backup config and override some - fields. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - backupStatusUpdates: - description: define how to update metadata for backup status. - items: - properties: - containerName: - description: which container name that kubectl can execute. - type: string - path: - description: 'specify the json path of backup object for patch. - example: manifests.backupLog -- means patch the backup json - path of status.manifests.backupLog.' - type: string - script: - description: the shell Script commands to collect backup status - metadata. The script must exist in the container of ContainerName - and the output format must be set to JSON. Note that outputting - to stderr may cause the result format to not be in JSON. - type: string - updateStage: - description: 'when to update the backup status, pre: before - backup, post: after backup' - enum: - - pre - - post - type: string - type: object - type: array - backupToolName: - description: which backup tool to perform database backup, only support - one tool. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - backupType: - default: snapshot - description: Backup ComponentDefRef. full or incremental or snapshot. - if unset, default is snapshot. - enum: - - full - - incremental - - snapshot - type: string - backupsHistoryLimit: - default: 7 - description: The number of automatic backups to retain. Value must - be non-negative integer. 0 means NO limit on the number of backups. - format: int32 - type: integer - hooks: - description: execute hook commands for backup. + datafile: + description: the policy for datafile backup. properties: - containerName: - description: which container can exec command - type: string - image: - description: exec command with image - type: string - postCommands: - description: post backup to perform commands + backupStatusUpdates: + description: define how to update metadata for backup status. items: - type: string - type: array - preCommands: - description: pre backup to perform commands - items: - type: string + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object type: array - type: object - onFailAttempted: - description: count of backup stop retries on fail. - format: int32 - type: integer - remoteVolume: - description: array of remote volumes from CSI driver definition. - properties: - awsElasticBlockStore: - description: 'awsElasticBlockStore represents an AWS Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty).' - format: int32 - type: integer - readOnly: - description: 'readOnly value true will force the readOnly - setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: boolean - volumeID: - description: 'volumeID is unique ID of the persistent disk - resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob storage - type: string - fsType: - description: fsType is Filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple blob - disks per storage account Dedicated: single blob disk per - storage account Managed: azure managed data disk (only - in managed availability set). defaults to shared' - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime + backupToolName: + description: which backup tool to perform database backup, only + support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + persistentVolumeClaim: + description: refer to PersistentVolumeClaim and the backup data + will be stored in the corresponding persistent volume. properties: - monitors: - description: 'monitors is Required: Monitors is a collection - of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' - type: string - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: boolean - secretFile: - description: 'secretFile is Optional: SecretFile is the path - to key ring for User, default is /etc/ceph/user.secret More - info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - secretRef: - description: 'secretRef is Optional: SecretRef is reference - to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + createPolicy: + default: IfNotPresent + description: 'createPolicy defines the policy for creating + the PersistentVolumeClaim, enum values: - Never: do nothing + if the PersistentVolumeClaim not exists. - IfNotPresent: + create the PersistentVolumeClaim if not present and the + accessModes only contains ''ReadWriteMany''.' + enum: + - IfNotPresent + - Never + type: string + initCapacity: + anyOf: + - type: integer + - type: string + description: initCapacity represents the init storage size + of the PersistentVolumeClaim which should be created if + not exist. and the default value is 100Gi if it is empty. + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + name: + description: the name of the PersistentVolumeClaim. + type: string + persistentVolumeConfigMap: + description: 'persistentVolumeConfigMap references the configmap + which contains a persistentVolume template. key must be + "persistentVolume" and value is the "PersistentVolume" struct. + support the following built-in Objects: - $(GENERATE_NAME): + generate a specific format "pvcName-pvcNamespace". if the + PersistentVolumeClaim not exists and CreatePolicy is "IfNotPresent", + the controller will create it by this template. this is + a mutually exclusive setting with "storageClassName".' 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?' + description: the name of the persistentVolume ConfigMap. type: string - type: object - user: - description: 'user is optional: User is the rados user name, - default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - required: - - monitors - type: object - cinder: - description: 'cinder represents a cinder volume attached and mounted - on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to - be "ext4" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - readOnly: - description: 'readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. More - info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: boolean - secretRef: - description: 'secretRef is optional: points to a secret object - containing parameters used to connect to OpenStack.' - 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?' + namespace: + description: the namespace of the persistentVolume ConfigMap. type: string + required: + - name + - namespace type: object - volumeID: - description: 'volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. type: string required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: 'defaultMode is optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items if unspecified, each key-value pair in - the Data field of the referenced ConfigMap will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - ConfigMap, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean + - name type: object - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). + target: + description: target database cluster for backup. properties: - driver: - description: driver is the name of the CSI driver that handles - this volume. Consult with your admin for the correct name - as registered in the cluster. - type: string - fsType: - description: fsType to mount. Ex. "ext4", "xfs", "ntfs". If - not provided, the empty value is passed to the associated - CSI driver which will determine the default filesystem to - apply. - type: string - nodePublishSecretRef: - description: nodePublishSecretRef is a reference to the secret - object containing sensitive information to pass to the CSI - driver to complete the CSI NodePublishVolume and NodeUnpublishVolume - calls. This field is optional, and may be empty if no secret - is required. If the secret object contains more than one - secret, all secret references are passed. + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. 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 - readOnly: - description: readOnly specifies a read-only configuration - for the volume. Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: volumeAttributes stores driver-specific properties - that are passed to the CSI driver. Consult your driver's - documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: - description: 'Optional: mode bits to use on created files - by default. Must be a Optional: mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Defaults to 0644. Directories within - the path are not affected by this setting. This might be - in conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits set.' - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are supported.' + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". + key: + description: key is the label key that the selector + applies to. type: string - fieldPath: - description: Path of the field to select in the - specified API version. + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array required: - - fieldPath + - key + - operator type: object - mode: - description: 'Optional: mode bits used to set permissions - on this file, must be an octal value between 0000 - and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative path name - of the file to be created. Must not be absolute or - contain the ''..'' path. Must be utf-8 encoded. The - first item of the relative path must not start with - ''..''' + type: array + matchLabels: + additionalProperties: type: string - resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - requests.cpu and requests.memory) are currently supported.' - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. + properties: + name: + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret + type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret + type: string + required: + - name + type: object + required: + - labelsSelector type: object - emptyDir: - description: 'emptyDir represents a temporary directory that shares - a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + required: + - persistentVolumeClaim + - target + type: object + logfile: + description: the policy for logfile backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupToolName: + description: which backup tool to perform database backup, only + support one tool. + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + persistentVolumeClaim: + description: refer to PersistentVolumeClaim and the backup data + will be stored in the corresponding persistent volume. properties: - medium: - description: 'medium represents what type of storage medium - should back this directory. The default is "" which means - to use the node''s default medium. Must be an empty string - (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - type: string - sizeLimit: + createPolicy: + default: IfNotPresent + description: 'createPolicy defines the policy for creating + the PersistentVolumeClaim, enum values: - Never: do nothing + if the PersistentVolumeClaim not exists. - IfNotPresent: + create the PersistentVolumeClaim if not present and the + accessModes only contains ''ReadWriteMany''.' + enum: + - IfNotPresent + - Never + type: string + initCapacity: anyOf: - type: integer - type: string - description: 'sizeLimit is the total amount of local storage - required for this EmptyDir volume. The size limit is also - applicable for memory medium. The maximum usage on memory - medium EmptyDir would be the minimum value between the SizeLimit - specified here and the sum of memory limits of all containers - in a pod. The default is nil which means that the limit - is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + description: initCapacity represents the init storage size + of the PersistentVolumeClaim which should be created if + not exist. and the default value is 100Gi if it is empty. pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true + name: + description: the name of the PersistentVolumeClaim. + type: string + persistentVolumeConfigMap: + description: 'persistentVolumeConfigMap references the configmap + which contains a persistentVolume template. key must be + "persistentVolume" and value is the "PersistentVolume" struct. + support the following built-in Objects: - $(GENERATE_NAME): + generate a specific format "pvcName-pvcNamespace". if the + PersistentVolumeClaim not exists and CreatePolicy is "IfNotPresent", + the controller will create it by this template. this is + a mutually exclusive setting with "storageClassName".' + properties: + name: + description: the name of the persistentVolume ConfigMap. + type: string + namespace: + description: the namespace of the persistentVolume ConfigMap. + type: string + required: + - name + - namespace + type: object + storageClassName: + description: storageClassName is the name of the StorageClass + required by the claim. + type: string + required: + - name type: object - ephemeral: - description: "ephemeral represents a volume that is handled by - a cluster storage driver. The volume's lifecycle is tied to - the pod that defines it - it will be created before the pod - starts, and deleted when the pod is removed. \n Use this if: - a) the volume is only needed while the pod runs, b) features - of normal volumes like restoring from snapshot or capacity tracking - are needed, c) the storage driver is specified through a storage - class, and d) the storage driver supports dynamic volume provisioning - through a PersistentVolumeClaim (see EphemeralVolumeSource for - more information on the connection between this volume type - and PersistentVolumeClaim). \n Use PersistentVolumeClaim or - one of the vendor-specific APIs for volumes that persist for - longer than the lifecycle of an individual pod. \n Use CSI for - light-weight local ephemeral volumes if the CSI driver is meant - to be used that way - see the documentation of the driver for - more information. \n A pod can use both types of ephemeral volumes - and persistent volumes at the same time." + target: + description: target database cluster for backup. properties: - volumeClaimTemplate: - description: "Will be used to create a stand-alone PVC to - provision the volume. The pod in which this EphemeralVolumeSource - is embedded will be the owner of the PVC, i.e. the PVC will - be deleted together with the pod. The name of the PVC will - be `-` where `` is the - name from the `PodSpec.Volumes` array entry. Pod validation - will reject the pod if the concatenated name is not valid - for a PVC (for example, too long). \n An existing PVC with - that name that is not owned by the pod will *not* be used - for the pod to avoid using an unrelated volume by mistake. - Starting the pod is then blocked until the unrelated PVC - is removed. If such a pre-created PVC is meant to be used - by the pod, the PVC has to updated with an owner reference - to the pod once the pod exists. Normally this should not - be necessary, but it may be useful when manually reconstructing - a broken cluster. \n This field is read-only and no changes - will be made by Kubernetes to the PVC after it has been - created. \n Required, must not be nil." + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. properties: - metadata: - description: May contain labels and annotations that will - be copied into the PVC when creating it. No other fields - are allowed and will be rejected during validation. - properties: - annotations: - additionalProperties: - type: string - type: object - finalizers: - items: - type: string - type: array - labels: - additionalProperties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. type: string - type: object - name: - type: string - namespace: - type: string - type: object - spec: - description: The specification for the PersistentVolumeClaim. - The entire content is copied unchanged into the PVC - that gets created from this template. The same fields - as in a PersistentVolumeClaim are also valid here. - properties: - accessModes: - description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. type: string - type: array - dataSource: - description: 'dataSource field can be used to specify - either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the - provisioner or an external controller can support - the specified data source, it will create a new - volume based on the contents of the specified data - source. When the AnyVolumeDataSource feature gate - is enabled, dataSource contents will be copied to - dataSourceRef, and dataSourceRef contents will be - copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, - then dataSourceRef will not be copied to dataSource.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name - type: object - dataSourceRef: - description: 'dataSourceRef specifies the object from - which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a - non-empty API group (non core object) or a PersistentVolumeClaim - object. When this field is specified, volume binding - will only succeed if the type of the specified object - matches some installed volume populator or dynamic - provisioner. This field will replace the functionality - of the dataSource field and as such if both fields - are non-empty, they must have the same value. For - backwards compatibility, when namespace isn''t specified - in dataSourceRef, both fields (dataSource and dataSourceRef) - will be set to the same value automatically if one - of them is empty and the other is non-empty. When - namespace is specified in dataSourceRef, dataSource - isn''t set to the same value and must be empty. - There are three important differences between dataSource - and dataSourceRef: * While dataSource only allows - two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim - objects. * While dataSource ignores disallowed values - (dropping them), dataSourceRef preserves all values, - and generates an error if a disallowed value is - specified. * While dataSource only allows local - objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource - feature gate to be enabled. (Alpha) Using the namespace - field of dataSourceRef requires the CrossNamespaceVolumeDataSource - feature gate to be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: - description: Namespace is the namespace of resource - being referenced Note that when a namespace - is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace - to allow that namespace's owner to accept the - reference. See the ReferenceGrant documentation - for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate - to be enabled. + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: type: string - required: - - kind - - name - type: object - resources: - description: 'resources represents the minimum resources - the volume should have. If RecoverVolumeExpansionFailure - feature is enabled users are allowed to specify - resource requirements that are lower than previous - value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, - defined in spec.resourceClaims, that are used - by this container. \n This is an alpha field - and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable." - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of - one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes - that resource available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is - omitted for a container, it defaults to Limits - if that is explicitly specified, otherwise to - an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement is - a selector that contains values, a key, and - an operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If - the operator is Exists or DoesNotExist, - the values array must be empty. This array - is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - storageClassName: - description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume - is required by the claim. Value of Filesystem is - implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference to - the PersistentVolume backing this claim. - type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. type: object - required: - - spec type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is attached - to a kubelet's host machine and then exposed to the pod. - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. TODO: how do we prevent errors in the filesystem - from compromising the machine' - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: 'wwids Optional: FC volume world wide identifiers - (wwids) Either wwids or combination of targetWWNs and lun - must be set, but not both simultaneously.' - items: - type: string - type: array - type: object - flexVolume: - description: flexVolume represents a generic volume resource that - is provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for this - volume. - type: string - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends - on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' - type: object - readOnly: - description: 'readOnly is Optional: defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - secretRef: - description: 'secretRef is Optional: secretRef is reference - to the secret object containing sensitive information to - pass to the plugin scripts. This may be empty if no secret - object is specified. If the secret object contains more - than one secret, all secrets are passed to the plugin scripts.' + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. 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?' + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ + type: string + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret + type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret type: string + required: + - name type: object required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to a - kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: datasetName is Name of the dataset stored as - metadata -> name on the dataset for Flocker should be considered - as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string + - labelsSelector type: object - gcePersistentDisk: - description: 'gcePersistentDisk represents a GCE Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + required: + - persistentVolumeClaim + - target + type: object + retention: + description: retention describe how long the Backup should be retained. + if not set, will be retained forever. + properties: + ttl: + description: ttl is a time string ending with the 'd'|'D'|'h'|'H' + character to describe how long the Backup should be retained. + if not set, will be retained forever. + pattern: ^\d+[d|D|h|H]$ + type: string + type: object + schedule: + description: schedule policy for backup. + properties: + datafile: + description: schedule policy for datafile backup. properties: - fsType: - description: 'fsType is filesystem type of the volume that - you want to mount. Tip: Ensure that the filesystem type - is supported by the host operating system. Examples: "ext4", - "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty). More - info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - format: int32 - type: integer - pdName: - description: 'pdName is unique name of the PD resource in - GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + enable: + description: enable or disable the schedule. type: boolean required: - - pdName + - cronExpression + - enable type: object - gitRepo: - description: 'gitRepo represents a git repository at a particular - revision. DEPRECATED: GitRepo is deprecated. To provision a - container with a git repo, mount an EmptyDir into an InitContainer - that clones the repo using git, then mount the EmptyDir into - the Pod''s container.' + logfile: + description: schedule policy for logfile backup. properties: - directory: - description: directory is the target directory name. Must - not contain or start with '..'. If '.' is supplied, the - volume directory will be the git repository. Otherwise, - if specified, the volume will contain the git repository - in the subdirectory with the given name. + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: 'glusterfs represents a Glusterfs mount on the host - that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' - properties: - endpoints: - description: 'endpoints is the endpoint name that details - Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - path: - description: 'path is the Glusterfs volume path. More info: - https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - readOnly: - description: 'readOnly here will force the Glusterfs volume - to be mounted with read-only permissions. Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + enable: + description: enable or disable the schedule. type: boolean required: - - endpoints - - path + - cronExpression + - enable type: object - hostPath: - description: 'hostPath represents a pre-existing file or directory - on the host machine that is directly exposed to the container. - This is generally used for system agents or other privileged - things that are allowed to see the host machine. Most containers - will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- TODO(jonesdl) We need to restrict who can use host directory - mounts and who can/can not mount host directories as read/write.' + snapshot: + description: schedule policy for snapshot backup. properties: - path: - description: 'path of the directory on the host. If the path - is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - type: - description: 'type for HostPath Volume Defaults to "" More - info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + cronExpression: + description: the cron expression for schedule, the timezone + is in UTC. see https://en.wikipedia.org/wiki/Cron. type: string + enable: + description: enable or disable the schedule. + type: boolean required: - - path + - cronExpression + - enable type: object - iscsi: - description: 'iscsi represents an ISCSI Disk resource that is - attached to a kubelet''s host machine and then exposed to the - pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + type: object + snapshot: + description: the policy for snapshot backup. + properties: + backupStatusUpdates: + description: define how to update metadata for backup status. + items: + properties: + containerName: + description: which container name that kubectl can execute. + type: string + path: + description: 'specify the json path of backup object for + patch. example: manifests.backupLog -- means patch the + backup json path of status.manifests.backupLog.' + type: string + script: + description: the shell Script commands to collect backup + status metadata. The script must exist in the container + of ContainerName and the output format must be set to + JSON. Note that outputting to stderr may cause the result + format to not be in JSON. + type: string + updateStage: + description: 'when to update the backup status, pre: before + backup, post: after backup' + enum: + - pre + - post + type: string + type: object + type: array + backupsHistoryLimit: + default: 7 + description: the number of automatic backups to retain. Value + must be non-negative integer. 0 means NO limit on the number + of backups. + format: int32 + type: integer + hooks: + description: execute hook commands for backup. properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - initiatorName: - description: initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, - new iSCSI interface : will be - created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. + containerName: + description: which container can exec command type: string - iscsiInterface: - description: iscsiInterface is the interface Name that uses - an iSCSI transport. Defaults to 'default' (tcp). + image: + description: exec command with image type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: portals is the iSCSI Target Portal List. The - portal is either an IP or ip_addr:port if the port is other - than default (typically TCP ports 860 and 3260). + postCommands: + description: post backup to perform commands + items: + type: string + type: array + preCommands: + description: pre backup to perform commands items: type: string type: array - readOnly: - description: readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication - 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 - targetPortal: - description: targetPortal is iSCSI Target Portal. The Portal - is either an IP or ip_addr:port if the port is other than - default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: 'name of the volume. Must be a DNS_LABEL and unique - within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - nfs: - description: 'nfs represents an NFS mount on the host that shares - a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - properties: - path: - description: 'path that is exported by the NFS server. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - readOnly: - description: 'readOnly here will force the NFS export to be - mounted with read-only permissions. Defaults to false. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: boolean - server: - description: 'server is the hostname or IP address of the - NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: 'persistentVolumeClaimVolumeSource represents a reference - to a PersistentVolumeClaim in the same namespace. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - properties: - claimName: - description: 'claimName is the name of a PersistentVolumeClaim - in the same namespace as the pod using this volume. More - info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - type: string - readOnly: - description: readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating - system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API + onFailAttempted: + description: count of backup stop retries on fail. + format: int32 + type: integer + target: + description: target database cluster for backup. properties: - defaultMode: - description: defaultMode are the mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Directories within the path are not - affected by this setting. This might be in conflict with - other options that affect the file mode, like fsGroup, and - the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project + labelsSelector: + description: labelsSelector is used to find matching pods. + Pods that match this label selector are counted to determine + the number of pods in their corresponding topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced ConfigMap - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the ConfigMap, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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?' + key: + description: key is the label key that the selector + applies to. type: string - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing the - pod field - properties: - fieldRef: - description: 'Required: Selects a field of - the pod: only annotations, labels, name - and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, defaults - to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to - set permissions on this file, must be an - octal value between 0000 and 0777 or a decimal - value between 0 and 511. YAML accepts both - octal and decimal values, JSON requires - decimal values for mode bits. If not specified, - the volume defaultMode will be used. This - might be in conflict with other options - that affect the file mode, like fsGroup, - and the result can be other mode bits set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' path. - Must be utf-8 encoded. The first item of - the relative path must not start with ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: - only resources limits and requests (limits.cpu, - limits.memory, requests.cpu and requests.memory) - are currently supported.' - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced Secret - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the Secret, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, NotIn, + Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists or + DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object + type: string type: array - 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 - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean - type: object - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project - properties: - audience: - description: audience is the intended audience of - the token. A recipient of a token must identify - itself with an identifier specified in the audience - of the token, and otherwise should reject the - token. The audience defaults to the identifier - of the apiserver. - type: string - expirationSeconds: - description: expirationSeconds is the requested - duration of validity of the service account token. - As the token approaches expiration, the kubelet - volume plugin will proactively rotate the service - account token. The kubelet will start trying to - rotate the token if the token is older than 80 - percent of its time to live or if the token is - older than 24 hours.Defaults to 1 hour and must - be at least 10 minutes. - format: int64 - type: integer - path: - description: path is the path relative to the mount - point of the file to project the token into. - type: string required: - - path + - key + - operator type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host that - shares a pod's lifetime - properties: - group: - description: group to map volume access to Default is no group - type: string - readOnly: - description: readOnly here will force the Quobyte volume to - be mounted with read-only permissions. Defaults to false. - type: boolean - registry: - description: registry represents a single or multiple Quobyte - Registry services specified as a string as host:port pair - (multiple entries are separated with commas) which acts - as the central registry for volumes - type: string - tenant: - description: tenant owning the given Quobyte volume in the - Backend Used with dynamically provisioned Quobyte volumes, - value is set by the plugin - type: string - user: - description: user to map volume access to Defaults to serivceaccount - user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: 'rbd represents a Rados Block Device mount on the - host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - image: - description: 'image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - keyring: - description: 'keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - monitors: - description: 'monitors is a collection of Ceph monitors. More - info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - items: - type: string - type: array - pool: - description: 'pool is the rados pool name. Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: boolean - secretRef: - description: 'secretRef is name of the authentication secret - for RBDUser. If provided overrides keyring. Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - 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: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field is + "key", the operator is "In", and the values array contains + only "value". The requirements are ANDed. + type: object type: object - user: - description: 'user is the rados user name. Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO API - Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO Protection - Domain for the configured storage. - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef references to the secret for ScaleIO - user and other sensitive information. If this is not provided, - Login operation will fail. + x-kubernetes-preserve-unknown-fields: true + secret: + description: secret is used to connect to the target database + cluster. If not set, secret will be inherited from backup + policy template. if still not set, the controller will check + if any system account for dataprotection has been created. 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?' + description: the secret name + pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string - type: object - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage for - a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as configured - in ScaleIO. - type: string - volumeName: - description: volumeName is the name of a volume already created - in the ScaleIO system that is associated with this volume - source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: 'secret represents a secret that should populate - this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items If unspecified, each key-value pair in - the Data field of the referenced Secret will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - Secret, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: 'secretName is the name of the secret in the - pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef specifies the secret to use for obtaining - the StorageOS API credentials. If not specified, default - values will be attempted. - 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?' + passwordKey: + default: password + description: passwordKey the map key of the password in + the connection credential secret type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the + connection credential secret + type: string + required: + - name type: object - volumeName: - description: volumeName is the human-readable name of the - StorageOS volume. Volume names are only unique within a - namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of the volume - within StorageOS. If no namespace is specified then the - Pod's namespace will be used. This allows the Kubernetes - name scoping to be mirrored within StorageOS for tighter - integration. Set VolumeName to any name to override the - default behaviour. Set to "default" if you are not using - namespaces within StorageOS. Namespaces that do not pre-exist - within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fsType is filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based Management - (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object - schedule: - description: The schedule in Cron format, the timezone is in UTC. - see https://en.wikipedia.org/wiki/Cron. - type: string - target: - description: database cluster service - properties: - labelsSelector: - description: LabelSelector is used to find matching pods. Pods - that match this label selector are counted to determine the - number of pods in their corresponding topology domain. - properties: - matchExpressions: - description: matchExpressions is a list of label selector - requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector - that contains values, a key, and an operator that relates - the key and values. - properties: - key: - description: key is the label key that the selector - applies to. - type: string - operator: - description: operator represents a key's relationship - to a set of values. Valid operators are In, NotIn, - Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If - the operator is In or NotIn, the values array must - be non-empty. If the operator is Exists or DoesNotExist, - the values array must be empty. This array is replaced - during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A - single {key,value} in the matchLabels map is equivalent - to an element of matchExpressions, whose key field is "key", - the operator is "In", and the values array contains only - "value". The requirements are ANDed. - type: object - type: object - x-kubernetes-preserve-unknown-fields: true - secret: - description: Secret is used to connect to the target database - cluster. If not set, secret will be inherited from backup policy - template. if still not set, the controller will check if any - system account for dataprotection has been created. - properties: - name: - description: the secret name - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - passwordKeyword: - description: PasswordKeyword the map keyword of the password - in the connection credential secret - type: string - userKeyword: - description: UserKeyword the map keyword of the user in the - connection credential secret - type: string required: - - name + - labelsSelector type: object required: - - labelsSelector + - target type: object - ttl: - description: TTL is a time.Duration-parseable string describing how - long the Backup should be retained for. - type: string - required: - - remoteVolume - - target type: object status: description: BackupPolicyStatus defines the observed state of BackupPolicy @@ -1749,22 +621,25 @@ spec: description: the reason if backup policy check failed. type: string lastScheduleTime: - description: Information when was the last time the job was successfully + description: information when was the last time the job was successfully scheduled. format: date-time type: string lastSuccessfulTime: - description: Information when was the last time the job successfully + description: information when was the last time the job successfully completed. format: date-time type: string + observedGeneration: + description: observedGeneration is the most recent generation observed + for this BackupPolicy. It corresponds to the Cluster's generation, + which is updated on mutation by the API Server. + format: int64 + type: integer phase: - description: 'backup policy phase valid value: available, failed, - new.' + description: 'backup policy phase valid value: Available, Failed.' enum: - - New - Available - - InProgress - Failed type: string type: object diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml deleted file mode 100644 index ea15b74c6..000000000 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backuppolicytemplates.yaml +++ /dev/null @@ -1,144 +0,0 @@ ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.9.0 - creationTimestamp: null - name: backuppolicytemplates.dataprotection.kubeblocks.io -spec: - group: dataprotection.kubeblocks.io - names: - categories: - - kubeblocks - kind: BackupPolicyTemplate - listKind: BackupPolicyTemplateList - plural: backuppolicytemplates - singular: backuppolicytemplate - scope: Cluster - versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: BackupPolicyTemplate is the Schema for the BackupPolicyTemplates - API (defined by provider) - 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: BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate - properties: - backupStatusUpdates: - description: define how to update metadata for backup status. - items: - properties: - containerName: - description: which container name that kubectl can execute. - type: string - path: - description: 'specify the json path of backup object for patch. - example: manifests.backupLog -- means patch the backup json - path of status.manifests.backupLog.' - type: string - script: - description: the shell Script commands to collect backup status - metadata. The script must exist in the container of ContainerName - and the output format must be set to JSON. Note that outputting - to stderr may cause the result format to not be in JSON. - type: string - updateStage: - description: 'when to update the backup status, pre: before - backup, post: after backup' - enum: - - pre - - post - type: string - type: object - type: array - backupToolName: - description: which backup tool to perform database backup, only support - one tool. - pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ - type: string - credentialKeyword: - description: CredentialKeyword determines backupTool connection credential - keyword in secret. the backupTool gets the credentials according - to the user and password keyword defined by secret - properties: - passwordKeyword: - default: password - description: PasswordKeyword the map keyword of the password in - the connection credential secret - type: string - userKeyword: - default: username - description: UserKeyword the map keyword of the user in the connection - credential secret - type: string - type: object - hooks: - description: execute hook commands for backup. - properties: - containerName: - description: which container can exec command - type: string - image: - description: exec command with image - type: string - postCommands: - description: post backup to perform commands - items: - type: string - type: array - preCommands: - description: pre backup to perform commands - items: - type: string - type: array - type: object - onFailAttempted: - description: limit count of backup stop retries on fail. if unset, - retry unlimit attempted. - format: int32 - type: integer - schedule: - description: The schedule in Cron format, see https://en.wikipedia.org/wiki/Cron. - type: string - ttl: - description: TTL is a time.Duration-parseable string describing how - long the Backup should be retained for. - type: string - required: - - backupToolName - type: object - status: - description: BackupPolicyTemplateStatus defines the observed state of - BackupPolicyTemplate - properties: - failureReason: - type: string - phase: - description: BackupPolicyTemplatePhase defines phases for BackupPolicyTemplate - CR. - enum: - - New - - Available - - InProgress - - Failed - type: string - type: object - type: object - served: true - storage: true - subresources: - status: {} diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml index 188fb3fb3..d8ce45b88 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backups.yaml @@ -39,7 +39,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Backup is the Schema for the backups API (defined by User) + description: Backup is the Schema for the backups API (defined by User). properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -54,37 +54,33 @@ spec: metadata: type: object spec: - description: BackupSpec defines the desired state of Backup + description: BackupSpec defines the desired state of Backup. properties: backupPolicyName: - description: which backupPolicy to perform this backup + description: Which backupPolicy is applied to perform this backup pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string backupType: - default: full - description: Backup Type. full or incremental or snapshot. if unset, - default is full. + default: datafile + description: Backup Type. datafile or logfile or snapshot. If not + set, datafile is the default type. enum: - - full - - incremental + - datafile + - logfile - snapshot type: string parentBackupName: description: if backupType is incremental, parentBackupName is required. type: string - ttl: - description: ttl is a time.Duration-parsable string describing how - long the Backup should be retained for. - type: string required: - backupPolicyName - backupType type: object status: - description: BackupStatus defines the observed state of Backup + description: BackupStatus defines the observed state of Backup. properties: backupToolName: - description: backupToolName referenced backup tool name. + description: backupToolName references the backup tool name. type: string completionTimestamp: description: Date/time when the backup finished being processed. @@ -101,26 +97,26 @@ spec: format: date-time type: string failureReason: - description: the reason if backup failed. + description: The reason for a backup failure. type: string manifests: - description: manifests determines the backup metadata info + description: manifests determines the backup metadata info. properties: backupLog: description: backupLog records startTime and stopTime of data - logging + logging. properties: startTime: - description: startTime record start time of data logging + description: startTime records the start time of data logging. format: date-time type: string stopTime: - description: stopTime record start time of data logging + description: stopTime records the stop time of data logging. format: date-time type: string type: object backupSnapshot: - description: snapshot records the volume snapshot metadata + description: snapshot records the volume snapshot metadata. properties: volumeSnapshotContentName: description: volumeSnapshotContentName specifies the name @@ -130,35 +126,32 @@ spec: in Kubernetes. This field is immutable. type: string volumeSnapshotName: - description: volumeSnapshotName record the volumeSnapshot - name + description: volumeSnapshotName records the volumeSnapshot + name. type: string type: object backupTool: description: backupTool records information about backup files generated by the backup tool. properties: - CheckPoint: - description: backup check point, for incremental backup. - type: string - backupToolName: - description: backupToolName referenced backup tool name. + checkpoint: + description: backup checkpoint, for incremental backup. type: string - checkSum: + checksum: description: checksum of backup file, generated by md5 or - sha1 or sha256 + sha1 or sha256. type: string filePath: description: filePath records the file path of backup. type: string uploadTotalSize: - description: backup upload total size string with capacity + description: Backup upload total size. A string with capacity units in the form of "1Gi", "1Mi", "1Ki". type: string type: object target: description: target records the target cluster metadata string, - which are in JSON format. + which is in JSON format. type: string userContext: additionalProperties: @@ -168,7 +161,10 @@ spec: type: object type: object parentBackupName: - description: record parentBackupName if backupType is incremental. + description: Records parentBackupName if backupType is incremental. + type: string + persistentVolumeClaimName: + description: remoteVolume saves the backup data. type: string phase: description: BackupPhase The current phase. Valid values are New, @@ -179,1542 +175,13 @@ spec: - Completed - Failed type: string - remoteVolume: - description: remoteVolume saves the backup data. - properties: - awsElasticBlockStore: - description: 'awsElasticBlockStore represents an AWS Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty).' - format: int32 - type: integer - readOnly: - description: 'readOnly value true will force the readOnly - setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: boolean - volumeID: - description: 'volumeID is unique ID of the persistent disk - resource in AWS (Amazon EBS volume). More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' - type: string - required: - - volumeID - type: object - azureDisk: - description: azureDisk represents an Azure Data Disk mount on - the host and bind mount to the pod. - properties: - cachingMode: - description: 'cachingMode is the Host Caching mode: None, - Read Only, Read Write.' - type: string - diskName: - description: diskName is the Name of the data disk in the - blob storage - type: string - diskURI: - description: diskURI is the URI of data disk in the blob storage - type: string - fsType: - description: fsType is Filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - kind: - description: 'kind expected values are Shared: multiple blob - disks per storage account Dedicated: single blob disk per - storage account Managed: azure managed data disk (only - in managed availability set). defaults to shared' - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - required: - - diskName - - diskURI - type: object - azureFile: - description: azureFile represents an Azure File Service mount - on the host and bind mount to the pod. - properties: - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretName: - description: secretName is the name of secret that contains - Azure Storage Account Name and Key - type: string - shareName: - description: shareName is the azure share Name - type: string - required: - - secretName - - shareName - type: object - cephfs: - description: cephFS represents a Ceph FS mount on the host that - shares a pod's lifetime - properties: - monitors: - description: 'monitors is Required: Monitors is a collection - of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - items: - type: string - type: array - path: - description: 'path is Optional: Used as the mounted root, - rather than the full Ceph tree, default is /' - type: string - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: boolean - secretFile: - description: 'secretFile is Optional: SecretFile is the path - to key ring for User, default is /etc/ceph/user.secret More - info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - secretRef: - description: 'secretRef is Optional: SecretRef is reference - to the authentication secret for User, default is empty. - More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - 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 - user: - description: 'user is optional: User is the rados user name, - default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' - type: string - required: - - monitors - type: object - cinder: - description: 'cinder represents a cinder volume attached and mounted - on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Examples: "ext4", "xfs", "ntfs". Implicitly inferred to - be "ext4" if unspecified. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - readOnly: - description: 'readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. More - info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: boolean - secretRef: - description: 'secretRef is optional: points to a secret object - containing parameters used to connect to OpenStack.' - 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 - volumeID: - description: 'volumeID used to identify the volume in cinder. - More info: https://examples.k8s.io/mysql-cinder-pd/README.md' - type: string - required: - - volumeID - type: object - configMap: - description: configMap represents a configMap that should populate - this volume - properties: - defaultMode: - description: 'defaultMode is optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items if unspecified, each key-value pair in - the Data field of the referenced ConfigMap will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - ConfigMap, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap or its - keys must be defined - type: boolean - type: object - csi: - description: csi (Container Storage Interface) represents ephemeral - storage that is handled by certain external CSI drivers (Beta - feature). - properties: - driver: - description: driver is the name of the CSI driver that handles - this volume. Consult with your admin for the correct name - as registered in the cluster. - type: string - fsType: - description: fsType to mount. Ex. "ext4", "xfs", "ntfs". If - not provided, the empty value is passed to the associated - CSI driver which will determine the default filesystem to - apply. - type: string - nodePublishSecretRef: - description: nodePublishSecretRef is a reference to the secret - object containing sensitive information to pass to the CSI - driver to complete the CSI NodePublishVolume and NodeUnpublishVolume - calls. This field is optional, and may be empty if no secret - is required. If the secret object contains more than one - secret, all secret references are passed. - 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 - readOnly: - description: readOnly specifies a read-only configuration - for the volume. Defaults to false (read/write). - type: boolean - volumeAttributes: - additionalProperties: - type: string - description: volumeAttributes stores driver-specific properties - that are passed to the CSI driver. Consult your driver's - documentation for supported values. - type: object - required: - - driver - type: object - downwardAPI: - description: downwardAPI represents downward API about the pod - that should populate this volume - properties: - defaultMode: - description: 'Optional: mode bits to use on created files - by default. Must be a Optional: mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Defaults to 0644. Directories within - the path are not affected by this setting. This might be - in conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits set.' - format: int32 - type: integer - items: - description: Items is a list of downward API volume file - items: - description: DownwardAPIVolumeFile represents information - to create the file containing the pod field - properties: - fieldRef: - description: 'Required: Selects a field of the pod: - only annotations, labels, name and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the FieldPath - is written in terms of, defaults to "v1". - type: string - fieldPath: - description: Path of the field to select in the - specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to set permissions - on this file, must be an octal value between 0000 - and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative path name - of the file to be created. Must not be absolute or - contain the ''..'' path. Must be utf-8 encoded. The - first item of the relative path must not start with - ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: only - resources limits and requests (limits.cpu, limits.memory, - requests.cpu and requests.memory) are currently supported.' - properties: - containerName: - description: 'Container name: required for volumes, - optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format of the - exposed resources, defaults to "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - emptyDir: - description: 'emptyDir represents a temporary directory that shares - a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - properties: - medium: - description: 'medium represents what type of storage medium - should back this directory. The default is "" which means - to use the node''s default medium. Must be an empty string - (default) or Memory. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' - type: string - sizeLimit: - anyOf: - - type: integer - - type: string - description: 'sizeLimit is the total amount of local storage - required for this EmptyDir volume. The size limit is also - applicable for memory medium. The maximum usage on memory - medium EmptyDir would be the minimum value between the SizeLimit - specified here and the sum of memory limits of all containers - in a pod. The default is nil which means that the limit - is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - type: object - ephemeral: - description: "ephemeral represents a volume that is handled by - a cluster storage driver. The volume's lifecycle is tied to - the pod that defines it - it will be created before the pod - starts, and deleted when the pod is removed. \n Use this if: - a) the volume is only needed while the pod runs, b) features - of normal volumes like restoring from snapshot or capacity tracking - are needed, c) the storage driver is specified through a storage - class, and d) the storage driver supports dynamic volume provisioning - through a PersistentVolumeClaim (see EphemeralVolumeSource for - more information on the connection between this volume type - and PersistentVolumeClaim). \n Use PersistentVolumeClaim or - one of the vendor-specific APIs for volumes that persist for - longer than the lifecycle of an individual pod. \n Use CSI for - light-weight local ephemeral volumes if the CSI driver is meant - to be used that way - see the documentation of the driver for - more information. \n A pod can use both types of ephemeral volumes - and persistent volumes at the same time." - properties: - volumeClaimTemplate: - description: "Will be used to create a stand-alone PVC to - provision the volume. The pod in which this EphemeralVolumeSource - is embedded will be the owner of the PVC, i.e. the PVC will - be deleted together with the pod. The name of the PVC will - be `-` where `` is the - name from the `PodSpec.Volumes` array entry. Pod validation - will reject the pod if the concatenated name is not valid - for a PVC (for example, too long). \n An existing PVC with - that name that is not owned by the pod will *not* be used - for the pod to avoid using an unrelated volume by mistake. - Starting the pod is then blocked until the unrelated PVC - is removed. If such a pre-created PVC is meant to be used - by the pod, the PVC has to updated with an owner reference - to the pod once the pod exists. Normally this should not - be necessary, but it may be useful when manually reconstructing - a broken cluster. \n This field is read-only and no changes - will be made by Kubernetes to the PVC after it has been - created. \n Required, must not be nil." - properties: - metadata: - description: May contain labels and annotations that will - be copied into the PVC when creating it. No other fields - are allowed and will be rejected during validation. - properties: - annotations: - additionalProperties: - type: string - type: object - finalizers: - items: - type: string - type: array - labels: - additionalProperties: - type: string - type: object - name: - type: string - namespace: - type: string - type: object - spec: - description: The specification for the PersistentVolumeClaim. - The entire content is copied unchanged into the PVC - that gets created from this template. The same fields - as in a PersistentVolumeClaim are also valid here. - properties: - accessModes: - description: 'accessModes contains the desired access - modes the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' - items: - type: string - type: array - dataSource: - description: 'dataSource field can be used to specify - either: * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) - * An existing PVC (PersistentVolumeClaim) If the - provisioner or an external controller can support - the specified data source, it will create a new - volume based on the contents of the specified data - source. When the AnyVolumeDataSource feature gate - is enabled, dataSource contents will be copied to - dataSourceRef, and dataSourceRef contents will be - copied to dataSource when dataSourceRef.namespace - is not specified. If the namespace is specified, - then dataSourceRef will not be copied to dataSource.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - required: - - kind - - name - type: object - dataSourceRef: - description: 'dataSourceRef specifies the object from - which to populate the volume with data, if a non-empty - volume is desired. This may be any object from a - non-empty API group (non core object) or a PersistentVolumeClaim - object. When this field is specified, volume binding - will only succeed if the type of the specified object - matches some installed volume populator or dynamic - provisioner. This field will replace the functionality - of the dataSource field and as such if both fields - are non-empty, they must have the same value. For - backwards compatibility, when namespace isn''t specified - in dataSourceRef, both fields (dataSource and dataSourceRef) - will be set to the same value automatically if one - of them is empty and the other is non-empty. When - namespace is specified in dataSourceRef, dataSource - isn''t set to the same value and must be empty. - There are three important differences between dataSource - and dataSourceRef: * While dataSource only allows - two specific types of objects, dataSourceRef allows - any non-core object, as well as PersistentVolumeClaim - objects. * While dataSource ignores disallowed values - (dropping them), dataSourceRef preserves all values, - and generates an error if a disallowed value is - specified. * While dataSource only allows local - objects, dataSourceRef allows objects in any namespaces. - (Beta) Using this field requires the AnyVolumeDataSource - feature gate to be enabled. (Alpha) Using the namespace - field of dataSourceRef requires the CrossNamespaceVolumeDataSource - feature gate to be enabled.' - properties: - apiGroup: - description: APIGroup is the group for the resource - being referenced. If APIGroup is not specified, - the specified Kind must be in the core API group. - For any other third-party types, APIGroup is - required. - type: string - kind: - description: Kind is the type of resource being - referenced - type: string - name: - description: Name is the name of resource being - referenced - type: string - namespace: - description: Namespace is the namespace of resource - being referenced Note that when a namespace - is specified, a gateway.networking.k8s.io/ReferenceGrant - object is required in the referent namespace - to allow that namespace's owner to accept the - reference. See the ReferenceGrant documentation - for details. (Alpha) This field requires the - CrossNamespaceVolumeDataSource feature gate - to be enabled. - type: string - required: - - kind - - name - type: object - resources: - description: 'resources represents the minimum resources - the volume should have. If RecoverVolumeExpansionFailure - feature is enabled users are allowed to specify - resource requirements that are lower than previous - value but must still be higher than capacity recorded - in the status field of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' - properties: - claims: - description: "Claims lists the names of resources, - defined in spec.resourceClaims, that are used - by this container. \n This is an alpha field - and requires enabling the DynamicResourceAllocation - feature gate. \n This field is immutable." - items: - description: ResourceClaim references one entry - in PodSpec.ResourceClaims. - properties: - name: - description: Name must match the name of - one entry in pod.spec.resourceClaims of - the Pod where this field is used. It makes - that resource available inside a container. - type: string - required: - - name - type: object - type: array - x-kubernetes-list-map-keys: - - name - x-kubernetes-list-type: map - limits: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - requests: - additionalProperties: - anyOf: - - type: integer - - type: string - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - description: 'Requests describes the minimum amount - of compute resources required. If Requests is - omitted for a container, it defaults to Limits - if that is explicitly specified, otherwise to - an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' - type: object - type: object - selector: - description: selector is a label query over volumes - to consider for binding. - properties: - matchExpressions: - description: matchExpressions is a list of label - selector requirements. The requirements are - ANDed. - items: - description: A label selector requirement is - a selector that contains values, a key, and - an operator that relates the key and values. - properties: - key: - description: key is the label key that the - selector applies to. - type: string - operator: - description: operator represents a key's - relationship to a set of values. Valid - operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string - values. If the operator is In or NotIn, - the values array must be non-empty. If - the operator is Exists or DoesNotExist, - the values array must be empty. This array - is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} - pairs. A single {key,value} in the matchLabels - map is equivalent to an element of matchExpressions, - whose key field is "key", the operator is "In", - and the values array contains only "value". - The requirements are ANDed. - type: object - type: object - storageClassName: - description: 'storageClassName is the name of the - StorageClass required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' - type: string - volumeMode: - description: volumeMode defines what type of volume - is required by the claim. Value of Filesystem is - implied when not included in claim spec. - type: string - volumeName: - description: volumeName is the binding reference to - the PersistentVolume backing this claim. - type: string - type: object - required: - - spec - type: object - type: object - fc: - description: fc represents a Fibre Channel resource that is attached - to a kubelet's host machine and then exposed to the pod. - properties: - fsType: - description: 'fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. TODO: how do we prevent errors in the filesystem - from compromising the machine' - type: string - lun: - description: 'lun is Optional: FC target lun number' - format: int32 - type: integer - readOnly: - description: 'readOnly is Optional: Defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - targetWWNs: - description: 'targetWWNs is Optional: FC target worldwide - names (WWNs)' - items: - type: string - type: array - wwids: - description: 'wwids Optional: FC volume world wide identifiers - (wwids) Either wwids or combination of targetWWNs and lun - must be set, but not both simultaneously.' - items: - type: string - type: array - type: object - flexVolume: - description: flexVolume represents a generic volume resource that - is provisioned/attached using an exec based plugin. - properties: - driver: - description: driver is the name of the driver to use for this - volume. - type: string - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". The default filesystem depends - on FlexVolume script. - type: string - options: - additionalProperties: - type: string - description: 'options is Optional: this field holds extra - command options if any.' - type: object - readOnly: - description: 'readOnly is Optional: defaults to false (read/write). - ReadOnly here will force the ReadOnly setting in VolumeMounts.' - type: boolean - secretRef: - description: 'secretRef is Optional: secretRef is reference - to the secret object containing sensitive information to - pass to the plugin scripts. This may be empty if no secret - object is specified. If the secret object contains more - than one secret, all secrets are passed to the plugin scripts.' - 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 - required: - - driver - type: object - flocker: - description: flocker represents a Flocker volume attached to a - kubelet's host machine. This depends on the Flocker control - service being running - properties: - datasetName: - description: datasetName is Name of the dataset stored as - metadata -> name on the dataset for Flocker should be considered - as deprecated - type: string - datasetUUID: - description: datasetUUID is the UUID of the dataset. This - is unique identifier of a Flocker dataset - type: string - type: object - gcePersistentDisk: - description: 'gcePersistentDisk represents a GCE Disk resource - that is attached to a kubelet''s host machine and then exposed - to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - properties: - fsType: - description: 'fsType is filesystem type of the volume that - you want to mount. Tip: Ensure that the filesystem type - is supported by the host operating system. Examples: "ext4", - "xfs", "ntfs". Implicitly inferred to be "ext4" if unspecified. - More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - partition: - description: 'partition is the partition in the volume that - you want to mount. If omitted, the default is to mount by - volume name. Examples: For volume /dev/sda1, you specify - the partition as "1". Similarly, the volume partition for - /dev/sda is "0" (or you can leave the property empty). More - info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - format: int32 - type: integer - pdName: - description: 'pdName is unique name of the PD resource in - GCE. Used to identify the disk in GCE. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' - type: boolean - required: - - pdName - type: object - gitRepo: - description: 'gitRepo represents a git repository at a particular - revision. DEPRECATED: GitRepo is deprecated. To provision a - container with a git repo, mount an EmptyDir into an InitContainer - that clones the repo using git, then mount the EmptyDir into - the Pod''s container.' - properties: - directory: - description: directory is the target directory name. Must - not contain or start with '..'. If '.' is supplied, the - volume directory will be the git repository. Otherwise, - if specified, the volume will contain the git repository - in the subdirectory with the given name. - type: string - repository: - description: repository is the URL - type: string - revision: - description: revision is the commit hash for the specified - revision. - type: string - required: - - repository - type: object - glusterfs: - description: 'glusterfs represents a Glusterfs mount on the host - that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/glusterfs/README.md' - properties: - endpoints: - description: 'endpoints is the endpoint name that details - Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - path: - description: 'path is the Glusterfs volume path. More info: - https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: string - readOnly: - description: 'readOnly here will force the Glusterfs volume - to be mounted with read-only permissions. Defaults to false. - More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' - type: boolean - required: - - endpoints - - path - type: object - hostPath: - description: 'hostPath represents a pre-existing file or directory - on the host machine that is directly exposed to the container. - This is generally used for system agents or other privileged - things that are allowed to see the host machine. Most containers - will NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath - --- TODO(jonesdl) We need to restrict who can use host directory - mounts and who can/can not mount host directories as read/write.' - properties: - path: - description: 'path of the directory on the host. If the path - is a symlink, it will follow the link to the real path. - More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - type: - description: 'type for HostPath Volume Defaults to "" More - info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' - type: string - required: - - path - type: object - iscsi: - description: 'iscsi represents an ISCSI Disk resource that is - attached to a kubelet''s host machine and then exposed to the - pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' - properties: - chapAuthDiscovery: - description: chapAuthDiscovery defines whether support iSCSI - Discovery CHAP authentication - type: boolean - chapAuthSession: - description: chapAuthSession defines whether support iSCSI - Session CHAP authentication - type: boolean - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#iscsi - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - initiatorName: - description: initiatorName is the custom iSCSI Initiator Name. - If initiatorName is specified with iscsiInterface simultaneously, - new iSCSI interface : will be - created for the connection. - type: string - iqn: - description: iqn is the target iSCSI Qualified Name. - type: string - iscsiInterface: - description: iscsiInterface is the interface Name that uses - an iSCSI transport. Defaults to 'default' (tcp). - type: string - lun: - description: lun represents iSCSI Target Lun number. - format: int32 - type: integer - portals: - description: portals is the iSCSI Target Portal List. The - portal is either an IP or ip_addr:port if the port is other - than default (typically TCP ports 860 and 3260). - items: - type: string - type: array - readOnly: - description: readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. - type: boolean - secretRef: - description: secretRef is the CHAP Secret for iSCSI target - and initiator authentication - 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 - targetPortal: - description: targetPortal is iSCSI Target Portal. The Portal - is either an IP or ip_addr:port if the port is other than - default (typically TCP ports 860 and 3260). - type: string - required: - - iqn - - lun - - targetPortal - type: object - name: - description: 'name of the volume. Must be a DNS_LABEL and unique - within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' - type: string - nfs: - description: 'nfs represents an NFS mount on the host that shares - a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - properties: - path: - description: 'path that is exported by the NFS server. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - readOnly: - description: 'readOnly here will force the NFS export to be - mounted with read-only permissions. Defaults to false. More - info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: boolean - server: - description: 'server is the hostname or IP address of the - NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' - type: string - required: - - path - - server - type: object - persistentVolumeClaim: - description: 'persistentVolumeClaimVolumeSource represents a reference - to a PersistentVolumeClaim in the same namespace. More info: - https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - properties: - claimName: - description: 'claimName is the name of a PersistentVolumeClaim - in the same namespace as the pod using this volume. More - info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' - type: string - readOnly: - description: readOnly Will force the ReadOnly setting in VolumeMounts. - Default false. - type: boolean - required: - - claimName - type: object - photonPersistentDisk: - description: photonPersistentDisk represents a PhotonController - persistent disk attached and mounted on kubelets host machine - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - pdID: - description: pdID is the ID that identifies Photon Controller - persistent disk - type: string - required: - - pdID - type: object - portworxVolume: - description: portworxVolume represents a portworx volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fSType represents the filesystem type to mount - Must be a filesystem type supported by the host operating - system. Ex. "ext4", "xfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - volumeID: - description: volumeID uniquely identifies a Portworx volume - type: string - required: - - volumeID - type: object - projected: - description: projected items for all in one resources secrets, - configmaps, and downward API - properties: - defaultMode: - description: defaultMode are the mode bits used to set permissions - on created files by default. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON requires decimal - values for mode bits. Directories within the path are not - affected by this setting. This might be in conflict with - other options that affect the file mode, like fsGroup, and - the result can be other mode bits set. - format: int32 - type: integer - sources: - description: sources is the list of volume projections - items: - description: Projection that may be projected along with - other supported volume types - properties: - configMap: - description: configMap information about the configMap - data to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced ConfigMap - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the ConfigMap, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional specify whether the ConfigMap - or its keys must be defined - type: boolean - type: object - downwardAPI: - description: downwardAPI information about the downwardAPI - data to project - properties: - items: - description: Items is a list of DownwardAPIVolume - file - items: - description: DownwardAPIVolumeFile represents - information to create the file containing the - pod field - properties: - fieldRef: - description: 'Required: Selects a field of - the pod: only annotations, labels, name - and namespace are supported.' - properties: - apiVersion: - description: Version of the schema the - FieldPath is written in terms of, defaults - to "v1". - type: string - fieldPath: - description: Path of the field to select - in the specified API version. - type: string - required: - - fieldPath - type: object - mode: - description: 'Optional: mode bits used to - set permissions on this file, must be an - octal value between 0000 and 0777 or a decimal - value between 0 and 511. YAML accepts both - octal and decimal values, JSON requires - decimal values for mode bits. If not specified, - the volume defaultMode will be used. This - might be in conflict with other options - that affect the file mode, like fsGroup, - and the result can be other mode bits set.' - format: int32 - type: integer - path: - description: 'Required: Path is the relative - path name of the file to be created. Must - not be absolute or contain the ''..'' path. - Must be utf-8 encoded. The first item of - the relative path must not start with ''..''' - type: string - resourceFieldRef: - description: 'Selects a resource of the container: - only resources limits and requests (limits.cpu, - limits.memory, requests.cpu and requests.memory) - are currently supported.' - properties: - containerName: - description: 'Container name: required - for volumes, optional for env vars' - type: string - divisor: - anyOf: - - type: integer - - type: string - description: Specifies the output format - of the exposed resources, defaults to - "1" - pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ - x-kubernetes-int-or-string: true - resource: - description: 'Required: resource to select' - type: string - required: - - resource - type: object - required: - - path - type: object - type: array - type: object - secret: - description: secret information about the secret data - to project - properties: - items: - description: items if unspecified, each key-value - pair in the Data field of the referenced Secret - will be projected into the volume as a file whose - name is the key and content is the value. If specified, - the listed keys will be projected into the specified - paths, and unlisted keys will not be present. - If a key is specified which is not present in - the Secret, the volume setup will error unless - it is marked optional. Paths must be relative - and may not contain the '..' path or start with - '..'. - items: - description: Maps a string key to a path within - a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits - used to set permissions on this file. Must - be an octal value between 0000 and 0777 - or a decimal value between 0 and 511. YAML - accepts both octal and decimal values, JSON - requires decimal values for mode bits. If - not specified, the volume defaultMode will - be used. This might be in conflict with - other options that affect the file mode, - like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - path: - description: path is the relative path of - the file to map the key to. May not be an - absolute path. May not contain the path - element '..'. May not start with the string - '..'. - type: string - required: - - key - - path - type: object - type: array - 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 - optional: - description: optional field specify whether the - Secret or its key must be defined - type: boolean - type: object - serviceAccountToken: - description: serviceAccountToken is information about - the serviceAccountToken data to project - properties: - audience: - description: audience is the intended audience of - the token. A recipient of a token must identify - itself with an identifier specified in the audience - of the token, and otherwise should reject the - token. The audience defaults to the identifier - of the apiserver. - type: string - expirationSeconds: - description: expirationSeconds is the requested - duration of validity of the service account token. - As the token approaches expiration, the kubelet - volume plugin will proactively rotate the service - account token. The kubelet will start trying to - rotate the token if the token is older than 80 - percent of its time to live or if the token is - older than 24 hours.Defaults to 1 hour and must - be at least 10 minutes. - format: int64 - type: integer - path: - description: path is the path relative to the mount - point of the file to project the token into. - type: string - required: - - path - type: object - type: object - type: array - type: object - quobyte: - description: quobyte represents a Quobyte mount on the host that - shares a pod's lifetime - properties: - group: - description: group to map volume access to Default is no group - type: string - readOnly: - description: readOnly here will force the Quobyte volume to - be mounted with read-only permissions. Defaults to false. - type: boolean - registry: - description: registry represents a single or multiple Quobyte - Registry services specified as a string as host:port pair - (multiple entries are separated with commas) which acts - as the central registry for volumes - type: string - tenant: - description: tenant owning the given Quobyte volume in the - Backend Used with dynamically provisioned Quobyte volumes, - value is set by the plugin - type: string - user: - description: user to map volume access to Defaults to serivceaccount - user - type: string - volume: - description: volume is a string that references an already - created Quobyte volume by name. - type: string - required: - - registry - - volume - type: object - rbd: - description: 'rbd represents a Rados Block Device mount on the - host that shares a pod''s lifetime. More info: https://examples.k8s.io/volumes/rbd/README.md' - properties: - fsType: - description: 'fsType is the filesystem type of the volume - that you want to mount. Tip: Ensure that the filesystem - type is supported by the host operating system. Examples: - "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#rbd - TODO: how do we prevent errors in the filesystem from compromising - the machine' - type: string - image: - description: 'image is the rados image name. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - keyring: - description: 'keyring is the path to key ring for RBDUser. - Default is /etc/ceph/keyring. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - monitors: - description: 'monitors is a collection of Ceph monitors. More - info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - items: - type: string - type: array - pool: - description: 'pool is the rados pool name. Default is rbd. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - readOnly: - description: 'readOnly here will force the ReadOnly setting - in VolumeMounts. Defaults to false. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: boolean - secretRef: - description: 'secretRef is name of the authentication secret - for RBDUser. If provided overrides keyring. Default is nil. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - 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 - user: - description: 'user is the rados user name. Default is admin. - More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' - type: string - required: - - image - - monitors - type: object - scaleIO: - description: scaleIO represents a ScaleIO persistent volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Default is "xfs". - type: string - gateway: - description: gateway is the host address of the ScaleIO API - Gateway. - type: string - protectionDomain: - description: protectionDomain is the name of the ScaleIO Protection - Domain for the configured storage. - type: string - readOnly: - description: readOnly Defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef references to the secret for ScaleIO - user and other sensitive information. If this is not provided, - Login operation will fail. - 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 - sslEnabled: - description: sslEnabled Flag enable/disable SSL communication - with Gateway, default false - type: boolean - storageMode: - description: storageMode indicates whether the storage for - a volume should be ThickProvisioned or ThinProvisioned. - Default is ThinProvisioned. - type: string - storagePool: - description: storagePool is the ScaleIO Storage Pool associated - with the protection domain. - type: string - system: - description: system is the name of the storage system as configured - in ScaleIO. - type: string - volumeName: - description: volumeName is the name of a volume already created - in the ScaleIO system that is associated with this volume - source. - type: string - required: - - gateway - - secretRef - - system - type: object - secret: - description: 'secret represents a secret that should populate - this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - properties: - defaultMode: - description: 'defaultMode is Optional: mode bits used to set - permissions on created files by default. Must be an octal - value between 0000 and 0777 or a decimal value between 0 - and 511. YAML accepts both octal and decimal values, JSON - requires decimal values for mode bits. Defaults to 0644. - Directories within the path are not affected by this setting. - This might be in conflict with other options that affect - the file mode, like fsGroup, and the result can be other - mode bits set.' - format: int32 - type: integer - items: - description: items If unspecified, each key-value pair in - the Data field of the referenced Secret will be projected - into the volume as a file whose name is the key and content - is the value. If specified, the listed keys will be projected - into the specified paths, and unlisted keys will not be - present. If a key is specified which is not present in the - Secret, the volume setup will error unless it is marked - optional. Paths must be relative and may not contain the - '..' path or start with '..'. - items: - description: Maps a string key to a path within a volume. - properties: - key: - description: key is the key to project. - type: string - mode: - description: 'mode is Optional: mode bits used to set - permissions on this file. Must be an octal value between - 0000 and 0777 or a decimal value between 0 and 511. - YAML accepts both octal and decimal values, JSON requires - decimal values for mode bits. If not specified, the - volume defaultMode will be used. This might be in - conflict with other options that affect the file mode, - like fsGroup, and the result can be other mode bits - set.' - format: int32 - type: integer - path: - description: path is the relative path of the file to - map the key to. May not be an absolute path. May not - contain the path element '..'. May not start with - the string '..'. - type: string - required: - - key - - path - type: object - type: array - optional: - description: optional field specify whether the Secret or - its keys must be defined - type: boolean - secretName: - description: 'secretName is the name of the secret in the - pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' - type: string - type: object - storageos: - description: storageOS represents a StorageOS volume attached - and mounted on Kubernetes nodes. - properties: - fsType: - description: fsType is the filesystem type to mount. Must - be a filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - readOnly: - description: readOnly defaults to false (read/write). ReadOnly - here will force the ReadOnly setting in VolumeMounts. - type: boolean - secretRef: - description: secretRef specifies the secret to use for obtaining - the StorageOS API credentials. If not specified, default - values will be attempted. - 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 - volumeName: - description: volumeName is the human-readable name of the - StorageOS volume. Volume names are only unique within a - namespace. - type: string - volumeNamespace: - description: volumeNamespace specifies the scope of the volume - within StorageOS. If no namespace is specified then the - Pod's namespace will be used. This allows the Kubernetes - name scoping to be mirrored within StorageOS for tighter - integration. Set VolumeName to any name to override the - default behaviour. Set to "default" if you are not using - namespaces within StorageOS. Namespaces that do not pre-exist - within StorageOS will be created. - type: string - type: object - vsphereVolume: - description: vsphereVolume represents a vSphere volume attached - and mounted on kubelets host machine - properties: - fsType: - description: fsType is filesystem type to mount. Must be a - filesystem type supported by the host operating system. - Ex. "ext4", "xfs", "ntfs". Implicitly inferred to be "ext4" - if unspecified. - type: string - storagePolicyID: - description: storagePolicyID is the storage Policy Based Management - (SPBM) profile ID associated with the StoragePolicyName. - type: string - storagePolicyName: - description: storagePolicyName is the storage Policy Based - Management (SPBM) profile name. - type: string - volumePath: - description: volumePath is the path that identifies vSphere - volume vmdk - type: string - required: - - volumePath - type: object - required: - - name - type: object startTimestamp: description: Date/time when the backup started being processed. format: date-time type: string totalSize: - description: backup total size string with capacity units in the form - of "1Gi", "1Mi", "1Ki". + description: Backup total size. A string with capacity units in the + form of "1Gi", "1Mi", "1Ki". type: string type: object type: object diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_backuptools.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_backuptools.yaml index 3f84de80f..3fcd126fe 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_backuptools.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_backuptools.yaml @@ -286,6 +286,13 @@ spec: type: object type: object x-kubernetes-preserve-unknown-fields: true + type: + default: file + description: the type of backup tool, file or pitr + enum: + - file + - pitr + type: string required: - backupCommands - image diff --git a/deploy/helm/crds/dataprotection.kubeblocks.io_restorejobs.yaml b/deploy/helm/crds/dataprotection.kubeblocks.io_restorejobs.yaml index 84334be2b..b282429e6 100644 --- a/deploy/helm/crds/dataprotection.kubeblocks.io_restorejobs.yaml +++ b/deploy/helm/crds/dataprotection.kubeblocks.io_restorejobs.yaml @@ -59,7 +59,7 @@ spec: description: the target database workload to restore properties: labelsSelector: - description: LabelSelector is used to find matching pods. Pods + description: labelsSelector is used to find matching pods. Pods that match this label selector are counted to determine the number of pods in their corresponding topology domain. properties: @@ -106,7 +106,7 @@ spec: type: object x-kubernetes-preserve-unknown-fields: true secret: - description: Secret is used to connect to the target database + description: secret is used to connect to the target database cluster. If not set, secret will be inherited from backup policy template. if still not set, the controller will check if any system account for dataprotection has been created. @@ -115,14 +115,16 @@ spec: description: the secret name pattern: ^[a-z0-9]([a-z0-9\.\-]*[a-z0-9])?$ type: string - passwordKeyword: - description: PasswordKeyword the map keyword of the password - in the connection credential secret - type: string - userKeyword: - description: UserKeyword the map keyword of the user in the + passwordKey: + default: password + description: passwordKey the map key of the password in the connection credential secret type: string + usernameKey: + default: username + description: usernameKey the map key of the user in the connection + credential secret + type: string required: - name type: object diff --git a/deploy/helm/crds/extensions.kubeblocks.io_addons.yaml b/deploy/helm/crds/extensions.kubeblocks.io_addons.yaml index 626c098b3..0ce3ddaf0 100644 --- a/deploy/helm/crds/extensions.kubeblocks.io_addons.yaml +++ b/deploy/helm/crds/extensions.kubeblocks.io_addons.yaml @@ -32,7 +32,7 @@ spec: name: v1alpha1 schema: openAPIV3Schema: - description: Addon is the Schema for the addons API + description: Addon is the Schema for the add-ons API. properties: apiVersion: description: 'APIVersion defines the versioned schema of this representation @@ -47,8 +47,26 @@ spec: metadata: type: object spec: - description: AddonSpec defines the desired state of Addon + description: AddonSpec defines the desired state of an add-on. properties: + cliPlugins: + description: Plugin installation spec. + items: + properties: + description: + description: The description of the plugin. + type: string + indexRepository: + description: The index repository of the plugin. + type: string + name: + description: Name of the plugin. + type: string + required: + - indexRepository + - name + type: object + type: array defaultInstallValues: description: Default installation parameters. items: @@ -58,7 +76,7 @@ spec: attributes to be set. type: boolean extras: - description: Install spec. for extra items. + description: Installs spec. for extra items. items: properties: name: @@ -82,7 +100,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount - of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -94,8 +112,8 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is - explicitly specified, otherwise to an implementation-defined - value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + explicitly specified; otherwise, it defaults to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -129,7 +147,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -140,19 +158,19 @@ spec: x-kubernetes-int-or-string: true description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, - otherwise to an implementation-defined value. More info: - https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + it defaults to Limits if that is explicitly specified; + otherwise, it defaults to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object selectors: - description: Addon default install parameters selectors. If - multiple selectors are provided that all selectors must evaluate + description: Addon installs parameters selectors by default. + If multiple selectors are provided, all selectors must evaluate to true. items: properties: key: - description: The selector key, valid values are KubeVersion, + description: The selector key. Valid values are KubeVersion, KubeGitVersion. "KubeVersion" the semver expression of Kubernetes versions, i.e., v1.24. "KubeGitVersion" may contain distro. info., i.e., v1.24.4+eks. @@ -164,10 +182,10 @@ spec: description: "Represents a key's relationship to a set of values. Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex. \n Possible enum - values: `\"Contains\"` line contains string `\"DoesNotContain\"` - line does not contain string `\"MatchRegex\"` line contains - a match to the regular expression `\"DoesNotMatchRegex\"` - line does not contain a match to the regular expression" + values: `\"Contains\"` line contains a string. `\"DoesNotContain\"` + line does not contain a string. `\"MatchRegex\"` line + contains a match to the regular expression. `\"DoesNotMatchRegex\"` + line does not contain a match to the regular expression." enum: - Contains - DoesNotContain @@ -175,8 +193,8 @@ spec: - DoesNotMatchRegex type: string values: - description: An array of string values. Server as "OR" - expression to the operator. + description: An array of string values. It serves as an + "OR" expression to the operator. items: type: string type: array @@ -198,8 +216,7 @@ spec: description: Addon description. type: string helm: - description: Helm installation spec., it's only being processed if - type=helm. + description: Helm installation spec. It's processed only when type=helm. properties: chartLocationURL: description: A Helm Chart location URL. @@ -207,17 +224,18 @@ spec: installOptions: additionalProperties: type: string - description: installOptions defines Helm release install options. + description: installOptions defines Helm release installation + options. type: object installValues: - description: HelmInstallValues defines Helm release install set - values. + description: HelmInstallValues defines Helm release installation + set values. properties: configMapRefs: - description: Selects a key of a ConfigMap item list, the value - of ConfigMap can be a JSON or YAML string content, use key - name with ".json" or ".yaml" or ".yml" extension name to - specify content type. + description: Selects a key of a ConfigMap item list. The value + of ConfigMap can be a JSON or YAML string content. Use a + key name with ".json" or ".yaml" or ".yml" extension name + to specify a content type. items: properties: key: @@ -232,10 +250,10 @@ spec: type: object type: array secretRefs: - description: Selects a key of a Secrets item list, the value - of Secrets can be a JSON or YAML string content, use key + description: Selects a key of a Secrets item list. The value + of Secrets can be a JSON or YAML string content. Use a key name with ".json" or ".yaml" or ".yml" extension name to - specify content type. + specify a content type. items: properties: key: @@ -250,13 +268,13 @@ spec: type: object type: array setJSONValues: - description: Helm install set JSON values, can specify multiple - or separate values with commas(key1=jsonval1,key2=jsonval2). + description: Helm install set JSON values. It can specify + multiple or separate values with commas(key1=jsonval1,key2=jsonval2). items: type: string type: array setValues: - description: Helm install set values, can specify multiple + description: Helm install set values. It can specify multiple or separate values with commas(key1=val1,key2=val2). items: type: string @@ -267,7 +285,7 @@ spec: type: array type: object valuesMapping: - description: valuesMapping defines addon normalized resources + description: valuesMapping defines add-on normalized resources parameters mapped to Helm values' keys. properties: extras: @@ -275,12 +293,12 @@ spec: items: properties: jsonMap: - description: 'jsonMap define the "key" mapping values, - valid keys are tolerations. Enum values explained: - `"tolerations"` sets toleration mapping key' + description: 'jsonMap defines the "key" mapping values. + The valid key is tolerations. Enum values explained: + `"tolerations"` sets the toleration mapping key.' properties: tolerations: - description: tolerations sets toleration mapping + description: tolerations sets the toleration mapping key. type: string type: object @@ -321,29 +339,29 @@ spec: via PVC resize. type: object storage: - description: storage sets storage size value mapping - key. + description: storage sets the storage size value + mapping key. type: string type: object valueMap: - description: 'valueMap define the "key" mapping values, - valid keys are replicaCount, persistentVolumeEnabled, + description: 'valueMap define the "key" mapping values. + Valid keys are replicaCount, persistentVolumeEnabled, and storageClass. Enum values explained: `"replicaCount"` - sets replicaCount value mapping key `"persistentVolumeEnabled"` - sets persistent volume enabled mapping key `"storageClass"` - sets storageClass mapping key' + sets the replicaCount value mapping key. `"persistentVolumeEnabled"` + sets the persistent volume enabled mapping key. `"storageClass"` + sets the storageClass mapping key.' properties: persistentVolumeEnabled: - description: persistentVolumeEnabled persistent + description: persistentVolumeEnabled sets the persistent volume enabled mapping key. type: string replicaCount: - description: replicaCount sets replicaCount value - mapping key. + description: replicaCount sets the replicaCount + value mapping key. type: string storageClass: - description: storageClass sets storageClass mapping - key. + description: storageClass sets the storageClass + mapping key. type: string type: object required: @@ -354,12 +372,12 @@ spec: - name x-kubernetes-list-type: map jsonMap: - description: 'jsonMap define the "key" mapping values, valid - keys are tolerations. Enum values explained: `"tolerations"` - sets toleration mapping key' + description: 'jsonMap defines the "key" mapping values. The + valid key is tolerations. Enum values explained: `"tolerations"` + sets the toleration mapping key.' properties: tolerations: - description: tolerations sets toleration mapping key. + description: tolerations sets the toleration mapping key. type: string type: object resources: @@ -395,27 +413,29 @@ spec: resize. type: object storage: - description: storage sets storage size value mapping key. + description: storage sets the storage size value mapping + key. type: string type: object valueMap: - description: 'valueMap define the "key" mapping values, valid + description: 'valueMap define the "key" mapping values. Valid keys are replicaCount, persistentVolumeEnabled, and storageClass. - Enum values explained: `"replicaCount"` sets replicaCount - value mapping key `"persistentVolumeEnabled"` sets persistent - volume enabled mapping key `"storageClass"` sets storageClass - mapping key' + Enum values explained: `"replicaCount"` sets the replicaCount + value mapping key. `"persistentVolumeEnabled"` sets the + persistent volume enabled mapping key. `"storageClass"` + sets the storageClass mapping key.' properties: persistentVolumeEnabled: - description: persistentVolumeEnabled persistent volume - enabled mapping key. + description: persistentVolumeEnabled sets the persistent + volume enabled mapping key. type: string replicaCount: - description: replicaCount sets replicaCount value mapping - key. + description: replicaCount sets the replicaCount value + mapping key. type: string storageClass: - description: storageClass sets storageClass mapping key. + description: storageClass sets the storageClass mapping + key. type: string type: object type: object @@ -430,7 +450,7 @@ spec: attributes to be set. type: boolean extras: - description: Install spec. for extra items. + description: Installs spec. for extra items. items: properties: name: @@ -454,7 +474,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of - compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -466,8 +486,8 @@ spec: description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, it defaults to Limits if that is - explicitly specified, otherwise to an implementation-defined - value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + explicitly specified; otherwise, it defaults to an + implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -501,7 +521,7 @@ spec: pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ x-kubernetes-int-or-string: true description: 'Limits describes the maximum amount of compute - resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object requests: additionalProperties: @@ -512,8 +532,9 @@ spec: x-kubernetes-int-or-string: true description: 'Requests describes the minimum amount of compute resources required. If Requests is omitted for a container, - it defaults to Limits if that is explicitly specified, otherwise - to an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + it defaults to Limits if that is explicitly specified; otherwise, + it defaults to an implementation-defined value. More info: + https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.' type: object type: object storageClass: @@ -524,20 +545,21 @@ spec: type: string type: object installable: - description: Addon installable spec., provide selector and auto-install + description: Addon installable spec. It provides selector and auto-install settings. properties: autoInstall: default: false - description: autoInstall defines an addon should auto installed + description: autoInstall defines an add-on should be installed + automatically. type: boolean selectors: - description: Addon installable selectors. If multiple selectors - are provided that all selectors must evaluate to true. + description: Add-on installable selectors. If multiple selectors + are provided, all selectors must evaluate to true. items: properties: key: - description: The selector key, valid values are KubeVersion, + description: The selector key. Valid values are KubeVersion, KubeGitVersion. "KubeVersion" the semver expression of Kubernetes versions, i.e., v1.24. "KubeGitVersion" may contain distro. info., i.e., v1.24.4+eks. @@ -549,10 +571,10 @@ spec: description: "Represents a key's relationship to a set of values. Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex. \n Possible enum values: - `\"Contains\"` line contains string `\"DoesNotContain\"` - line does not contain string `\"MatchRegex\"` line contains - a match to the regular expression `\"DoesNotMatchRegex\"` - line does not contain a match to the regular expression" + `\"Contains\"` line contains a string. `\"DoesNotContain\"` + line does not contain a string. `\"MatchRegex\"` line + contains a match to the regular expression. `\"DoesNotMatchRegex\"` + line does not contain a match to the regular expression." enum: - Contains - DoesNotContain @@ -560,8 +582,8 @@ spec: - DoesNotMatchRegex type: string values: - description: An array of string values. Server as "OR" expression - to the operator. + description: An array of string values. It serves as an + "OR" expression to the operator. items: type: string type: array @@ -574,7 +596,7 @@ spec: - autoInstall type: object type: - description: Addon type, valid value is helm. + description: Add-on type. The valid value is helm. enum: - Helm type: string @@ -587,10 +609,11 @@ spec: otherwise rule: 'has(self.type) && self.type == ''Helm'' ? has(self.helm) : !has(self.helm)' status: - description: AddonStatus defines the observed state of Addon + description: AddonStatus defines the observed state of an add-on. properties: conditions: - description: Describe current state of Addon API installation conditions. + description: Describes the current state of add-on API installation + conditions. items: description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct @@ -660,12 +683,12 @@ spec: type: array observedGeneration: description: observedGeneration is the most recent generation observed - for this Addon. It corresponds to the Addon's generation, which + for this add-on. It corresponds to the add-on's generation, which is updated on mutation by the API Server. format: int64 type: integer phase: - description: Addon installation phases. Valid values are Disabled, + description: Add-on installation phases. Valid values are Disabled, Enabled, Failed, Enabling, Disabling. enum: - Disabled diff --git a/deploy/helm/crds/workloads.kubeblocks.io_consensussets.yaml b/deploy/helm/crds/workloads.kubeblocks.io_consensussets.yaml new file mode 100644 index 000000000..4bcdc8f36 --- /dev/null +++ b/deploy/helm/crds/workloads.kubeblocks.io_consensussets.yaml @@ -0,0 +1,8649 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.9.0 + creationTimestamp: null + name: consensussets.workloads.kubeblocks.io +spec: + group: workloads.kubeblocks.io + names: + categories: + - kubeblocks + - all + kind: ConsensusSet + listKind: ConsensusSetList + plural: consensussets + shortNames: + - csset + singular: consensusset + scope: Namespaced + versions: + - additionalPrinterColumns: + - description: leader pod name. + jsonPath: .status.membersStatus[?(@.role.isLeader==true)].podName + name: LEADER + type: string + - description: ready replicas. + jsonPath: .status.readyReplicas + name: READY + type: string + - description: total replicas. + jsonPath: .status.replicas + name: REPLICAS + type: string + - jsonPath: .metadata.creationTimestamp + name: AGE + type: date + name: v1alpha1 + schema: + openAPIV3Schema: + description: ConsensusSet is the Schema for the consensussets 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: ConsensusSetSpec defines the desired state of ConsensusSet + properties: + credential: + description: Credential used to connect to DB engine + properties: + password: + description: Password variable name will be KB_CONSENSUS_SET_PASSWORD + properties: + value: + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the + container and any service environment variables. If a variable + cannot be resolved, the reference in the input string will + be unchanged. Double $$ are reduced to a single $, which + allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + type: object + type: object + username: + description: Username variable name will be KB_CONSENSUS_SET_USERNAME + properties: + value: + description: 'Variable references $(VAR_NAME) are expanded + using the previously defined environment variables in the + container and any service environment variables. If a variable + cannot be resolved, the reference in the input string will + be unchanged. Double $$ are reduced to a single $, which + allows for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the variable + exists or not. Defaults to "".' + type: string + valueFrom: + description: Source for the environment variable's value. + Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap or its + key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: supports metadata.name, + metadata.namespace, `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, status.hostIP, + status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the FieldPath is + written in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field to select in the specified + API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: only + resources limits and requests (limits.cpu, limits.memory, + limits.ephemeral-storage, requests.cpu, requests.memory + and requests.ephemeral-storage) are currently supported.' + properties: + containerName: + description: 'Container name: required for volumes, + optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format of the exposed + resources, defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in the pod's namespace + properties: + key: + description: The key of the secret to select from. Must + be a valid secret key. + type: string + 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 + optional: + description: Specify whether the Secret or its key + must be defined + type: boolean + required: + - key + type: object + type: object + type: object + required: + - password + - username + type: object + membershipReconfiguration: + description: MembershipReconfiguration provides actions to do membership + dynamic reconfiguration. + properties: + logSyncAction: + description: LogSyncAction specifies how to trigger the new member + to start log syncing previous none-nil action's Image wil be + used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + memberJoinAction: + description: MemberJoinAction specifies how to add member previous + none-nil action's Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + memberLeaveAction: + description: MemberLeaveAction specifies how to remove member + previous none-nil action's Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + promoteAction: + description: PromoteAction specifies how to tell the cluster that + the new member can join voting now previous none-nil action's + Image wil be used if not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + switchoverAction: + description: SwitchoverAction specifies how to do switchover latest + [BusyBox](https://busybox.net/) image will be used if Image + not configured + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be used + to retrieve of process role info + type: string + required: + - command + type: object + type: object + replicas: + default: 1 + description: Replicas defines number of Pods + format: int32 + minimum: 0 + type: integer + roleObservation: + description: RoleObservation provides method to observe role. + properties: + failureThreshold: + default: 3 + description: Minimum consecutive failures for the observation + to be considered failed after having succeeded. Defaults to + 3. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + initialDelaySeconds: + default: 0 + description: Number of seconds after the container has started + before role observation has started. + format: int32 + minimum: 0 + type: integer + observationActions: + description: 'ObservationActions define Actions to be taken in + serial. after all actions done, the final output should be a + single string of the role name defined in spec.Roles latest + [BusyBox](https://busybox.net/) image will be used if Image + not configured Environment variables can be used in Command: + - v_KB_CONSENSUS_SET_LAST_STDOUT stdout from last action, watch + ''v_'' prefixed - KB_CONSENSUS_SET_USERNAME username part of + credential - KB_CONSENSUS_SET_PASSWORD password part of credential' + items: + properties: + command: + description: Command will be executed in Container to retrieve + or process role info + items: + type: string + type: array + image: + description: utility image contains command that can be + used to retrieve of process role info + type: string + required: + - command + type: object + type: array + periodSeconds: + default: 2 + description: How often (in seconds) to perform the observation. + Default to 2 seconds. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + successThreshold: + default: 1 + description: Minimum consecutive successes for the observation + to be considered successful after having failed. Defaults to + 1. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + timeoutSeconds: + default: 1 + description: Number of seconds after which the observation times + out. Defaults to 1 second. Minimum value is 1. + format: int32 + minimum: 1 + type: integer + required: + - observationActions + type: object + roles: + description: Roles, a list of roles defined in this consensus system. + items: + properties: + accessMode: + default: ReadWrite + description: AccessMode, what service this member capable. + enum: + - None + - Readonly + - ReadWrite + type: string + canVote: + default: true + description: CanVote, whether this member has voting rights + type: boolean + isLeader: + default: false + description: IsLeader, whether this member is the leader + type: boolean + name: + default: leader + description: Name, role name. + type: string + required: + - accessMode + - name + type: object + type: array + service: + description: service defines the behavior of a service spec. provides + read-write service https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status + properties: + allocateLoadBalancerNodePorts: + description: allocateLoadBalancerNodePorts defines if NodePorts + will be automatically allocated for services with type LoadBalancer. Default + is "true". It may be set to "false" if the cluster load-balancer + does not rely on NodePorts. If the caller requests specific + NodePorts (by specifying a value), those requests will be respected, + regardless of this field. This field may only be set for services + with type LoadBalancer and will be cleared if the type is changed + to any other type. + type: boolean + clusterIP: + description: 'clusterIP is the IP address of the service and is + usually assigned randomly. If an address is specified manually, + is in-range (as per system configuration), and is not in use, + it will be allocated to the service; otherwise creation of the + service will fail. This field may not be changed through updates + unless the type field is also being changed to ExternalName + (which requires this field to be blank) or the type field is + being changed from ExternalName (in which case this field may + optionally be specified, as describe above). Valid values are + "None", empty string (""), or a valid IP address. Setting this + to "None" makes a "headless service" (no virtual IP), which + is useful when direct endpoint connections are preferred and + proxying is not required. Only applies to types ClusterIP, + NodePort, and LoadBalancer. If this field is specified when + creating a Service of type ExternalName, creation will fail. + This field will be wiped when updating a Service to type ExternalName. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + clusterIPs: + description: "ClusterIPs is a list of IP addresses assigned to + this service, and are usually assigned randomly. If an address + is specified manually, is in-range (as per system configuration), + and is not in use, it will be allocated to the service; otherwise + creation of the service will fail. This field may not be changed + through updates unless the type field is also being changed + to ExternalName (which requires this field to be empty) or the + type field is being changed from ExternalName (in which case + this field may optionally be specified, as describe above). + \ Valid values are \"None\", empty string (\"\"), or a valid + IP address. Setting this to \"None\" makes a \"headless service\" + (no virtual IP), which is useful when direct endpoint connections + are preferred and proxying is not required. Only applies to + types ClusterIP, NodePort, and LoadBalancer. If this field is + specified when creating a Service of type ExternalName, creation + will fail. This field will be wiped when updating a Service + to type ExternalName. If this field is not specified, it will + be initialized from the clusterIP field. If this field is specified, + clients must ensure that clusterIPs[0] and clusterIP have the + same value. \n This field may hold a maximum of two entries + (dual-stack IPs, in either order). These IPs must correspond + to the values of the ipFamilies field. Both clusterIPs and ipFamilies + are governed by the ipFamilyPolicy field. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies" + items: + type: string + type: array + x-kubernetes-list-type: atomic + externalIPs: + description: externalIPs is a list of IP addresses for which nodes + in the cluster will also accept traffic for this service. These + IPs are not managed by Kubernetes. The user is responsible + for ensuring that traffic arrives at a node with this IP. A + common example is external load-balancers that are not part + of the Kubernetes system. + items: + type: string + type: array + externalName: + description: externalName is the external reference that discovery + mechanisms will return as an alias for this service (e.g. a + DNS CNAME record). No proxying will be involved. Must be a + lowercase RFC-1123 hostname (https://tools.ietf.org/html/rfc1123) + and requires `type` to be "ExternalName". + type: string + externalTrafficPolicy: + description: externalTrafficPolicy describes how nodes distribute + service traffic they receive on one of the Service's "externally-facing" + addresses (NodePorts, ExternalIPs, and LoadBalancer IPs). If + set to "Local", the proxy will configure the service in a way + that assumes that external load balancers will take care of + balancing the service traffic between nodes, and so each node + will deliver traffic only to the node-local endpoints of the + service, without masquerading the client source IP. (Traffic + mistakenly sent to a node with no endpoints will be dropped.) + The default value, "Cluster", uses the standard behavior of + routing to all endpoints evenly (possibly modified by topology + and other features). Note that traffic sent to an External IP + or LoadBalancer IP from within the cluster will always get "Cluster" + semantics, but clients sending to a NodePort from within the + cluster may need to take traffic policy into account when picking + a node. + type: string + healthCheckNodePort: + description: healthCheckNodePort specifies the healthcheck nodePort + for the service. This only applies when type is set to LoadBalancer + and externalTrafficPolicy is set to Local. If a value is specified, + is in-range, and is not in use, it will be used. If not specified, + a value will be automatically allocated. External systems (e.g. + load-balancers) can use this port to determine if a given node + holds endpoints for this service or not. If this field is specified + when creating a Service which does not need it, creation will + fail. This field will be wiped when updating a Service to no + longer need it (e.g. changing type). This field cannot be updated + once set. + format: int32 + type: integer + internalTrafficPolicy: + description: InternalTrafficPolicy describes how nodes distribute + service traffic they receive on the ClusterIP. If set to "Local", + the proxy will assume that pods only want to talk to endpoints + of the service on the same node as the pod, dropping the traffic + if there are no local endpoints. The default value, "Cluster", + uses the standard behavior of routing to all endpoints evenly + (possibly modified by topology and other features). + type: string + ipFamilies: + description: "IPFamilies is a list of IP families (e.g. IPv4, + IPv6) assigned to this service. This field is usually assigned + automatically based on cluster configuration and the ipFamilyPolicy + field. If this field is specified manually, the requested family + is available in the cluster, and ipFamilyPolicy allows it, it + will be used; otherwise creation of the service will fail. This + field is conditionally mutable: it allows for adding or removing + a secondary IP family, but it does not allow changing the primary + IP family of the Service. Valid values are \"IPv4\" and \"IPv6\". + \ This field only applies to Services of types ClusterIP, NodePort, + and LoadBalancer, and does apply to \"headless\" services. This + field will be wiped when updating a Service to type ExternalName. + \n This field may hold a maximum of two entries (dual-stack + families, in either order). These families must correspond + to the values of the clusterIPs field, if specified. Both clusterIPs + and ipFamilies are governed by the ipFamilyPolicy field." + items: + description: IPFamily represents the IP Family (IPv4 or IPv6). + This type is used to express the family of an IP expressed + by a type (e.g. service.spec.ipFamilies). + type: string + type: array + x-kubernetes-list-type: atomic + ipFamilyPolicy: + description: IPFamilyPolicy represents the dual-stack-ness requested + or required by this Service. If there is no value provided, + then this field will be set to SingleStack. Services can be + "SingleStack" (a single IP family), "PreferDualStack" (two IP + families on dual-stack configured clusters or a single IP family + on single-stack clusters), or "RequireDualStack" (two IP families + on dual-stack configured clusters, otherwise fail). The ipFamilies + and clusterIPs fields depend on the value of this field. This + field will be wiped when updating a service to type ExternalName. + type: string + loadBalancerClass: + description: loadBalancerClass is the class of the load balancer + implementation this Service belongs to. If specified, the value + of this field must be a label-style identifier, with an optional + prefix, e.g. "internal-vip" or "example.com/internal-vip". Unprefixed + names are reserved for end-users. This field can only be set + when the Service type is 'LoadBalancer'. If not set, the default + load balancer implementation is used, today this is typically + done through the cloud provider integration, but should apply + for any default implementation. If set, it is assumed that a + load balancer implementation is watching for Services with a + matching class. Any default load balancer implementation (e.g. + cloud providers) should ignore Services that set this field. + This field can only be set when creating or updating a Service + to type 'LoadBalancer'. Once set, it can not be changed. This + field will be wiped when a service is updated to a non 'LoadBalancer' + type. + type: string + loadBalancerIP: + description: 'Only applies to Service Type: LoadBalancer. This + feature depends on whether the underlying cloud-provider supports + specifying the loadBalancerIP when a load balancer is created. + This field will be ignored if the cloud-provider does not support + the feature. Deprecated: This field was under-specified and + its meaning varies across implementations, and it cannot support + dual-stack. As of Kubernetes v1.24, users are encouraged to + use implementation-specific annotations when available. This + field may be removed in a future API version.' + type: string + loadBalancerSourceRanges: + description: 'If specified and supported by the platform, this + will restrict traffic through the cloud-provider load-balancer + will be restricted to the specified client IPs. This field will + be ignored if the cloud-provider does not support the feature." + More info: https://kubernetes.io/docs/tasks/access-application-cluster/create-external-load-balancer/' + items: + type: string + type: array + ports: + description: 'The list of ports that are exposed by this service. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + items: + description: ServicePort contains information on service's port. + properties: + appProtocol: + description: The application protocol for this port. This + field follows standard Kubernetes label syntax. Un-prefixed + names are reserved for IANA standard service names (as + per RFC-6335 and https://www.iana.org/assignments/service-names). + Non-standard protocols should use prefixed names such + as mycompany.com/my-custom-protocol. + type: string + name: + description: The name of this port within the service. This + must be a DNS_LABEL. All ports within a ServiceSpec must + have unique names. When considering the endpoints for + a Service, this must match the 'name' field in the EndpointPort. + Optional if only one ServicePort is defined on this service. + type: string + nodePort: + description: 'The port on each node on which this service + is exposed when type is NodePort or LoadBalancer. Usually + assigned by the system. If a value is specified, in-range, + and not in use it will be used, otherwise the operation + will fail. If not specified, a port will be allocated + if this Service requires one. If this field is specified + when creating a Service which does not need it, creation + will fail. This field will be wiped when updating a Service + to no longer need it (e.g. changing type from NodePort + to ClusterIP). More info: https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport' + format: int32 + type: integer + port: + description: The port that will be exposed by this service. + format: int32 + type: integer + protocol: + default: TCP + description: The IP protocol for this port. Supports "TCP", + "UDP", and "SCTP". Default is TCP. + type: string + targetPort: + anyOf: + - type: integer + - type: string + description: 'Number or name of the port to access on the + pods targeted by the service. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. If this is + a string, it will be looked up as a named port in the + target Pod''s container ports. If this is not specified, + the value of the ''port'' field is used (an identity map). + This field is ignored for services with clusterIP=None, + and should be omitted or set equal to the ''port'' field. + More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service' + x-kubernetes-int-or-string: true + required: + - port + type: object + type: array + x-kubernetes-list-map-keys: + - port + - protocol + x-kubernetes-list-type: map + publishNotReadyAddresses: + description: publishNotReadyAddresses indicates that any agent + which deals with endpoints for this Service should disregard + any indications of ready/not-ready. The primary use case for + setting this field is for a StatefulSet's Headless Service to + propagate SRV DNS records for its Pods for the purpose of peer + discovery. The Kubernetes controllers that generate Endpoints + and EndpointSlice resources for Services interpret this to mean + that all endpoints are considered "ready" even if the Pods themselves + are not. Agents which consume only Kubernetes generated endpoints + through the Endpoints or EndpointSlice resources can safely + assume this behavior. + type: boolean + selector: + additionalProperties: + type: string + description: 'Route service traffic to pods with label keys and + values matching this selector. If empty or not present, the + service is assumed to have an external process managing its + endpoints, which Kubernetes will not modify. Only applies to + types ClusterIP, NodePort, and LoadBalancer. Ignored if type + is ExternalName. More info: https://kubernetes.io/docs/concepts/services-networking/service/' + type: object + x-kubernetes-map-type: atomic + sessionAffinity: + description: 'Supports "ClientIP" and "None". Used to maintain + session affinity. Enable client IP based session affinity. Must + be ClientIP or None. Defaults to None. More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies' + type: string + sessionAffinityConfig: + description: sessionAffinityConfig contains the configurations + of session affinity. + properties: + clientIP: + description: clientIP contains the configurations of Client + IP based session affinity. + properties: + timeoutSeconds: + description: timeoutSeconds specifies the seconds of ClientIP + type session sticky time. The value must be >0 && <=86400(for + 1 day) if ServiceAffinity == "ClientIP". Default value + is 10800(for 3 hours). + format: int32 + type: integer + type: object + type: object + type: + description: 'type determines how the Service is exposed. Defaults + to ClusterIP. Valid options are ExternalName, ClusterIP, NodePort, + and LoadBalancer. "ClusterIP" allocates a cluster-internal IP + address for load-balancing to endpoints. Endpoints are determined + by the selector or if that is not specified, by manual construction + of an Endpoints object or EndpointSlice objects. If clusterIP + is "None", no virtual IP is allocated and the endpoints are + published as a set of endpoints rather than a virtual IP. "NodePort" + builds on ClusterIP and allocates a port on every node which + routes to the same endpoints as the clusterIP. "LoadBalancer" + builds on NodePort and creates an external load-balancer (if + supported in the current cloud) which routes to the same endpoints + as the clusterIP. "ExternalName" aliases this service to the + specified externalName. Several other fields do not apply to + ExternalName services. More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types' + type: string + type: object + x-kubernetes-preserve-unknown-fields: true + template: + description: PodTemplateSpec describes the data a pod should have + when created from a template + properties: + metadata: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: 'Specification of the desired behavior of the pod. + More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#spec-and-status' + properties: + activeDeadlineSeconds: + description: Optional duration in seconds the pod may be active + on the node relative to StartTime before the system will + actively try to mark it failed and kill associated containers. + Value must be a positive integer. + format: int64 + type: integer + affinity: + description: If specified, the pod's scheduling constraints + properties: + nodeAffinity: + description: Describes node affinity scheduling rules + for the pod. + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, + etc.), compute a sum by iterating through the elements + of this field and adding "weight" to the sum if + the node matches the corresponding matchExpressions; + the node(s) with the highest sum are the most preferred. + items: + description: An empty preferred scheduling term + matches all objects with implicit weight 0 (i.e. + it's a no-op). A null preferred scheduling term + matches no objects (i.e. is also a no-op). + properties: + preference: + description: A node selector term, associated + with the corresponding weight. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + weight: + description: Weight associated with matching + the corresponding nodeSelectorTerm, in the + range 1-100. + format: int32 + type: integer + required: + - preference + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + affinity requirements specified by this field cease + to be met at some point during pod execution (e.g. + due to an update), the system may or may not try + to eventually evict the pod from its node. + properties: + nodeSelectorTerms: + description: Required. A list of node selector + terms. The terms are ORed. + items: + description: A null or empty node selector term + matches no objects. The requirements of them + are ANDed. The TopologySelectorTerm type implements + a subset of the NodeSelectorTerm. + properties: + matchExpressions: + description: A list of node selector requirements + by node's labels. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchFields: + description: A list of node selector requirements + by node's fields. + items: + description: A node selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: The label key that the + selector applies to. + type: string + operator: + description: Represents a key's relationship + to a set of values. Valid operators + are In, NotIn, Exists, DoesNotExist. + Gt, and Lt. + type: string + values: + description: An array of string values. + If the operator is In or NotIn, + the values array must be non-empty. + If the operator is Exists or DoesNotExist, + the values array must be empty. + If the operator is Gt or Lt, the + values array must have a single + element, which will be interpreted + as an integer. This array is replaced + during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + type: object + type: array + required: + - nodeSelectorTerms + type: object + type: object + podAffinity: + description: Describes pod affinity scheduling rules (e.g. + co-locate this pod in the same node, zone, etc. as some + other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling affinity expressions, + etc.), compute a sum by iterating through the elements + of this field and adding "weight" to the sum if + the node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest sum + are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in the + range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + affinity requirements specified by this field cease + to be met at some point during pod execution (e.g. + due to a pod label update), the system may or may + not try to eventually evict the pod from its node. + When there are multiple elements, the lists of nodes + corresponding to each podAffinityTerm are intersected, + i.e. all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the given + namespace(s)) that this pod should be co-located + (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node + whose value of the label with key + matches that of any node on which a pod of the + set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. The term is applied + to the union of the namespaces selected by + this field and the ones listed in the namespaces + field. null selector and null or empty namespaces + list means "this pod's namespace". An empty + selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + The term is applied to the union of the namespaces + listed in this field and the ones selected + by namespaceSelector. null or empty namespaces + list and null namespaceSelector means "this + pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the + pods matching the labelSelector in the specified + namespaces, where co-located is defined as + running on a node whose value of the label + with key topologyKey matches that of any node + on which any of the selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + podAntiAffinity: + description: Describes pod anti-affinity scheduling rules + (e.g. avoid putting this pod in the same node, zone, + etc. as some other pod(s)). + properties: + preferredDuringSchedulingIgnoredDuringExecution: + description: The scheduler will prefer to schedule + pods to nodes that satisfy the anti-affinity expressions + specified by this field, but it may choose a node + that violates one or more of the expressions. The + node that is most preferred is the one with the + greatest sum of weights, i.e. for each node that + meets all of the scheduling requirements (resource + request, requiredDuringScheduling anti-affinity + expressions, etc.), compute a sum by iterating through + the elements of this field and adding "weight" to + the sum if the node has pods which matches the corresponding + podAffinityTerm; the node(s) with the highest sum + are the most preferred. + items: + description: The weights of all of the matched WeightedPodAffinityTerm + fields are added per-node to find the most preferred + node(s) + properties: + podAffinityTerm: + description: Required. A pod affinity term, + associated with the corresponding weight. + properties: + labelSelector: + description: A label query over a set of + resources, in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set + of namespaces that the term applies to. + The term is applied to the union of the + namespaces selected by this field and + the ones listed in the namespaces field. + null selector and null or empty namespaces + list means "this pod's namespace". An + empty selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static + list of namespace names that the term + applies to. The term is applied to the + union of the namespaces listed in this + field and the ones selected by namespaceSelector. + null or empty namespaces list and null + namespaceSelector means "this pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located + (affinity) or not co-located (anti-affinity) + with the pods matching the labelSelector + in the specified namespaces, where co-located + is defined as running on a node whose + value of the label with key topologyKey + matches that of any node on which any + of the selected pods is running. Empty + topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + weight: + description: weight associated with matching + the corresponding podAffinityTerm, in the + range 1-100. + format: int32 + type: integer + required: + - podAffinityTerm + - weight + type: object + type: array + requiredDuringSchedulingIgnoredDuringExecution: + description: If the anti-affinity requirements specified + by this field are not met at scheduling time, the + pod will not be scheduled onto the node. If the + anti-affinity requirements specified by this field + cease to be met at some point during pod execution + (e.g. due to a pod label update), the system may + or may not try to eventually evict the pod from + its node. When there are multiple elements, the + lists of nodes corresponding to each podAffinityTerm + are intersected, i.e. all terms must be satisfied. + items: + description: Defines a set of pods (namely those + matching the labelSelector relative to the given + namespace(s)) that this pod should be co-located + (affinity) or not co-located (anti-affinity) with, + where co-located is defined as running on a node + whose value of the label with key + matches that of any node on which a pod of the + set of pods is running + properties: + labelSelector: + description: A label query over a set of resources, + in this case pods. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaceSelector: + description: A label query over the set of namespaces + that the term applies to. The term is applied + to the union of the namespaces selected by + this field and the ones listed in the namespaces + field. null selector and null or empty namespaces + list means "this pod's namespace". An empty + selector ({}) matches all namespaces. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The requirements + are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label key + that the selector applies to. + type: string + operator: + description: operator represents a + key's relationship to a set of values. + Valid operators are In, NotIn, Exists + and DoesNotExist. + type: string + values: + description: values is an array of + string values. If the operator is + In or NotIn, the values array must + be non-empty. If the operator is + Exists or DoesNotExist, the values + array must be empty. This array + is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are ANDed. + type: object + type: object + namespaces: + description: namespaces specifies a static list + of namespace names that the term applies to. + The term is applied to the union of the namespaces + listed in this field and the ones selected + by namespaceSelector. null or empty namespaces + list and null namespaceSelector means "this + pod's namespace". + items: + type: string + type: array + topologyKey: + description: This pod should be co-located (affinity) + or not co-located (anti-affinity) with the + pods matching the labelSelector in the specified + namespaces, where co-located is defined as + running on a node whose value of the label + with key topologyKey matches that of any node + on which any of the selected pods is running. + Empty topologyKey is not allowed. + type: string + required: + - topologyKey + type: object + type: array + type: object + type: object + automountServiceAccountToken: + description: AutomountServiceAccountToken indicates whether + a service account token should be automatically mounted. + type: boolean + containers: + description: List of containers belonging to the pod. Containers + cannot currently be added or removed. There must be at least + one container in a Pod. Cannot be updated. + items: + description: A single application container that you want + to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. The container + image''s CMD is used if this is not provided. Variable + references $(VAR_NAME) are expanded using the container''s + environment. If a variable cannot be resolved, the + reference in the input string will be unchanged. Double + $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The container image''s ENTRYPOINT is used + if this is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. If + a variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config + management to default or override container images + in workload controllers like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system should + take in response to container lifecycle events. Cannot + be updated. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container liveness. + Container will be restarted if the probe fails. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: List of ports to expose from the container. + Not specifying a port here DOES NOT prevent that port + from being exposed. Any port which is listening on + the default "0.0.0.0" address inside a container will + be accessible from the network. Modifying this array + with strategic merge patch may corrupt the data. For + more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container service readiness. + Container will be removed from service endpoints if + the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by this container. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the security options + the container should be run with. If set, the fields + of SecurityContext override the equivalent fields + of PodSecurityContext. More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that the Pod has + successfully initialized. If specified, no other probes + are executed until this completes successfully. If + this probe fails, the Pod will be restarted, just + as if the livenessProbe failed. This can be used to + provide different probe parameters at the beginning + of a Pod''s lifecycle, when it might take a long time + to load data or warm a cache, than during steady-state + operation. This cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + dnsConfig: + description: Specifies the DNS parameters of a pod. Parameters + specified here will be merged to the generated DNS configuration + based on DNSPolicy. + properties: + nameservers: + description: A list of DNS name server IP addresses. This + will be appended to the base nameservers generated from + DNSPolicy. Duplicated nameservers will be removed. + items: + type: string + type: array + options: + description: A list of DNS resolver options. This will + be merged with the base options generated from DNSPolicy. + Duplicated entries will be removed. Resolution options + given in Options will override those that appear in + the base DNSPolicy. + items: + description: PodDNSConfigOption defines DNS resolver + options of a pod. + properties: + name: + description: Required. + type: string + value: + type: string + type: object + type: array + searches: + description: A list of DNS search domains for host-name + lookup. This will be appended to the base search paths + generated from DNSPolicy. Duplicated search paths will + be removed. + items: + type: string + type: array + type: object + dnsPolicy: + description: Set DNS policy for the pod. Defaults to "ClusterFirst". + Valid values are 'ClusterFirstWithHostNet', 'ClusterFirst', + 'Default' or 'None'. DNS parameters given in DNSConfig will + be merged with the policy selected with DNSPolicy. To have + DNS options set along with hostNetwork, you have to specify + DNS policy explicitly to 'ClusterFirstWithHostNet'. + type: string + enableServiceLinks: + description: 'EnableServiceLinks indicates whether information + about services should be injected into pod''s environment + variables, matching the syntax of Docker links. Optional: + Defaults to true.' + type: boolean + ephemeralContainers: + description: List of ephemeral containers run in this pod. + Ephemeral containers may be run in an existing pod to perform + user-initiated actions such as debugging. This list cannot + be specified when creating a pod, and it cannot be modified + by updating the pod spec. In order to add an ephemeral container + to an existing pod, use the pod's ephemeralcontainers subresource. + items: + description: "An EphemeralContainer is a temporary container + that you may add to an existing Pod for user-initiated + activities such as debugging. Ephemeral containers have + no resource or scheduling guarantees, and they will not + be restarted when they exit or when a Pod is removed or + restarted. The kubelet may evict a Pod if an ephemeral + container causes the Pod to exceed its resource allocation. + \n To add an ephemeral container, use the ephemeralcontainers + subresource of an existing Pod. Ephemeral containers may + not be removed or restarted." + properties: + args: + description: 'Arguments to the entrypoint. The image''s + CMD is used if this is not provided. Variable references + $(VAR_NAME) are expanded using the container''s environment. + If a variable cannot be resolved, the reference in + the input string will be unchanged. Double $$ are + reduced to a single $, which allows for escaping the + $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The image''s ENTRYPOINT is used if this is + not provided. Variable references $(VAR_NAME) are + expanded using the container''s environment. If a + variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Lifecycle is not allowed for ephemeral + containers. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the ephemeral container specified + as a DNS_LABEL. This name must be unique among all + containers, init containers and ephemeral containers. + type: string + ports: + description: Ports are not allowed for ephemeral containers. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: Resources are not allowed for ephemeral + containers. Ephemeral containers use spare resources + already allocated to the pod. + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'Optional: SecurityContext defines the + security options the ephemeral container should be + run with. If set, the fields of SecurityContext override + the equivalent fields of PodSecurityContext.' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: Probes are not allowed for ephemeral containers. + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + targetContainerName: + description: "If set, the name of the container from + PodSpec that this ephemeral container targets. The + ephemeral container will be run in the namespaces + (IPC, PID, etc) of this container. If not set then + the ephemeral container uses the namespaces configured + in the Pod spec. \n The container runtime must implement + support for this feature. If the runtime does not + support namespace targeting then the result of setting + this field is undefined." + type: string + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Subpath mounts are not allowed for ephemeral + containers. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + hostAliases: + description: HostAliases is an optional list of hosts and + IPs that will be injected into the pod's hosts file if specified. + This is only valid for non-hostNetwork pods. + items: + description: HostAlias holds the mapping between IP and + hostnames that will be injected as an entry in the pod's + hosts file. + properties: + hostnames: + description: Hostnames for the above IP address. + items: + type: string + type: array + ip: + description: IP address of the host file entry. + type: string + type: object + type: array + hostIPC: + description: 'Use the host''s ipc namespace. Optional: Default + to false.' + type: boolean + hostNetwork: + description: Host networking requested for this pod. Use the + host's network namespace. If this option is set, the ports + that will be used must be specified. Default to false. + type: boolean + hostPID: + description: 'Use the host''s pid namespace. Optional: Default + to false.' + type: boolean + hostUsers: + description: 'Use the host''s user namespace. Optional: Default + to true. If set to true or not present, the pod will be + run in the host user namespace, useful for when the pod + needs a feature only available to the host user namespace, + such as loading a kernel module with CAP_SYS_MODULE. When + set to false, a new userns is created for the pod. Setting + false is useful for mitigating container breakout vulnerabilities + even allowing users to run their containers as root without + actually having root privileges on the host. This field + is alpha-level and is only honored by servers that enable + the UserNamespacesSupport feature.' + type: boolean + hostname: + description: Specifies the hostname of the Pod If not specified, + the pod's hostname will be set to a system-defined value. + type: string + imagePullSecrets: + description: 'ImagePullSecrets is an optional list of references + to secrets in the same namespace to use for pulling any + of the images used by this PodSpec. If specified, these + secrets will be passed to individual puller implementations + for them to use. More info: https://kubernetes.io/docs/concepts/containers/images#specifying-imagepullsecrets-on-a-pod' + 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 + initContainers: + description: 'List of initialization containers belonging + to the pod. Init containers are executed in order prior + to containers being started. If any init container fails, + the pod is considered to have failed and is handled according + to its restartPolicy. The name for an init container or + normal container must be unique among all containers. Init + containers may not have Lifecycle actions, Readiness probes, + Liveness probes, or Startup probes. The resourceRequirements + of an init container are taken into account during scheduling + by finding the highest request/limit for each resource type, + and then using the max of of that value or the sum of the + normal containers. Limits are applied to init containers + in a similar fashion. Init containers cannot currently be + added or removed. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/init-containers/' + items: + description: A single application container that you want + to run within a pod. + properties: + args: + description: 'Arguments to the entrypoint. The container + image''s CMD is used if this is not provided. Variable + references $(VAR_NAME) are expanded using the container''s + environment. If a variable cannot be resolved, the + reference in the input string will be unchanged. Double + $$ are reduced to a single $, which allows for escaping + the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" will produce + the string literal "$(VAR_NAME)". Escaped references + will never be expanded, regardless of whether the + variable exists or not. Cannot be updated. More info: + https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + command: + description: 'Entrypoint array. Not executed within + a shell. The container image''s ENTRYPOINT is used + if this is not provided. Variable references $(VAR_NAME) + are expanded using the container''s environment. If + a variable cannot be resolved, the reference in the + input string will be unchanged. Double $$ are reduced + to a single $, which allows for escaping the $(VAR_NAME) + syntax: i.e. "$$(VAR_NAME)" will produce the string + literal "$(VAR_NAME)". Escaped references will never + be expanded, regardless of whether the variable exists + or not. Cannot be updated. More info: https://kubernetes.io/docs/tasks/inject-data-application/define-command-argument-container/#running-a-command-in-a-shell' + items: + type: string + type: array + env: + description: List of environment variables to set in + the container. Cannot be updated. + items: + description: EnvVar represents an environment variable + present in a Container. + properties: + name: + description: Name of the environment variable. + Must be a C_IDENTIFIER. + type: string + value: + description: 'Variable references $(VAR_NAME) + are expanded using the previously defined environment + variables in the container and any service environment + variables. If a variable cannot be resolved, + the reference in the input string will be unchanged. + Double $$ are reduced to a single $, which allows + for escaping the $(VAR_NAME) syntax: i.e. "$$(VAR_NAME)" + will produce the string literal "$(VAR_NAME)". + Escaped references will never be expanded, regardless + of whether the variable exists or not. Defaults + to "".' + type: string + valueFrom: + description: Source for the environment variable's + value. Cannot be used if value is not empty. + properties: + configMapKeyRef: + description: Selects a key of a ConfigMap. + properties: + key: + description: The key to select. + type: string + 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 + optional: + description: Specify whether the ConfigMap + or its key must be defined + type: boolean + required: + - key + type: object + fieldRef: + description: 'Selects a field of the pod: + supports metadata.name, metadata.namespace, + `metadata.labels['''']`, `metadata.annotations['''']`, + spec.nodeName, spec.serviceAccountName, + status.hostIP, status.podIP, status.podIPs.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, limits.ephemeral-storage, + requests.cpu, requests.memory and requests.ephemeral-storage) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + secretKeyRef: + description: Selects a key of a secret in + the pod's namespace + properties: + key: + description: The key of the secret to + select from. Must be a valid secret + key. + type: string + 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 + optional: + description: Specify whether the Secret + or its key must be defined + type: boolean + required: + - key + type: object + type: object + required: + - name + type: object + type: array + envFrom: + description: List of sources to populate environment + variables in the container. The keys defined within + a source must be a C_IDENTIFIER. All invalid keys + will be reported as an event when the container is + starting. When a key exists in multiple sources, the + value associated with the last source will take precedence. + Values defined by an Env with a duplicate key will + take precedence. Cannot be updated. + items: + description: EnvFromSource represents the source of + a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + 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 + optional: + description: Specify whether the ConfigMap + must be defined + type: boolean + type: object + prefix: + description: An optional identifier to prepend + to each key in the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + 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 + optional: + description: Specify whether the Secret must + be defined + type: boolean + type: object + type: object + type: array + image: + description: 'Container image name. More info: https://kubernetes.io/docs/concepts/containers/images + This field is optional to allow higher level config + management to default or override container images + in workload controllers like Deployments and StatefulSets.' + type: string + imagePullPolicy: + description: 'Image pull policy. One of Always, Never, + IfNotPresent. Defaults to Always if :latest tag is + specified, or IfNotPresent otherwise. Cannot be updated. + More info: https://kubernetes.io/docs/concepts/containers/images#updating-images' + type: string + lifecycle: + description: Actions that the management system should + take in response to container lifecycle events. Cannot + be updated. + properties: + postStart: + description: 'PostStart is called immediately after + a container is created. If the handler fails, + the container is terminated and restarted according + to its restart policy. Other management of the + container blocks until the hook completes. More + info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + preStop: + description: 'PreStop is called immediately before + a container is terminated due to an API request + or management event such as liveness/startup probe + failure, preemption, resource contention, etc. + The handler is not called if the container crashes + or exits. The Pod''s termination grace period + countdown begins before the PreStop hook is executed. + Regardless of the outcome of the handler, the + container will eventually terminate within the + Pod''s termination grace period (unless delayed + by finalizers). Other management of the container + blocks until the hook completes or until the termination + grace period is reached. More info: https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#container-hooks' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line + to execute inside the container, the working + directory for the command is root ('/') + in the container's filesystem. The command + is simply exec'd, it is not run inside + a shell, so traditional shell instructions + ('|', etc) won't work. To use a shell, + you need to explicitly call out to that + shell. Exit status of 0 is treated as + live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set + "Host" in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the + request. HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP + server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting + to the host. Defaults to HTTP. + type: string + required: + - port + type: object + tcpSocket: + description: Deprecated. TCPSocket is NOT supported + as a LifecycleHandler and kept for the backward + compatibility. There are no validation of + this field and lifecycle hooks will fail in + runtime when tcp handler is specified. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port + to access on the container. Number must + be in the range 1 to 65535. Name must + be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + type: object + type: object + livenessProbe: + description: 'Periodic probe of container liveness. + Container will be restarted if the probe fails. Cannot + be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + name: + description: Name of the container specified as a DNS_LABEL. + Each container in a pod must have a unique name (DNS_LABEL). + Cannot be updated. + type: string + ports: + description: List of ports to expose from the container. + Not specifying a port here DOES NOT prevent that port + from being exposed. Any port which is listening on + the default "0.0.0.0" address inside a container will + be accessible from the network. Modifying this array + with strategic merge patch may corrupt the data. For + more information See https://github.com/kubernetes/kubernetes/issues/108255. + Cannot be updated. + items: + description: ContainerPort represents a network port + in a single container. + properties: + containerPort: + description: Number of port to expose on the pod's + IP address. This must be a valid port number, + 0 < x < 65536. + format: int32 + type: integer + hostIP: + description: What host IP to bind the external + port to. + type: string + hostPort: + description: Number of port to expose on the host. + If specified, this must be a valid port number, + 0 < x < 65536. If HostNetwork is specified, + this must match ContainerPort. Most containers + do not need this. + format: int32 + type: integer + name: + description: If specified, this must be an IANA_SVC_NAME + and unique within the pod. Each named port in + a pod must have a unique name. Name for the + port that can be referred to by services. + type: string + protocol: + default: TCP + description: Protocol for port. Must be UDP, TCP, + or SCTP. Defaults to "TCP". + type: string + required: + - containerPort + type: object + type: array + x-kubernetes-list-map-keys: + - containerPort + - protocol + x-kubernetes-list-type: map + readinessProbe: + description: 'Periodic probe of container service readiness. + Container will be removed from service endpoints if + the probe fails. Cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + resources: + description: 'Compute Resources required by this container. + Cannot be updated. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + properties: + claims: + description: "Claims lists the names of resources, + defined in spec.resourceClaims, that are used + by this container. \n This is an alpha field and + requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references one entry + in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one + entry in pod.spec.resourceClaims of the + Pod where this field is used. It makes that + resource available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount + of compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is + omitted for a container, it defaults to Limits + if that is explicitly specified, otherwise to + an implementation-defined value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + securityContext: + description: 'SecurityContext defines the security options + the container should be run with. If set, the fields + of SecurityContext override the equivalent fields + of PodSecurityContext. More info: https://kubernetes.io/docs/tasks/configure-pod-container/security-context/' + properties: + allowPrivilegeEscalation: + description: 'AllowPrivilegeEscalation controls + whether a process can gain more privileges than + its parent process. This bool directly controls + if the no_new_privs flag will be set on the container + process. AllowPrivilegeEscalation is true always + when the container is: 1) run as Privileged 2) + has CAP_SYS_ADMIN Note that this field cannot + be set when spec.os.name is windows.' + type: boolean + capabilities: + description: The capabilities to add/drop when running + containers. Defaults to the default set of capabilities + granted by the container runtime. Note that this + field cannot be set when spec.os.name is windows. + properties: + add: + description: Added capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + drop: + description: Removed capabilities + items: + description: Capability represent POSIX capabilities + type + type: string + type: array + type: object + privileged: + description: Run container in privileged mode. Processes + in privileged containers are essentially equivalent + to root on the host. Defaults to false. Note that + this field cannot be set when spec.os.name is + windows. + type: boolean + procMount: + description: procMount denotes the type of proc + mount to use for the containers. The default is + DefaultProcMount which uses the container runtime + defaults for readonly paths and masked paths. + This requires the ProcMountType feature flag to + be enabled. Note that this field cannot be set + when spec.os.name is windows. + type: string + readOnlyRootFilesystem: + description: Whether this container has a read-only + root filesystem. Default is false. Note that this + field cannot be set when spec.os.name is windows. + type: boolean + runAsGroup: + description: The GID to run the entrypoint of the + container process. Uses runtime default if unset. + May also be set in PodSecurityContext. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run + as a non-root user. If true, the Kubelet will + validate the image at runtime to ensure that it + does not run as UID 0 (root) and fail to start + the container if it does. If unset or false, no + such validation will be performed. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the + container process. Defaults to user specified + in image metadata if unspecified. May also be + set in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in + SecurityContext takes precedence. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to + the container. If unspecified, the container runtime + will allocate a random SELinux context for each + container. May also be set in PodSecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is windows. + properties: + level: + description: Level is SELinux level label that + applies to the container. + type: string + role: + description: Role is a SELinux role label that + applies to the container. + type: string + type: + description: Type is a SELinux type label that + applies to the container. + type: string + user: + description: User is a SELinux user label that + applies to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by this + container. If seccomp options are provided at + both the pod & container level, the container + options override the pod options. Note that this + field cannot be set when spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. + The profile must be preconfigured on the node + to work. Must be a descending path, relative + to the kubelet's configured seccomp profile + location. Must only be set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: + \n Localhost - a profile defined in a file + on the node should be used. RuntimeDefault + - the container runtime default profile should + be used. Unconfined - no profile should be + applied." + type: string + required: + - type + type: object + windowsOptions: + description: The Windows specific settings applied + to all containers. If unspecified, the options + from the PodSecurityContext will be used. If set + in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence. + Note that this field cannot be set when spec.os.name + is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the + GMSA admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential + spec named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name + of the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. + This field is alpha-level and will only be + honored by components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the + feature flag will result in errors when validating + the Pod. All of a Pod's containers must have + the same effective HostProcess value (it is + not allowed to have a mix of HostProcess containers + and non-HostProcess containers). In addition, + if HostProcess is true then HostNetwork must + also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run + the entrypoint of the container process. Defaults + to the user specified in image metadata if + unspecified. May also be set in PodSecurityContext. + If set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes + precedence. + type: string + type: object + type: object + startupProbe: + description: 'StartupProbe indicates that the Pod has + successfully initialized. If specified, no other probes + are executed until this completes successfully. If + this probe fails, the Pod will be restarted, just + as if the livenessProbe failed. This can be used to + provide different probe parameters at the beginning + of a Pod''s lifecycle, when it might take a long time + to load data or warm a cache, than during steady-state + operation. This cannot be updated. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + properties: + exec: + description: Exec specifies the action to take. + properties: + command: + description: Command is the command line to + execute inside the container, the working + directory for the command is root ('/') in + the container's filesystem. The command is + simply exec'd, it is not run inside a shell, + so traditional shell instructions ('|', etc) + won't work. To use a shell, you need to explicitly + call out to that shell. Exit status of 0 is + treated as live/healthy and non-zero is unhealthy. + items: + type: string + type: array + type: object + failureThreshold: + description: Minimum consecutive failures for the + probe to be considered failed after having succeeded. + Defaults to 3. Minimum value is 1. + format: int32 + type: integer + grpc: + description: GRPC specifies an action involving + a GRPC port. This is a beta field and requires + enabling GRPCContainerProbe feature gate. + properties: + port: + description: Port number of the gRPC service. + Number must be in the range 1 to 65535. + format: int32 + type: integer + service: + description: "Service is the name of the service + to place in the gRPC HealthCheckRequest (see + https://github.com/grpc/grpc/blob/master/doc/health-checking.md). + \n If this is not specified, the default behavior + is defined by gRPC." + type: string + required: + - port + type: object + httpGet: + description: HTTPGet specifies the http request + to perform. + properties: + host: + description: Host name to connect to, defaults + to the pod IP. You probably want to set "Host" + in httpHeaders instead. + type: string + httpHeaders: + description: Custom headers to set in the request. + HTTP allows repeated headers. + items: + description: HTTPHeader describes a custom + header to be used in HTTP probes + properties: + name: + description: The header field name + type: string + value: + description: The header field value + type: string + required: + - name + - value + type: object + type: array + path: + description: Path to access on the HTTP server. + type: string + port: + anyOf: + - type: integer + - type: string + description: Name or number of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + scheme: + description: Scheme to use for connecting to + the host. Defaults to HTTP. + type: string + required: + - port + type: object + initialDelaySeconds: + description: 'Number of seconds after the container + has started before liveness probes are initiated. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + periodSeconds: + description: How often (in seconds) to perform the + probe. Default to 10 seconds. Minimum value is + 1. + format: int32 + type: integer + successThreshold: + description: Minimum consecutive successes for the + probe to be considered successful after having + failed. Defaults to 1. Must be 1 for liveness + and startup. Minimum value is 1. + format: int32 + type: integer + tcpSocket: + description: TCPSocket specifies an action involving + a TCP port. + properties: + host: + description: 'Optional: Host name to connect + to, defaults to the pod IP.' + type: string + port: + anyOf: + - type: integer + - type: string + description: Number or name of the port to access + on the container. Number must be in the range + 1 to 65535. Name must be an IANA_SVC_NAME. + x-kubernetes-int-or-string: true + required: + - port + type: object + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod + needs to terminate gracefully upon probe failure. + The grace period is the duration in seconds after + the processes running in the pod are sent a termination + signal and the time when the processes are forcibly + halted with a kill signal. Set this value longer + than the expected cleanup time for your process. + If this value is nil, the pod's terminationGracePeriodSeconds + will be used. Otherwise, this value overrides + the value provided by the pod spec. Value must + be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity + to shut down). This is a beta field and requires + enabling ProbeTerminationGracePeriod feature gate. + Minimum value is 1. spec.terminationGracePeriodSeconds + is used if unset. + format: int64 + type: integer + timeoutSeconds: + description: 'Number of seconds after which the + probe times out. Defaults to 1 second. Minimum + value is 1. More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle#container-probes' + format: int32 + type: integer + type: object + stdin: + description: Whether this container should allocate + a buffer for stdin in the container runtime. If this + is not set, reads from stdin in the container will + always result in EOF. Default is false. + type: boolean + stdinOnce: + description: Whether the container runtime should close + the stdin channel after it has been opened by a single + attach. When stdin is true the stdin stream will remain + open across multiple attach sessions. If stdinOnce + is set to true, stdin is opened on container start, + is empty until the first client attaches to stdin, + and then remains open and accepts data until the client + disconnects, at which time stdin is closed and remains + closed until the container is restarted. If this flag + is false, a container processes that reads from stdin + will never receive an EOF. Default is false + type: boolean + terminationMessagePath: + description: 'Optional: Path at which the file to which + the container''s termination message will be written + is mounted into the container''s filesystem. Message + written is intended to be brief final status, such + as an assertion failure message. Will be truncated + by the node if greater than 4096 bytes. The total + message length across all containers will be limited + to 12kb. Defaults to /dev/termination-log. Cannot + be updated.' + type: string + terminationMessagePolicy: + description: Indicate how the termination message should + be populated. File will use the contents of terminationMessagePath + to populate the container status message on both success + and failure. FallbackToLogsOnError will use the last + chunk of container log output if the termination message + file is empty and the container exited with an error. + The log output is limited to 2048 bytes or 80 lines, + whichever is smaller. Defaults to File. Cannot be + updated. + type: string + tty: + description: Whether this container should allocate + a TTY for itself, also requires 'stdin' to be true. + Default is false. + type: boolean + volumeDevices: + description: volumeDevices is the list of block devices + to be used by the container. + items: + description: volumeDevice describes a mapping of a + raw block device within a container. + properties: + devicePath: + description: devicePath is the path inside of + the container that the device will be mapped + to. + type: string + name: + description: name must match the name of a persistentVolumeClaim + in the pod + type: string + required: + - devicePath + - name + type: object + type: array + volumeMounts: + description: Pod volumes to mount into the container's + filesystem. Cannot be updated. + items: + description: VolumeMount describes a mounting of a + Volume within a container. + properties: + mountPath: + description: Path within the container at which + the volume should be mounted. Must not contain + ':'. + type: string + mountPropagation: + description: mountPropagation determines how mounts + are propagated from the host to container and + the other way around. When not set, MountPropagationNone + is used. This field is beta in 1.10. + type: string + name: + description: This must match the Name of a Volume. + type: string + readOnly: + description: Mounted read-only if true, read-write + otherwise (false or unspecified). Defaults to + false. + type: boolean + subPath: + description: Path within the volume from which + the container's volume should be mounted. Defaults + to "" (volume's root). + type: string + subPathExpr: + description: Expanded path within the volume from + which the container's volume should be mounted. + Behaves similarly to SubPath but environment + variable references $(VAR_NAME) are expanded + using the container's environment. Defaults + to "" (volume's root). SubPathExpr and SubPath + are mutually exclusive. + type: string + required: + - mountPath + - name + type: object + type: array + workingDir: + description: Container's working directory. If not specified, + the container runtime's default will be used, which + might be configured in the container image. Cannot + be updated. + type: string + required: + - name + type: object + type: array + nodeName: + description: NodeName is a request to schedule this pod onto + a specific node. If it is non-empty, the scheduler simply + schedules this pod onto that node, assuming that it fits + resource requirements. + type: string + nodeSelector: + additionalProperties: + type: string + description: 'NodeSelector is a selector which must be true + for the pod to fit on a node. Selector which must match + a node''s labels for the pod to be scheduled on that node. + More info: https://kubernetes.io/docs/concepts/configuration/assign-pod-node/' + type: object + x-kubernetes-map-type: atomic + os: + description: "Specifies the OS of the containers in the pod. + Some pod and container fields are restricted if this is + set. \n If the OS field is set to linux, the following fields + must be unset: -securityContext.windowsOptions \n If the + OS field is set to windows, following fields must be unset: + - spec.hostPID - spec.hostIPC - spec.hostUsers - spec.securityContext.seLinuxOptions + - spec.securityContext.seccompProfile - spec.securityContext.fsGroup + - spec.securityContext.fsGroupChangePolicy - spec.securityContext.sysctls + - spec.shareProcessNamespace - spec.securityContext.runAsUser + - spec.securityContext.runAsGroup - spec.securityContext.supplementalGroups + - spec.containers[*].securityContext.seLinuxOptions - spec.containers[*].securityContext.seccompProfile + - spec.containers[*].securityContext.capabilities - spec.containers[*].securityContext.readOnlyRootFilesystem + - spec.containers[*].securityContext.privileged - spec.containers[*].securityContext.allowPrivilegeEscalation + - spec.containers[*].securityContext.procMount - spec.containers[*].securityContext.runAsUser + - spec.containers[*].securityContext.runAsGroup" + properties: + name: + description: 'Name is the name of the operating system. + The currently supported values are linux and windows. + Additional value may be defined in future and can be + one of: https://github.com/opencontainers/runtime-spec/blob/master/config.md#platform-specific-configuration + Clients should expect to handle additional values and + treat unrecognized values in this field as os: null' + type: string + required: + - name + type: object + overhead: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Overhead represents the resource overhead associated + with running a pod for a given RuntimeClass. This field + will be autopopulated at admission time by the RuntimeClass + admission controller. If the RuntimeClass admission controller + is enabled, overhead must not be set in Pod create requests. + The RuntimeClass admission controller will reject Pod create + requests which have the overhead already set. If RuntimeClass + is configured and selected in the PodSpec, Overhead will + be set to the value defined in the corresponding RuntimeClass, + otherwise it will remain unset and treated as zero. More + info: https://git.k8s.io/enhancements/keps/sig-node/688-pod-overhead/README.md' + type: object + preemptionPolicy: + description: PreemptionPolicy is the Policy for preempting + pods with lower priority. One of Never, PreemptLowerPriority. + Defaults to PreemptLowerPriority if unset. + type: string + priority: + description: The priority value. Various system components + use this field to find the priority of the pod. When Priority + Admission Controller is enabled, it prevents users from + setting this field. The admission controller populates this + field from PriorityClassName. The higher the value, the + higher the priority. + format: int32 + type: integer + priorityClassName: + description: If specified, indicates the pod's priority. "system-node-critical" + and "system-cluster-critical" are two special keywords which + indicate the highest priorities with the former being the + highest priority. Any other name must be defined by creating + a PriorityClass object with that name. If not specified, + the pod priority will be default or zero if there is no + default. + type: string + readinessGates: + description: 'If specified, all readiness gates will be evaluated + for pod readiness. A pod is ready when all its containers + are ready AND all conditions specified in the readiness + gates have status equal to "True" More info: https://git.k8s.io/enhancements/keps/sig-network/580-pod-readiness-gates' + items: + description: PodReadinessGate contains the reference to + a pod condition + properties: + conditionType: + description: ConditionType refers to a condition in + the pod's condition list with matching type. + type: string + required: + - conditionType + type: object + type: array + resourceClaims: + description: "ResourceClaims defines which ResourceClaims + must be allocated and reserved before the Pod is allowed + to start. The resources will be made available to those + containers which consume them by name. \n This is an alpha + field and requires enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: PodResourceClaim references exactly one ResourceClaim + through a ClaimSource. It adds a name to it that uniquely + identifies the ResourceClaim inside the Pod. Containers + that need access to the ResourceClaim reference it with + this name. + properties: + name: + description: Name uniquely identifies this resource + claim inside the pod. This must be a DNS_LABEL. + type: string + source: + description: Source describes where to find the ResourceClaim. + properties: + resourceClaimName: + description: ResourceClaimName is the name of a + ResourceClaim object in the same namespace as + this pod. + type: string + resourceClaimTemplateName: + description: "ResourceClaimTemplateName is the name + of a ResourceClaimTemplate object in the same + namespace as this pod. \n The template will be + used to create a new ResourceClaim, which will + be bound to this pod. When this pod is deleted, + the ResourceClaim will also be deleted. The name + of the ResourceClaim will be -, where is the PodResourceClaim.Name. + Pod validation will reject the pod if the concatenated + name is not valid for a ResourceClaim (e.g. too + long). \n An existing ResourceClaim with that + name that is not owned by the pod will not be + used for the pod to avoid using an unrelated resource + by mistake. Scheduling and pod startup are then + blocked until the unrelated ResourceClaim is removed. + \n This field is immutable and no changes will + be made to the corresponding ResourceClaim by + the control plane after creating the ResourceClaim." + type: string + type: object + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + restartPolicy: + description: 'Restart policy for all containers within the + pod. One of Always, OnFailure, Never. Default to Always. + More info: https://kubernetes.io/docs/concepts/workloads/pods/pod-lifecycle/#restart-policy' + type: string + runtimeClassName: + description: 'RuntimeClassName refers to a RuntimeClass object + in the node.k8s.io group, which should be used to run this + pod. If no RuntimeClass resource matches the named class, + the pod will not be run. If unset or empty, the "legacy" + RuntimeClass will be used, which is an implicit class with + an empty definition that uses the default runtime handler. + More info: https://git.k8s.io/enhancements/keps/sig-node/585-runtime-class' + type: string + schedulerName: + description: If specified, the pod will be dispatched by specified + scheduler. If not specified, the pod will be dispatched + by default scheduler. + type: string + schedulingGates: + description: "SchedulingGates is an opaque list of values + that if specified will block scheduling the pod. More info: + \ https://git.k8s.io/enhancements/keps/sig-scheduling/3521-pod-scheduling-readiness. + \n This is an alpha-level feature enabled by PodSchedulingReadiness + feature gate." + items: + description: PodSchedulingGate is associated to a Pod to + guard its scheduling. + properties: + name: + description: Name of the scheduling gate. Each scheduling + gate must have a unique name field. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + securityContext: + description: 'SecurityContext holds pod-level security attributes + and common container settings. Optional: Defaults to empty. See + type description for default values of each field.' + properties: + fsGroup: + description: "A special supplemental group that applies + to all containers in a pod. Some volume types allow + the Kubelet to change the ownership of that volume to + be owned by the pod: \n 1. The owning GID will be the + FSGroup 2. The setgid bit is set (new files created + in the volume will be owned by FSGroup) 3. The permission + bits are OR'd with rw-rw---- \n If unset, the Kubelet + will not modify the ownership and permissions of any + volume. Note that this field cannot be set when spec.os.name + is windows." + format: int64 + type: integer + fsGroupChangePolicy: + description: 'fsGroupChangePolicy defines behavior of + changing ownership and permission of the volume before + being exposed inside Pod. This field will only apply + to volume types which support fsGroup based ownership(and + permissions). It will have no effect on ephemeral volume + types such as: secret, configmaps and emptydir. Valid + values are "OnRootMismatch" and "Always". If not specified, + "Always" is used. Note that this field cannot be set + when spec.os.name is windows.' + type: string + runAsGroup: + description: The GID to run the entrypoint of the container + process. Uses runtime default if unset. May also be + set in SecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. Note that this + field cannot be set when spec.os.name is windows. + format: int64 + type: integer + runAsNonRoot: + description: Indicates that the container must run as + a non-root user. If true, the Kubelet will validate + the image at runtime to ensure that it does not run + as UID 0 (root) and fail to start the container if it + does. If unset or false, no such validation will be + performed. May also be set in SecurityContext. If set + in both SecurityContext and PodSecurityContext, the + value specified in SecurityContext takes precedence. + type: boolean + runAsUser: + description: The UID to run the entrypoint of the container + process. Defaults to user specified in image metadata + if unspecified. May also be set in SecurityContext. If + set in both SecurityContext and PodSecurityContext, + the value specified in SecurityContext takes precedence + for that container. Note that this field cannot be set + when spec.os.name is windows. + format: int64 + type: integer + seLinuxOptions: + description: The SELinux context to be applied to all + containers. If unspecified, the container runtime will + allocate a random SELinux context for each container. May + also be set in SecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence for that container. Note that this + field cannot be set when spec.os.name is windows. + properties: + level: + description: Level is SELinux level label that applies + to the container. + type: string + role: + description: Role is a SELinux role label that applies + to the container. + type: string + type: + description: Type is a SELinux type label that applies + to the container. + type: string + user: + description: User is a SELinux user label that applies + to the container. + type: string + type: object + seccompProfile: + description: The seccomp options to use by the containers + in this pod. Note that this field cannot be set when + spec.os.name is windows. + properties: + localhostProfile: + description: localhostProfile indicates a profile + defined in a file on the node should be used. The + profile must be preconfigured on the node to work. + Must be a descending path, relative to the kubelet's + configured seccomp profile location. Must only be + set if type is "Localhost". + type: string + type: + description: "type indicates which kind of seccomp + profile will be applied. Valid options are: \n Localhost + - a profile defined in a file on the node should + be used. RuntimeDefault - the container runtime + default profile should be used. Unconfined - no + profile should be applied." + type: string + required: + - type + type: object + supplementalGroups: + description: A list of groups applied to the first process + run in each container, in addition to the container's + primary GID, the fsGroup (if specified), and group memberships + defined in the container image for the uid of the container + process. If unspecified, no additional groups are added + to any container. Note that group memberships defined + in the container image for the uid of the container + process are still effective, even if they are not included + in this list. Note that this field cannot be set when + spec.os.name is windows. + items: + format: int64 + type: integer + type: array + sysctls: + description: Sysctls hold a list of namespaced sysctls + used for the pod. Pods with unsupported sysctls (by + the container runtime) might fail to launch. Note that + this field cannot be set when spec.os.name is windows. + items: + description: Sysctl defines a kernel parameter to be + set + properties: + name: + description: Name of a property to set + type: string + value: + description: Value of a property to set + type: string + required: + - name + - value + type: object + type: array + windowsOptions: + description: The Windows specific settings applied to + all containers. If unspecified, the options within a + container's SecurityContext will be used. If set in + both SecurityContext and PodSecurityContext, the value + specified in SecurityContext takes precedence. Note + that this field cannot be set when spec.os.name is linux. + properties: + gmsaCredentialSpec: + description: GMSACredentialSpec is where the GMSA + admission webhook (https://github.com/kubernetes-sigs/windows-gmsa) + inlines the contents of the GMSA credential spec + named by the GMSACredentialSpecName field. + type: string + gmsaCredentialSpecName: + description: GMSACredentialSpecName is the name of + the GMSA credential spec to use. + type: string + hostProcess: + description: HostProcess determines if a container + should be run as a 'Host Process' container. This + field is alpha-level and will only be honored by + components that enable the WindowsHostProcessContainers + feature flag. Setting this field without the feature + flag will result in errors when validating the Pod. + All of a Pod's containers must have the same effective + HostProcess value (it is not allowed to have a mix + of HostProcess containers and non-HostProcess containers). In + addition, if HostProcess is true then HostNetwork + must also be set to true. + type: boolean + runAsUserName: + description: The UserName in Windows to run the entrypoint + of the container process. Defaults to the user specified + in image metadata if unspecified. May also be set + in PodSecurityContext. If set in both SecurityContext + and PodSecurityContext, the value specified in SecurityContext + takes precedence. + type: string + type: object + type: object + serviceAccount: + description: 'DeprecatedServiceAccount is a depreciated alias + for ServiceAccountName. Deprecated: Use serviceAccountName + instead.' + type: string + serviceAccountName: + description: 'ServiceAccountName is the name of the ServiceAccount + to use to run this pod. More info: https://kubernetes.io/docs/tasks/configure-pod-container/configure-service-account/' + type: string + setHostnameAsFQDN: + description: If true the pod's hostname will be configured + as the pod's FQDN, rather than the leaf name (the default). + In Linux containers, this means setting the FQDN in the + hostname field of the kernel (the nodename field of struct + utsname). In Windows containers, this means setting the + registry value of hostname for the registry key HKEY_LOCAL_MACHINE\\SYSTEM\\CurrentControlSet\\Services\\Tcpip\\Parameters + to FQDN. If a pod does not have FQDN, this has no effect. + Default to false. + type: boolean + shareProcessNamespace: + description: 'Share a single process namespace between all + of the containers in a pod. When this is set containers + will be able to view and signal processes from other containers + in the same pod, and the first process in each container + will not be assigned PID 1. HostPID and ShareProcessNamespace + cannot both be set. Optional: Default to false.' + type: boolean + subdomain: + description: If specified, the fully qualified Pod hostname + will be "...svc.". If not specified, the pod will not have a domainname + at all. + type: string + terminationGracePeriodSeconds: + description: Optional duration in seconds the pod needs to + terminate gracefully. May be decreased in delete request. + Value must be non-negative integer. The value zero indicates + stop immediately via the kill signal (no opportunity to + shut down). If this value is nil, the default grace period + will be used instead. The grace period is the duration in + seconds after the processes running in the pod are sent + a termination signal and the time when the processes are + forcibly halted with a kill signal. Set this value longer + than the expected cleanup time for your process. Defaults + to 30 seconds. + format: int64 + type: integer + tolerations: + description: If specified, the pod's tolerations. + items: + description: The pod this Toleration is attached to tolerates + any taint that matches the triple using + the matching operator . + properties: + effect: + description: Effect indicates the taint effect to match. + Empty means match all taint effects. When specified, + allowed values are NoSchedule, PreferNoSchedule and + NoExecute. + type: string + key: + description: Key is the taint key that the toleration + applies to. Empty means match all taint keys. If the + key is empty, operator must be Exists; this combination + means to match all values and all keys. + type: string + operator: + description: Operator represents a key's relationship + to the value. Valid operators are Exists and Equal. + Defaults to Equal. Exists is equivalent to wildcard + for value, so that a pod can tolerate all taints of + a particular category. + type: string + tolerationSeconds: + description: TolerationSeconds represents the period + of time the toleration (which must be of effect NoExecute, + otherwise this field is ignored) tolerates the taint. + By default, it is not set, which means tolerate the + taint forever (do not evict). Zero and negative values + will be treated as 0 (evict immediately) by the system. + format: int64 + type: integer + value: + description: Value is the taint value the toleration + matches to. If the operator is Exists, the value should + be empty, otherwise just a regular string. + type: string + type: object + type: array + topologySpreadConstraints: + description: TopologySpreadConstraints describes how a group + of pods ought to spread across topology domains. Scheduler + will schedule pods in a way which abides by the constraints. + All topologySpreadConstraints are ANDed. + items: + description: TopologySpreadConstraint specifies how to spread + matching pods among the given topology. + properties: + labelSelector: + description: LabelSelector is used to find matching + pods. Pods that match this label selector are counted + to determine the number of pods in their corresponding + topology domain. + properties: + matchExpressions: + description: matchExpressions is a list of label + selector requirements. The requirements are ANDed. + items: + description: A label selector requirement is a + selector that contains values, a key, and an + operator that relates the key and values. + properties: + key: + description: key is the label key that the + selector applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string + values. If the operator is In or NotIn, + the values array must be non-empty. If the + operator is Exists or DoesNotExist, the + values array must be empty. This array is + replaced during a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} + pairs. A single {key,value} in the matchLabels + map is equivalent to an element of matchExpressions, + whose key field is "key", the operator is "In", + and the values array contains only "value". The + requirements are ANDed. + type: object + type: object + matchLabelKeys: + description: MatchLabelKeys is a set of pod label keys + to select the pods over which spreading will be calculated. + The keys are used to lookup values from the incoming + pod labels, those key-value labels are ANDed with + labelSelector to select the group of existing pods + over which spreading will be calculated for the incoming + pod. Keys that don't exist in the incoming pod labels + will be ignored. A null or empty list means only match + against labelSelector. + items: + type: string + type: array + x-kubernetes-list-type: atomic + maxSkew: + description: 'MaxSkew describes the degree to which + pods may be unevenly distributed. When `whenUnsatisfiable=DoNotSchedule`, + it is the maximum permitted difference between the + number of matching pods in the target topology and + the global minimum. The global minimum is the minimum + number of matching pods in an eligible domain or zero + if the number of eligible domains is less than MinDomains. + For example, in a 3-zone cluster, MaxSkew is set to + 1, and pods with the same labelSelector spread as + 2/2/1: In this case, the global minimum is 1. | zone1 + | zone2 | zone3 | | P P | P P | P | - if MaxSkew + is 1, incoming pod can only be scheduled to zone3 + to become 2/2/2; scheduling it onto zone1(zone2) would + make the ActualSkew(3-1) on zone1(zone2) violate MaxSkew(1). + - if MaxSkew is 2, incoming pod can be scheduled onto + any zone. When `whenUnsatisfiable=ScheduleAnyway`, + it is used to give higher precedence to topologies + that satisfy it. It''s a required field. Default value + is 1 and 0 is not allowed.' + format: int32 + type: integer + minDomains: + description: "MinDomains indicates a minimum number + of eligible domains. When the number of eligible domains + with matching topology keys is less than minDomains, + Pod Topology Spread treats \"global minimum\" as 0, + and then the calculation of Skew is performed. And + when the number of eligible domains with matching + topology keys equals or greater than minDomains, this + value has no effect on scheduling. As a result, when + the number of eligible domains is less than minDomains, + scheduler won't schedule more than maxSkew Pods to + those domains. If value is nil, the constraint behaves + as if MinDomains is equal to 1. Valid values are integers + greater than 0. When value is not nil, WhenUnsatisfiable + must be DoNotSchedule. \n For example, in a 3-zone + cluster, MaxSkew is set to 2, MinDomains is set to + 5 and pods with the same labelSelector spread as 2/2/2: + | zone1 | zone2 | zone3 | | P P | P P | P P | + The number of domains is less than 5(MinDomains), + so \"global minimum\" is treated as 0. In this situation, + new pod with the same labelSelector cannot be scheduled, + because computed skew will be 3(3 - 0) if new Pod + is scheduled to any of the three zones, it will violate + MaxSkew. \n This is a beta field and requires the + MinDomainsInPodTopologySpread feature gate to be enabled + (enabled by default)." + format: int32 + type: integer + nodeAffinityPolicy: + description: "NodeAffinityPolicy indicates how we will + treat Pod's nodeAffinity/nodeSelector when calculating + pod topology spread skew. Options are: - Honor: only + nodes matching nodeAffinity/nodeSelector are included + in the calculations. - Ignore: nodeAffinity/nodeSelector + are ignored. All nodes are included in the calculations. + \n If this value is nil, the behavior is equivalent + to the Honor policy. This is a beta-level feature + default enabled by the NodeInclusionPolicyInPodTopologySpread + feature flag." + type: string + nodeTaintsPolicy: + description: "NodeTaintsPolicy indicates how we will + treat node taints when calculating pod topology spread + skew. Options are: - Honor: nodes without taints, + along with tainted nodes for which the incoming pod + has a toleration, are included. - Ignore: node taints + are ignored. All nodes are included. \n If this value + is nil, the behavior is equivalent to the Ignore policy. + This is a beta-level feature default enabled by the + NodeInclusionPolicyInPodTopologySpread feature flag." + type: string + topologyKey: + description: TopologyKey is the key of node labels. + Nodes that have a label with this key and identical + values are considered to be in the same topology. + We consider each as a "bucket", and try + to put balanced number of pods into each bucket. We + define a domain as a particular instance of a topology. + Also, we define an eligible domain as a domain whose + nodes meet the requirements of nodeAffinityPolicy + and nodeTaintsPolicy. e.g. If TopologyKey is "kubernetes.io/hostname", + each Node is a domain of that topology. And, if TopologyKey + is "topology.kubernetes.io/zone", each zone is a domain + of that topology. It's a required field. + type: string + whenUnsatisfiable: + description: 'WhenUnsatisfiable indicates how to deal + with a pod if it doesn''t satisfy the spread constraint. + - DoNotSchedule (default) tells the scheduler not + to schedule it. - ScheduleAnyway tells the scheduler + to schedule the pod in any location, but giving higher + precedence to topologies that would help reduce the + skew. A constraint is considered "Unsatisfiable" for + an incoming pod if and only if every possible node + assignment for that pod would violate "MaxSkew" on + some topology. For example, in a 3-zone cluster, MaxSkew + is set to 1, and pods with the same labelSelector + spread as 3/1/1: | zone1 | zone2 | zone3 | | P P P + | P | P | If WhenUnsatisfiable is set to DoNotSchedule, + incoming pod can only be scheduled to zone2(zone3) + to become 3/2/1(3/1/2) as ActualSkew(2-1) on zone2(zone3) + satisfies MaxSkew(1). In other words, the cluster + can still be imbalanced, but scheduler won''t make + it *more* imbalanced. It''s a required field.' + type: string + required: + - maxSkew + - topologyKey + - whenUnsatisfiable + type: object + type: array + x-kubernetes-list-map-keys: + - topologyKey + - whenUnsatisfiable + x-kubernetes-list-type: map + volumes: + description: 'List of volumes that can be mounted by containers + belonging to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes' + items: + description: Volume represents a named volume in a pod that + may be accessed by any container in the pod. + properties: + awsElasticBlockStore: + description: 'awsElasticBlockStore represents an AWS + Disk resource that is attached to a kubelet''s host + machine and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + properties: + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + partition: + description: 'partition is the partition in the + volume that you want to mount. If omitted, the + default is to mount by volume name. Examples: + For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda + is "0" (or you can leave the property empty).' + format: int32 + type: integer + readOnly: + description: 'readOnly value true will force the + readOnly setting in VolumeMounts. More info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: boolean + volumeID: + description: 'volumeID is unique ID of the persistent + disk resource in AWS (Amazon EBS volume). More + info: https://kubernetes.io/docs/concepts/storage/volumes#awselasticblockstore' + type: string + required: + - volumeID + type: object + azureDisk: + description: azureDisk represents an Azure Data Disk + mount on the host and bind mount to the pod. + properties: + cachingMode: + description: 'cachingMode is the Host Caching mode: + None, Read Only, Read Write.' + type: string + diskName: + description: diskName is the Name of the data disk + in the blob storage + type: string + diskURI: + description: diskURI is the URI of data disk in + the blob storage + type: string + fsType: + description: fsType is Filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + kind: + description: 'kind expected values are Shared: multiple + blob disks per storage account Dedicated: single + blob disk per storage account Managed: azure + managed data disk (only in managed availability + set). defaults to shared' + type: string + readOnly: + description: readOnly Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + required: + - diskName + - diskURI + type: object + azureFile: + description: azureFile represents an Azure File Service + mount on the host and bind mount to the pod. + properties: + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretName: + description: secretName is the name of secret that + contains Azure Storage Account Name and Key + type: string + shareName: + description: shareName is the azure share Name + type: string + required: + - secretName + - shareName + type: object + cephfs: + description: cephFS represents a Ceph FS mount on the + host that shares a pod's lifetime + properties: + monitors: + description: 'monitors is Required: Monitors is + a collection of Ceph monitors More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + items: + type: string + type: array + path: + description: 'path is Optional: Used as the mounted + root, rather than the full Ceph tree, default + is /' + type: string + readOnly: + description: 'readOnly is Optional: Defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: boolean + secretFile: + description: 'secretFile is Optional: SecretFile + is the path to key ring for User, default is /etc/ceph/user.secret + More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + secretRef: + description: 'secretRef is Optional: SecretRef is + reference to the authentication secret for User, + default is empty. More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + 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 + user: + description: 'user is optional: User is the rados + user name, default is admin More info: https://examples.k8s.io/volumes/cephfs/README.md#how-to-use-it' + type: string + required: + - monitors + type: object + cinder: + description: 'cinder represents a cinder volume attached + and mounted on kubelets host machine. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Examples: "ext4", "xfs", "ntfs". + Implicitly inferred to be "ext4" if unspecified. + More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + readOnly: + description: 'readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: boolean + secretRef: + description: 'secretRef is optional: points to a + secret object containing parameters used to connect + to OpenStack.' + 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 + volumeID: + description: 'volumeID used to identify the volume + in cinder. More info: https://examples.k8s.io/mysql-cinder-pd/README.md' + type: string + required: + - volumeID + type: object + configMap: + description: configMap represents a configMap that should + populate this volume + properties: + defaultMode: + description: 'defaultMode is optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items if unspecified, each key-value + pair in the Data field of the referenced ConfigMap + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the ConfigMap, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. Must + be an octal value between 0000 and 0777 + or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON + requires decimal values for mode bits. If + not specified, the volume defaultMode will + be used. This might be in conflict with + other options that affect the file mode, + like fsGroup, and the result can be other + mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be an + absolute path. May not contain the path + element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional specify whether the ConfigMap + or its keys must be defined + type: boolean + type: object + csi: + description: csi (Container Storage Interface) represents + ephemeral storage that is handled by certain external + CSI drivers (Beta feature). + properties: + driver: + description: driver is the name of the CSI driver + that handles this volume. Consult with your admin + for the correct name as registered in the cluster. + type: string + fsType: + description: fsType to mount. Ex. "ext4", "xfs", + "ntfs". If not provided, the empty value is passed + to the associated CSI driver which will determine + the default filesystem to apply. + type: string + nodePublishSecretRef: + description: nodePublishSecretRef is a reference + to the secret object containing sensitive information + to pass to the CSI driver to complete the CSI + NodePublishVolume and NodeUnpublishVolume calls. + This field is optional, and may be empty if no + secret is required. If the secret object contains + more than one secret, all secret references are + passed. + 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 + readOnly: + description: readOnly specifies a read-only configuration + for the volume. Defaults to false (read/write). + type: boolean + volumeAttributes: + additionalProperties: + type: string + description: volumeAttributes stores driver-specific + properties that are passed to the CSI driver. + Consult your driver's documentation for supported + values. + type: object + required: + - driver + type: object + downwardAPI: + description: downwardAPI represents downward API about + the pod that should populate this volume + properties: + defaultMode: + description: 'Optional: mode bits to use on created + files by default. Must be a Optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: Items is a list of downward API volume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing the + pod field + properties: + fieldRef: + description: 'Required: Selects a field of + the pod: only annotations, labels, name + and namespace are supported.' + properties: + apiVersion: + description: Version of the schema the + FieldPath is written in terms of, defaults + to "v1". + type: string + fieldPath: + description: Path of the field to select + in the specified API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits used to + set permissions on this file, must be an + octal value between 0000 and 0777 or a decimal + value between 0 and 511. YAML accepts both + octal and decimal values, JSON requires + decimal values for mode bits. If not specified, + the volume defaultMode will be used. This + might be in conflict with other options + that affect the file mode, like fsGroup, + and the result can be other mode bits set.' + format: int32 + type: integer + path: + description: 'Required: Path is the relative + path name of the file to be created. Must + not be absolute or contain the ''..'' path. + Must be utf-8 encoded. The first item of + the relative path must not start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource of the container: + only resources limits and requests (limits.cpu, + limits.memory, requests.cpu and requests.memory) + are currently supported.' + properties: + containerName: + description: 'Container name: required + for volumes, optional for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output format + of the exposed resources, defaults to + "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + emptyDir: + description: 'emptyDir represents a temporary directory + that shares a pod''s lifetime. More info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + properties: + medium: + description: 'medium represents what type of storage + medium should back this directory. The default + is "" which means to use the node''s default medium. + Must be an empty string (default) or Memory. More + info: https://kubernetes.io/docs/concepts/storage/volumes#emptydir' + type: string + sizeLimit: + anyOf: + - type: integer + - type: string + description: 'sizeLimit is the total amount of local + storage required for this EmptyDir volume. The + size limit is also applicable for memory medium. + The maximum usage on memory medium EmptyDir would + be the minimum value between the SizeLimit specified + here and the sum of memory limits of all containers + in a pod. The default is nil which means that + the limit is undefined. More info: http://kubernetes.io/docs/user-guide/volumes#emptydir' + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + type: object + ephemeral: + description: "ephemeral represents a volume that is + handled by a cluster storage driver. The volume's + lifecycle is tied to the pod that defines it - it + will be created before the pod starts, and deleted + when the pod is removed. \n Use this if: a) the volume + is only needed while the pod runs, b) features of + normal volumes like restoring from snapshot or capacity + tracking are needed, c) the storage driver is specified + through a storage class, and d) the storage driver + supports dynamic volume provisioning through a PersistentVolumeClaim + (see EphemeralVolumeSource for more information on + the connection between this volume type and PersistentVolumeClaim). + \n Use PersistentVolumeClaim or one of the vendor-specific + APIs for volumes that persist for longer than the + lifecycle of an individual pod. \n Use CSI for light-weight + local ephemeral volumes if the CSI driver is meant + to be used that way - see the documentation of the + driver for more information. \n A pod can use both + types of ephemeral volumes and persistent volumes + at the same time." + properties: + volumeClaimTemplate: + description: "Will be used to create a stand-alone + PVC to provision the volume. The pod in which + this EphemeralVolumeSource is embedded will be + the owner of the PVC, i.e. the PVC will be deleted + together with the pod. The name of the PVC will + be `-` where `` + is the name from the `PodSpec.Volumes` array entry. + Pod validation will reject the pod if the concatenated + name is not valid for a PVC (for example, too + long). \n An existing PVC with that name that + is not owned by the pod will *not* be used for + the pod to avoid using an unrelated volume by + mistake. Starting the pod is then blocked until + the unrelated PVC is removed. If such a pre-created + PVC is meant to be used by the pod, the PVC has + to updated with an owner reference to the pod + once the pod exists. Normally this should not + be necessary, but it may be useful when manually + reconstructing a broken cluster. \n This field + is read-only and no changes will be made by Kubernetes + to the PVC after it has been created. \n Required, + must not be nil." + properties: + metadata: + description: May contain labels and annotations + that will be copied into the PVC when creating + it. No other fields are allowed and will be + rejected during validation. + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: The specification for the PersistentVolumeClaim. + The entire content is copied unchanged into + the PVC that gets created from this template. + The same fields as in a PersistentVolumeClaim + are also valid here. + properties: + accessModes: + description: 'accessModes contains the desired + access modes the volume should have. More + info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'dataSource field can be used + to specify either: * An existing VolumeSnapshot + object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) + If the provisioner or an external controller + can support the specified data source, + it will create a new volume based on the + contents of the specified data source. + When the AnyVolumeDataSource feature gate + is enabled, dataSource contents will be + copied to dataSourceRef, and dataSourceRef + contents will be copied to dataSource + when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef + will not be copied to dataSource.' + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. If + APIGroup is not specified, the specified + Kind must be in the core API group. + For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + required: + - kind + - name + type: object + dataSourceRef: + description: 'dataSourceRef specifies the + object from which to populate the volume + with data, if a non-empty volume is desired. + This may be any object from a non-empty + API group (non core object) or a PersistentVolumeClaim + object. When this field is specified, + volume binding will only succeed if the + type of the specified object matches some + installed volume populator or dynamic + provisioner. This field will replace the + functionality of the dataSource field + and as such if both fields are non-empty, + they must have the same value. For backwards + compatibility, when namespace isn''t specified + in dataSourceRef, both fields (dataSource + and dataSourceRef) will be set to the + same value automatically if one of them + is empty and the other is non-empty. When + namespace is specified in dataSourceRef, + dataSource isn''t set to the same value + and must be empty. There are three important + differences between dataSource and dataSourceRef: + * While dataSource only allows two specific + types of objects, dataSourceRef allows + any non-core object, as well as PersistentVolumeClaim + objects. * While dataSource ignores disallowed + values (dropping them), dataSourceRef + preserves all values, and generates an + error if a disallowed value is specified. + * While dataSource only allows local objects, + dataSourceRef allows objects in any namespaces. + (Beta) Using this field requires the AnyVolumeDataSource + feature gate to be enabled. (Alpha) Using + the namespace field of dataSourceRef requires + the CrossNamespaceVolumeDataSource feature + gate to be enabled.' + properties: + apiGroup: + description: APIGroup is the group for + the resource being referenced. If + APIGroup is not specified, the specified + Kind must be in the core API group. + For any other third-party types, APIGroup + is required. + type: string + kind: + description: Kind is the type of resource + being referenced + type: string + name: + description: Name is the name of resource + being referenced + type: string + namespace: + description: Namespace is the namespace + of resource being referenced Note + that when a namespace is specified, + a gateway.networking.k8s.io/ReferenceGrant + object is required in the referent + namespace to allow that namespace's + owner to accept the reference. See + the ReferenceGrant documentation for + details. (Alpha) This field requires + the CrossNamespaceVolumeDataSource + feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: 'resources represents the minimum + resources the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to + specify resource requirements that are + lower than previous value but must still + be higher than capacity recorded in the + status field of the claim. More info: + https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + claims: + description: "Claims lists the names + of resources, defined in spec.resourceClaims, + that are used by this container. \n + This is an alpha field and requires + enabling the DynamicResourceAllocation + feature gate. \n This field is immutable." + items: + description: ResourceClaim references + one entry in PodSpec.ResourceClaims. + properties: + name: + description: Name must match the + name of one entry in pod.spec.resourceClaims + of the Pod where this field + is used. It makes that resource + available inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum + amount of compute resources allowed. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the + minimum amount of compute resources + required. If Requests is omitted for + a container, it defaults to Limits + if that is explicitly specified, otherwise + to an implementation-defined value. + More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: selector is a label query over + volumes to consider for binding. + properties: + matchExpressions: + description: matchExpressions is a list + of label selector requirements. The + requirements are ANDed. + items: + description: A label selector requirement + is a selector that contains values, + a key, and an operator that relates + the key and values. + properties: + key: + description: key is the label + key that the selector applies + to. + type: string + operator: + description: operator represents + a key's relationship to a set + of values. Valid operators are + In, NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array + of string values. If the operator + is In or NotIn, the values array + must be non-empty. If the operator + is Exists or DoesNotExist, the + values array must be empty. + This array is replaced during + a strategic merge patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of + {key,value} pairs. A single {key,value} + in the matchLabels map is equivalent + to an element of matchExpressions, + whose key field is "key", the operator + is "In", and the values array contains + only "value". The requirements are + ANDed. + type: object + type: object + storageClassName: + description: 'storageClassName is the name + of the StorageClass required by the claim. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeMode: + description: volumeMode defines what type + of volume is required by the claim. Value + of Filesystem is implied when not included + in claim spec. + type: string + volumeName: + description: volumeName is the binding reference + to the PersistentVolume backing this claim. + type: string + type: object + required: + - spec + type: object + type: object + fc: + description: fc represents a Fibre Channel resource + that is attached to a kubelet's host machine and then + exposed to the pod. + properties: + fsType: + description: 'fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. TODO: how + do we prevent errors in the filesystem from compromising + the machine' + type: string + lun: + description: 'lun is Optional: FC target lun number' + format: int32 + type: integer + readOnly: + description: 'readOnly is Optional: Defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts.' + type: boolean + targetWWNs: + description: 'targetWWNs is Optional: FC target + worldwide names (WWNs)' + items: + type: string + type: array + wwids: + description: 'wwids Optional: FC volume world wide + identifiers (wwids) Either wwids or combination + of targetWWNs and lun must be set, but not both + simultaneously.' + items: + type: string + type: array + type: object + flexVolume: + description: flexVolume represents a generic volume + resource that is provisioned/attached using an exec + based plugin. + properties: + driver: + description: driver is the name of the driver to + use for this volume. + type: string + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". The + default filesystem depends on FlexVolume script. + type: string + options: + additionalProperties: + type: string + description: 'options is Optional: this field holds + extra command options if any.' + type: object + readOnly: + description: 'readOnly is Optional: defaults to + false (read/write). ReadOnly here will force the + ReadOnly setting in VolumeMounts.' + type: boolean + secretRef: + description: 'secretRef is Optional: secretRef is + reference to the secret object containing sensitive + information to pass to the plugin scripts. This + may be empty if no secret object is specified. + If the secret object contains more than one secret, + all secrets are passed to the plugin scripts.' + 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 + required: + - driver + type: object + flocker: + description: flocker represents a Flocker volume attached + to a kubelet's host machine. This depends on the Flocker + control service being running + properties: + datasetName: + description: datasetName is Name of the dataset + stored as metadata -> name on the dataset for + Flocker should be considered as deprecated + type: string + datasetUUID: + description: datasetUUID is the UUID of the dataset. + This is unique identifier of a Flocker dataset + type: string + type: object + gcePersistentDisk: + description: 'gcePersistentDisk represents a GCE Disk + resource that is attached to a kubelet''s host machine + and then exposed to the pod. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + properties: + fsType: + description: 'fsType is filesystem type of the volume + that you want to mount. Tip: Ensure that the filesystem + type is supported by the host operating system. + Examples: "ext4", "xfs", "ntfs". Implicitly inferred + to be "ext4" if unspecified. More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + partition: + description: 'partition is the partition in the + volume that you want to mount. If omitted, the + default is to mount by volume name. Examples: + For volume /dev/sda1, you specify the partition + as "1". Similarly, the volume partition for /dev/sda + is "0" (or you can leave the property empty). + More info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + format: int32 + type: integer + pdName: + description: 'pdName is unique name of the PD resource + in GCE. Used to identify the disk in GCE. More + info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. More + info: https://kubernetes.io/docs/concepts/storage/volumes#gcepersistentdisk' + type: boolean + required: + - pdName + type: object + gitRepo: + description: 'gitRepo represents a git repository at + a particular revision. DEPRECATED: GitRepo is deprecated. + To provision a container with a git repo, mount an + EmptyDir into an InitContainer that clones the repo + using git, then mount the EmptyDir into the Pod''s + container.' + properties: + directory: + description: directory is the target directory name. + Must not contain or start with '..'. If '.' is + supplied, the volume directory will be the git + repository. Otherwise, if specified, the volume + will contain the git repository in the subdirectory + with the given name. + type: string + repository: + description: repository is the URL + type: string + revision: + description: revision is the commit hash for the + specified revision. + type: string + required: + - repository + type: object + glusterfs: + description: 'glusterfs represents a Glusterfs mount + on the host that shares a pod''s lifetime. More info: + https://examples.k8s.io/volumes/glusterfs/README.md' + properties: + endpoints: + description: 'endpoints is the endpoint name that + details Glusterfs topology. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + path: + description: 'path is the Glusterfs volume path. + More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: string + readOnly: + description: 'readOnly here will force the Glusterfs + volume to be mounted with read-only permissions. + Defaults to false. More info: https://examples.k8s.io/volumes/glusterfs/README.md#create-a-pod' + type: boolean + required: + - endpoints + - path + type: object + hostPath: + description: 'hostPath represents a pre-existing file + or directory on the host machine that is directly + exposed to the container. This is generally used for + system agents or other privileged things that are + allowed to see the host machine. Most containers will + NOT need this. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath + --- TODO(jonesdl) We need to restrict who can use + host directory mounts and who can/can not mount host + directories as read/write.' + properties: + path: + description: 'path of the directory on the host. + If the path is a symlink, it will follow the link + to the real path. More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + type: + description: 'type for HostPath Volume Defaults + to "" More info: https://kubernetes.io/docs/concepts/storage/volumes#hostpath' + type: string + required: + - path + type: object + iscsi: + description: 'iscsi represents an ISCSI Disk resource + that is attached to a kubelet''s host machine and + then exposed to the pod. More info: https://examples.k8s.io/volumes/iscsi/README.md' + properties: + chapAuthDiscovery: + description: chapAuthDiscovery defines whether support + iSCSI Discovery CHAP authentication + type: boolean + chapAuthSession: + description: chapAuthSession defines whether support + iSCSI Session CHAP authentication + type: boolean + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#iscsi + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + initiatorName: + description: initiatorName is the custom iSCSI Initiator + Name. If initiatorName is specified with iscsiInterface + simultaneously, new iSCSI interface : will be created for the connection. + type: string + iqn: + description: iqn is the target iSCSI Qualified Name. + type: string + iscsiInterface: + description: iscsiInterface is the interface Name + that uses an iSCSI transport. Defaults to 'default' + (tcp). + type: string + lun: + description: lun represents iSCSI Target Lun number. + format: int32 + type: integer + portals: + description: portals is the iSCSI Target Portal + List. The portal is either an IP or ip_addr:port + if the port is other than default (typically TCP + ports 860 and 3260). + items: + type: string + type: array + readOnly: + description: readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. + type: boolean + secretRef: + description: secretRef is the CHAP Secret for iSCSI + target and initiator authentication + 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 + targetPortal: + description: targetPortal is iSCSI Target Portal. + The Portal is either an IP or ip_addr:port if + the port is other than default (typically TCP + ports 860 and 3260). + type: string + required: + - iqn + - lun + - targetPortal + type: object + name: + description: 'name of the volume. Must be a DNS_LABEL + and unique within the pod. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names' + type: string + nfs: + description: 'nfs represents an NFS mount on the host + that shares a pod''s lifetime More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + properties: + path: + description: 'path that is exported by the NFS server. + More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + readOnly: + description: 'readOnly here will force the NFS export + to be mounted with read-only permissions. Defaults + to false. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: boolean + server: + description: 'server is the hostname or IP address + of the NFS server. More info: https://kubernetes.io/docs/concepts/storage/volumes#nfs' + type: string + required: + - path + - server + type: object + persistentVolumeClaim: + description: 'persistentVolumeClaimVolumeSource represents + a reference to a PersistentVolumeClaim in the same + namespace. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + claimName: + description: 'claimName is the name of a PersistentVolumeClaim + in the same namespace as the pod using this volume. + More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + type: string + readOnly: + description: readOnly Will force the ReadOnly setting + in VolumeMounts. Default false. + type: boolean + required: + - claimName + type: object + photonPersistentDisk: + description: photonPersistentDisk represents a PhotonController + persistent disk attached and mounted on kubelets host + machine + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + pdID: + description: pdID is the ID that identifies Photon + Controller persistent disk + type: string + required: + - pdID + type: object + portworxVolume: + description: portworxVolume represents a portworx volume + attached and mounted on kubelets host machine + properties: + fsType: + description: fSType represents the filesystem type + to mount Must be a filesystem type supported by + the host operating system. Ex. "ext4", "xfs". + Implicitly inferred to be "ext4" if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + volumeID: + description: volumeID uniquely identifies a Portworx + volume + type: string + required: + - volumeID + type: object + projected: + description: projected items for all in one resources + secrets, configmaps, and downward API + properties: + defaultMode: + description: defaultMode are the mode bits used + to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Directories within the path + are not affected by this setting. This might be + in conflict with other options that affect the + file mode, like fsGroup, and the result can be + other mode bits set. + format: int32 + type: integer + sources: + description: sources is the list of volume projections + items: + description: Projection that may be projected + along with other supported volume types + properties: + configMap: + description: configMap information about the + configMap data to project + properties: + items: + description: items if unspecified, each + key-value pair in the Data field of + the referenced ConfigMap will be projected + into the volume as a file whose name + is the key and content is the value. + If specified, the listed keys will be + projected into the specified paths, + and unlisted keys will not be present. + If a key is specified which is not present + in the ConfigMap, the volume setup will + error unless it is marked optional. + Paths must be relative and may not contain + the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file. Must be an octal + value between 0000 and 0777 or + a decimal value between 0 and + 511. YAML accepts both octal and + decimal values, JSON requires + decimal values for mode bits. + If not specified, the volume defaultMode + will be used. This might be in + conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: path is the relative + path of the file to map the key + to. May not be an absolute path. + May not contain the path element + '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional specify whether + the ConfigMap or its keys must be defined + type: boolean + type: object + downwardAPI: + description: downwardAPI information about + the downwardAPI data to project + properties: + items: + description: Items is a list of DownwardAPIVolume + file + items: + description: DownwardAPIVolumeFile represents + information to create the file containing + the pod field + properties: + fieldRef: + description: 'Required: Selects + a field of the pod: only annotations, + labels, name and namespace are + supported.' + properties: + apiVersion: + description: Version of the + schema the FieldPath is written + in terms of, defaults to "v1". + type: string + fieldPath: + description: Path of the field + to select in the specified + API version. + type: string + required: + - fieldPath + type: object + mode: + description: 'Optional: mode bits + used to set permissions on this + file, must be an octal value between + 0000 and 0777 or a decimal value + between 0 and 511. YAML accepts + both octal and decimal values, + JSON requires decimal values for + mode bits. If not specified, the + volume defaultMode will be used. + This might be in conflict with + other options that affect the + file mode, like fsGroup, and the + result can be other mode bits + set.' + format: int32 + type: integer + path: + description: 'Required: Path is the + relative path name of the file + to be created. Must not be absolute + or contain the ''..'' path. Must + be utf-8 encoded. The first item + of the relative path must not + start with ''..''' + type: string + resourceFieldRef: + description: 'Selects a resource + of the container: only resources + limits and requests (limits.cpu, + limits.memory, requests.cpu and + requests.memory) are currently + supported.' + properties: + containerName: + description: 'Container name: + required for volumes, optional + for env vars' + type: string + divisor: + anyOf: + - type: integer + - type: string + description: Specifies the output + format of the exposed resources, + defaults to "1" + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + resource: + description: 'Required: resource + to select' + type: string + required: + - resource + type: object + required: + - path + type: object + type: array + type: object + secret: + description: secret information about the + secret data to project + properties: + items: + description: items if unspecified, each + key-value pair in the Data field of + the referenced Secret will be projected + into the volume as a file whose name + is the key and content is the value. + If specified, the listed keys will be + projected into the specified paths, + and unlisted keys will not be present. + If a key is specified which is not present + in the Secret, the volume setup will + error unless it is marked optional. + Paths must be relative and may not contain + the '..' path or start with '..'. + items: + description: Maps a string key to a + path within a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: + mode bits used to set permissions + on this file. Must be an octal + value between 0000 and 0777 or + a decimal value between 0 and + 511. YAML accepts both octal and + decimal values, JSON requires + decimal values for mode bits. + If not specified, the volume defaultMode + will be used. This might be in + conflict with other options that + affect the file mode, like fsGroup, + and the result can be other mode + bits set.' + format: int32 + type: integer + path: + description: path is the relative + path of the file to map the key + to. May not be an absolute path. + May not contain the path element + '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + 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 + optional: + description: optional field specify whether + the Secret or its key must be defined + type: boolean + type: object + serviceAccountToken: + description: serviceAccountToken is information + about the serviceAccountToken data to project + properties: + audience: + description: audience is the intended + audience of the token. A recipient of + a token must identify itself with an + identifier specified in the audience + of the token, and otherwise should reject + the token. The audience defaults to + the identifier of the apiserver. + type: string + expirationSeconds: + description: expirationSeconds is the + requested duration of validity of the + service account token. As the token + approaches expiration, the kubelet volume + plugin will proactively rotate the service + account token. The kubelet will start + trying to rotate the token if the token + is older than 80 percent of its time + to live or if the token is older than + 24 hours.Defaults to 1 hour and must + be at least 10 minutes. + format: int64 + type: integer + path: + description: path is the path relative + to the mount point of the file to project + the token into. + type: string + required: + - path + type: object + type: object + type: array + type: object + quobyte: + description: quobyte represents a Quobyte mount on the + host that shares a pod's lifetime + properties: + group: + description: group to map volume access to Default + is no group + type: string + readOnly: + description: readOnly here will force the Quobyte + volume to be mounted with read-only permissions. + Defaults to false. + type: boolean + registry: + description: registry represents a single or multiple + Quobyte Registry services specified as a string + as host:port pair (multiple entries are separated + with commas) which acts as the central registry + for volumes + type: string + tenant: + description: tenant owning the given Quobyte volume + in the Backend Used with dynamically provisioned + Quobyte volumes, value is set by the plugin + type: string + user: + description: user to map volume access to Defaults + to serivceaccount user + type: string + volume: + description: volume is a string that references + an already created Quobyte volume by name. + type: string + required: + - registry + - volume + type: object + rbd: + description: 'rbd represents a Rados Block Device mount + on the host that shares a pod''s lifetime. More info: + https://examples.k8s.io/volumes/rbd/README.md' + properties: + fsType: + description: 'fsType is the filesystem type of the + volume that you want to mount. Tip: Ensure that + the filesystem type is supported by the host operating + system. Examples: "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. More info: + https://kubernetes.io/docs/concepts/storage/volumes#rbd + TODO: how do we prevent errors in the filesystem + from compromising the machine' + type: string + image: + description: 'image is the rados image name. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + keyring: + description: 'keyring is the path to key ring for + RBDUser. Default is /etc/ceph/keyring. More info: + https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + monitors: + description: 'monitors is a collection of Ceph monitors. + More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + items: + type: string + type: array + pool: + description: 'pool is the rados pool name. Default + is rbd. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + readOnly: + description: 'readOnly here will force the ReadOnly + setting in VolumeMounts. Defaults to false. More + info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: boolean + secretRef: + description: 'secretRef is name of the authentication + secret for RBDUser. If provided overrides keyring. + Default is nil. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + 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 + user: + description: 'user is the rados user name. Default + is admin. More info: https://examples.k8s.io/volumes/rbd/README.md#how-to-use-it' + type: string + required: + - image + - monitors + type: object + scaleIO: + description: scaleIO represents a ScaleIO persistent + volume attached and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Default + is "xfs". + type: string + gateway: + description: gateway is the host address of the + ScaleIO API Gateway. + type: string + protectionDomain: + description: protectionDomain is the name of the + ScaleIO Protection Domain for the configured storage. + type: string + readOnly: + description: readOnly Defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretRef: + description: secretRef references to the secret + for ScaleIO user and other sensitive information. + If this is not provided, Login operation will + fail. + 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 + sslEnabled: + description: sslEnabled Flag enable/disable SSL + communication with Gateway, default false + type: boolean + storageMode: + description: storageMode indicates whether the storage + for a volume should be ThickProvisioned or ThinProvisioned. + Default is ThinProvisioned. + type: string + storagePool: + description: storagePool is the ScaleIO Storage + Pool associated with the protection domain. + type: string + system: + description: system is the name of the storage system + as configured in ScaleIO. + type: string + volumeName: + description: volumeName is the name of a volume + already created in the ScaleIO system that is + associated with this volume source. + type: string + required: + - gateway + - secretRef + - system + type: object + secret: + description: 'secret represents a secret that should + populate this volume. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + properties: + defaultMode: + description: 'defaultMode is Optional: mode bits + used to set permissions on created files by default. + Must be an octal value between 0000 and 0777 or + a decimal value between 0 and 511. YAML accepts + both octal and decimal values, JSON requires decimal + values for mode bits. Defaults to 0644. Directories + within the path are not affected by this setting. + This might be in conflict with other options that + affect the file mode, like fsGroup, and the result + can be other mode bits set.' + format: int32 + type: integer + items: + description: items If unspecified, each key-value + pair in the Data field of the referenced Secret + will be projected into the volume as a file whose + name is the key and content is the value. If specified, + the listed keys will be projected into the specified + paths, and unlisted keys will not be present. + If a key is specified which is not present in + the Secret, the volume setup will error unless + it is marked optional. Paths must be relative + and may not contain the '..' path or start with + '..'. + items: + description: Maps a string key to a path within + a volume. + properties: + key: + description: key is the key to project. + type: string + mode: + description: 'mode is Optional: mode bits + used to set permissions on this file. Must + be an octal value between 0000 and 0777 + or a decimal value between 0 and 511. YAML + accepts both octal and decimal values, JSON + requires decimal values for mode bits. If + not specified, the volume defaultMode will + be used. This might be in conflict with + other options that affect the file mode, + like fsGroup, and the result can be other + mode bits set.' + format: int32 + type: integer + path: + description: path is the relative path of + the file to map the key to. May not be an + absolute path. May not contain the path + element '..'. May not start with the string + '..'. + type: string + required: + - key + - path + type: object + type: array + optional: + description: optional field specify whether the + Secret or its keys must be defined + type: boolean + secretName: + description: 'secretName is the name of the secret + in the pod''s namespace to use. More info: https://kubernetes.io/docs/concepts/storage/volumes#secret' + type: string + type: object + storageos: + description: storageOS represents a StorageOS volume + attached and mounted on Kubernetes nodes. + properties: + fsType: + description: fsType is the filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + readOnly: + description: readOnly defaults to false (read/write). + ReadOnly here will force the ReadOnly setting + in VolumeMounts. + type: boolean + secretRef: + description: secretRef specifies the secret to use + for obtaining the StorageOS API credentials. If + not specified, default values will be attempted. + 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 + volumeName: + description: volumeName is the human-readable name + of the StorageOS volume. Volume names are only + unique within a namespace. + type: string + volumeNamespace: + description: volumeNamespace specifies the scope + of the volume within StorageOS. If no namespace + is specified then the Pod's namespace will be + used. This allows the Kubernetes name scoping + to be mirrored within StorageOS for tighter integration. + Set VolumeName to any name to override the default + behaviour. Set to "default" if you are not using + namespaces within StorageOS. Namespaces that do + not pre-exist within StorageOS will be created. + type: string + type: object + vsphereVolume: + description: vsphereVolume represents a vSphere volume + attached and mounted on kubelets host machine + properties: + fsType: + description: fsType is filesystem type to mount. + Must be a filesystem type supported by the host + operating system. Ex. "ext4", "xfs", "ntfs". Implicitly + inferred to be "ext4" if unspecified. + type: string + storagePolicyID: + description: storagePolicyID is the storage Policy + Based Management (SPBM) profile ID associated + with the StoragePolicyName. + type: string + storagePolicyName: + description: storagePolicyName is the storage Policy + Based Management (SPBM) profile name. + type: string + volumePath: + description: volumePath is the path that identifies + vSphere volume vmdk + type: string + required: + - volumePath + type: object + required: + - name + type: object + type: array + required: + - containers + type: object + type: object + updateStrategy: + default: Serial + description: 'UpdateStrategy, Pods update strategy. serial: update + Pods one by one that guarantee minimum component unavailable time. + Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) + -> Follower(with AccessMode=readWrite) -> Leader bestEffortParallel: + update Pods in parallel that guarantee minimum component un-writable + time. Learner, Follower(minority) in parallel -> Follower(majority) + -> Leader, keep majority online all the time. parallel: force parallel' + enum: + - Serial + - BestEffortParallel + - Parallel + type: string + volumeClaimTemplates: + description: volumeClaimTemplates is a list of claims that pods are + allowed to reference. The ConsensusSet controller is responsible + for mapping network identities to claims in a way that maintains + the identity of a pod. Every claim in this list must have at least + one matching (by name) volumeMount in one container in the template. + A claim in this list takes precedence over any volumes in the template, + with the same name. + items: + description: PersistentVolumeClaim is a user's request for and claim + to a persistent volume + 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: + description: 'Standard object''s metadata. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata' + properties: + annotations: + additionalProperties: + type: string + type: object + finalizers: + items: + type: string + type: array + labels: + additionalProperties: + type: string + type: object + name: + type: string + namespace: + type: string + type: object + spec: + description: 'spec defines the desired characteristics of a + volume requested by a pod author. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + accessModes: + description: 'accessModes contains the desired access modes + the volume should have. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + dataSource: + description: 'dataSource field can be used to specify either: + * An existing VolumeSnapshot object (snapshot.storage.k8s.io/VolumeSnapshot) + * An existing PVC (PersistentVolumeClaim) If the provisioner + or an external controller can support the specified data + source, it will create a new volume based on the contents + of the specified data source. When the AnyVolumeDataSource + feature gate is enabled, dataSource contents will be copied + to dataSourceRef, and dataSourceRef contents will be copied + to dataSource when dataSourceRef.namespace is not specified. + If the namespace is specified, then dataSourceRef will + not be copied to dataSource.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, the + specified Kind must be in the core API group. For + any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + required: + - kind + - name + type: object + dataSourceRef: + description: 'dataSourceRef specifies the object from which + to populate the volume with data, if a non-empty volume + is desired. This may be any object from a non-empty API + group (non core object) or a PersistentVolumeClaim object. + When this field is specified, volume binding will only + succeed if the type of the specified object matches some + installed volume populator or dynamic provisioner. This + field will replace the functionality of the dataSource + field and as such if both fields are non-empty, they must + have the same value. For backwards compatibility, when + namespace isn''t specified in dataSourceRef, both fields + (dataSource and dataSourceRef) will be set to the same + value automatically if one of them is empty and the other + is non-empty. When namespace is specified in dataSourceRef, + dataSource isn''t set to the same value and must be empty. + There are three important differences between dataSource + and dataSourceRef: * While dataSource only allows two + specific types of objects, dataSourceRef allows any non-core + object, as well as PersistentVolumeClaim objects. * While + dataSource ignores disallowed values (dropping them), + dataSourceRef preserves all values, and generates an error + if a disallowed value is specified. * While dataSource + only allows local objects, dataSourceRef allows objects + in any namespaces. (Beta) Using this field requires the + AnyVolumeDataSource feature gate to be enabled. (Alpha) + Using the namespace field of dataSourceRef requires the + CrossNamespaceVolumeDataSource feature gate to be enabled.' + properties: + apiGroup: + description: APIGroup is the group for the resource + being referenced. If APIGroup is not specified, the + specified Kind must be in the core API group. For + any other third-party types, APIGroup is required. + type: string + kind: + description: Kind is the type of resource being referenced + type: string + name: + description: Name is the name of resource being referenced + type: string + namespace: + description: Namespace is the namespace of resource + being referenced Note that when a namespace is specified, + a gateway.networking.k8s.io/ReferenceGrant object + is required in the referent namespace to allow that + namespace's owner to accept the reference. See the + ReferenceGrant documentation for details. (Alpha) + This field requires the CrossNamespaceVolumeDataSource + feature gate to be enabled. + type: string + required: + - kind + - name + type: object + resources: + description: 'resources represents the minimum resources + the volume should have. If RecoverVolumeExpansionFailure + feature is enabled users are allowed to specify resource + requirements that are lower than previous value but must + still be higher than capacity recorded in the status field + of the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources' + properties: + claims: + description: "Claims lists the names of resources, defined + in spec.resourceClaims, that are used by this container. + \n This is an alpha field and requires enabling the + DynamicResourceAllocation feature gate. \n This field + is immutable." + items: + description: ResourceClaim references one entry in + PodSpec.ResourceClaims. + properties: + name: + description: Name must match the name of one entry + in pod.spec.resourceClaims of the Pod where + this field is used. It makes that resource available + inside a container. + type: string + required: + - name + type: object + type: array + x-kubernetes-list-map-keys: + - name + x-kubernetes-list-type: map + limits: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Limits describes the maximum amount of + compute resources allowed. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + requests: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: 'Requests describes the minimum amount + of compute resources required. If Requests is omitted + for a container, it defaults to Limits if that is + explicitly specified, otherwise to an implementation-defined + value. More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/' + type: object + type: object + selector: + description: selector is a label query over volumes to consider + for binding. + properties: + matchExpressions: + description: matchExpressions is a list of label selector + requirements. The requirements are ANDed. + items: + description: A label selector requirement is a selector + that contains values, a key, and an operator that + relates the key and values. + properties: + key: + description: key is the label key that the selector + applies to. + type: string + operator: + description: operator represents a key's relationship + to a set of values. Valid operators are In, + NotIn, Exists and DoesNotExist. + type: string + values: + description: values is an array of string values. + If the operator is In or NotIn, the values array + must be non-empty. If the operator is Exists + or DoesNotExist, the values array must be empty. + This array is replaced during a strategic merge + patch. + items: + type: string + type: array + required: + - key + - operator + type: object + type: array + matchLabels: + additionalProperties: + type: string + description: matchLabels is a map of {key,value} pairs. + A single {key,value} in the matchLabels map is equivalent + to an element of matchExpressions, whose key field + is "key", the operator is "In", and the values array + contains only "value". The requirements are ANDed. + type: object + type: object + storageClassName: + description: 'storageClassName is the name of the StorageClass + required by the claim. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1' + type: string + volumeMode: + description: volumeMode defines what type of volume is required + by the claim. Value of Filesystem is implied when not + included in claim spec. + type: string + volumeName: + description: volumeName is the binding reference to the + PersistentVolume backing this claim. + type: string + type: object + status: + description: 'status represents the current information/status + of a persistent volume claim. Read-only. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#persistentvolumeclaims' + properties: + accessModes: + description: 'accessModes contains the actual access modes + the volume backing the PVC has. More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1' + items: + type: string + type: array + allocatedResources: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: allocatedResources is the storage resource + within AllocatedResources tracks the capacity allocated + to a PVC. It may be larger than the actual capacity when + a volume expansion operation is requested. For storage + quota, the larger value from allocatedResources and PVC.spec.resources + is used. If allocatedResources is not set, PVC.spec.resources + alone is used for quota calculation. If a volume expansion + capacity request is lowered, allocatedResources is only + lowered if there are no expansion operations in progress + and if the actual volume capacity is equal or lower than + the requested capacity. This is an alpha field and requires + enabling RecoverVolumeExpansionFailure feature. + type: object + capacity: + additionalProperties: + anyOf: + - type: integer + - type: string + pattern: ^(\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))(([KMGTPE]i)|[numkMGTPE]|([eE](\+|-)?(([0-9]+(\.[0-9]*)?)|(\.[0-9]+))))?$ + x-kubernetes-int-or-string: true + description: capacity represents the actual resources of + the underlying volume. + type: object + conditions: + description: conditions is the current Condition of persistent + volume claim. If underlying persistent volume is being + resized then the Condition will be set to 'ResizeStarted'. + items: + description: PersistentVolumeClaimCondition contails details + about state of pvc + properties: + lastProbeTime: + description: lastProbeTime is the time we probed the + condition. + format: date-time + type: string + lastTransitionTime: + description: lastTransitionTime is the time the condition + transitioned from one status to another. + format: date-time + type: string + message: + description: message is the human-readable message + indicating details about last transition. + type: string + reason: + description: reason is a unique, this should be a + short, machine understandable string that gives + the reason for condition's last transition. If it + reports "ResizeStarted" that means the underlying + persistent volume is being resized. + type: string + status: + type: string + type: + description: PersistentVolumeClaimConditionType is + a valid value of PersistentVolumeClaimCondition.Type + type: string + required: + - status + - type + type: object + type: array + phase: + description: phase represents the current phase of PersistentVolumeClaim. + type: string + resizeStatus: + description: resizeStatus stores status of resize operation. + ResizeStatus is not set by default but when expansion + is complete resizeStatus is set to empty string by resize + controller or kubelet. This is an alpha field and requires + enabling RecoverVolumeExpansionFailure feature. + type: string + type: object + type: object + type: array + required: + - roleObservation + - roles + - service + - template + type: object + status: + description: ConsensusSetStatus defines the observed state of ConsensusSet + properties: + availableReplicas: + description: Total number of available pods (ready for at least minReadySeconds) + targeted by this statefulset. + format: int32 + type: integer + collisionCount: + description: collisionCount is the count of hash collisions for the + StatefulSet. The StatefulSet controller uses this field as a collision + avoidance mechanism when it needs to create the name for the newest + ControllerRevision. + format: int32 + type: integer + conditions: + description: Represents the latest available observations of a statefulset's + current state. + items: + description: StatefulSetCondition describes the state of a statefulset + at a certain point. + properties: + lastTransitionTime: + description: Last time the condition transitioned from one status + to another. + format: date-time + type: string + message: + description: A human readable message indicating details about + the transition. + type: string + reason: + description: The reason for the condition's last transition. + type: string + status: + description: Status of the condition, one of True, False, Unknown. + type: string + type: + description: Type of statefulset condition. + type: string + required: + - status + - type + type: object + type: array + currentReplicas: + description: currentReplicas is the number of Pods created by the + StatefulSet controller from the StatefulSet version indicated by + currentRevision. + format: int32 + type: integer + currentRevision: + description: currentRevision, if not empty, indicates the version + of the StatefulSet used to generate Pods in the sequence [0,currentReplicas). + type: string + initReplicas: + description: InitReplicas is the number of pods(members) when cluster + first initialized it's set to spec.Replicas at object creation time + and never changes + format: int32 + type: integer + membersStatus: + description: members' status. + items: + properties: + podName: + default: Unknown + description: PodName pod name. + type: string + role: + properties: + accessMode: + default: ReadWrite + description: AccessMode, what service this member capable. + enum: + - None + - Readonly + - ReadWrite + type: string + canVote: + default: true + description: CanVote, whether this member has voting rights + type: boolean + isLeader: + default: false + description: IsLeader, whether this member is the leader + type: boolean + name: + default: leader + description: Name, role name. + type: string + required: + - accessMode + - name + type: object + required: + - podName + - role + type: object + type: array + observedGeneration: + description: observedGeneration is the most recent generation observed + for this StatefulSet. It corresponds to the StatefulSet's generation, + which is updated on mutation by the API Server. + format: int64 + type: integer + readyInitReplicas: + description: ReadyInitReplicas is the number of pods(members) already + in MembersStatus in the cluster initialization stage will never + change once equals to InitReplicas + format: int32 + type: integer + readyReplicas: + description: readyReplicas is the number of pods created for this + StatefulSet with a Ready Condition. + format: int32 + type: integer + replicas: + description: replicas is the number of Pods created by the StatefulSet + controller. + format: int32 + type: integer + updateRevision: + description: updateRevision, if not empty, indicates the version of + the StatefulSet used to generate Pods in the sequence [replicas-updatedReplicas,replicas) + type: string + updatedReplicas: + description: updatedReplicas is the number of Pods created by the + StatefulSet controller from the StatefulSet version indicated by + updateRevision. + format: int32 + type: integer + required: + - initReplicas + - replicas + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/deploy/helm/dashboards/cadvisor-exporter.json b/deploy/helm/dashboards/cadvisor.json similarity index 98% rename from deploy/helm/dashboards/cadvisor-exporter.json rename to deploy/helm/dashboards/cadvisor.json index d8793e6dd..8b8176931 100644 --- a/deploy/helm/dashboards/cadvisor-exporter.json +++ b/deploy/helm/dashboards/cadvisor.json @@ -26,7 +26,6 @@ "fiscalYearStartMonth": 0, "gnetId": 14282, "graphTooltip": 0, - "id": 5, "links": [ { "asDropdown": false, @@ -110,7 +109,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -210,7 +210,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -310,7 +311,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -410,7 +412,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -510,7 +513,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -610,7 +614,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -710,7 +715,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -810,7 +816,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -910,7 +917,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1010,7 +1018,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1110,7 +1119,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2000,7 +2010,7 @@ "refId": "A" } ], - "title": "Read Network Traffic", + "title": "Send Network Traffic", "type": "timeseries" }, { @@ -2260,7 +2270,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2276,7 +2287,7 @@ "h": 6, "w": 8, "x": 0, - "y": 27 + "y": 3 }, "id": 43, "options": { @@ -2360,7 +2371,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2376,7 +2388,7 @@ "h": 6, "w": 8, "x": 8, - "y": 27 + "y": 3 }, "id": 44, "options": { @@ -2460,7 +2472,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2476,7 +2489,7 @@ "h": 6, "w": 8, "x": 16, - "y": 27 + "y": 3 }, "id": 45, "options": { @@ -2560,7 +2573,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2576,7 +2590,7 @@ "h": 6, "w": 8, "x": 0, - "y": 33 + "y": 9 }, "id": 46, "options": { @@ -2660,7 +2674,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2676,7 +2691,7 @@ "h": 6, "w": 8, "x": 8, - "y": 33 + "y": 9 }, "id": 48, "options": { @@ -2760,7 +2775,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2776,7 +2792,7 @@ "h": 6, "w": 8, "x": 16, - "y": 33 + "y": 9 }, "id": 47, "options": { @@ -2860,7 +2876,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2876,7 +2893,7 @@ "h": 6, "w": 8, "x": 0, - "y": 39 + "y": 15 }, "id": 49, "options": { @@ -2960,7 +2977,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2976,7 +2994,7 @@ "h": 6, "w": 8, "x": 8, - "y": 39 + "y": 15 }, "id": 50, "options": { @@ -3174,4 +3192,4 @@ "uid": "pMEd7m0Mz", "version": 1, "weekStart": "" -} +} \ No newline at end of file diff --git a/deploy/helm/dashboards/jmx-dashboard-basic.json b/deploy/helm/dashboards/jmx.json similarity index 100% rename from deploy/helm/dashboards/jmx-dashboard-basic.json rename to deploy/helm/dashboards/jmx.json diff --git a/deploy/helm/dashboards/kafka-exporter-overview.json b/deploy/helm/dashboards/kafka.json similarity index 100% rename from deploy/helm/dashboards/kafka-exporter-overview.json rename to deploy/helm/dashboards/kafka.json diff --git a/deploy/helm/dashboards/mongodb.json b/deploy/helm/dashboards/mongodb.json new file mode 100644 index 000000000..1d370d879 --- /dev/null +++ b/deploy/helm/dashboards/mongodb.json @@ -0,0 +1,5684 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "Dashboard for MongoDB ReplicaSet managed by KubeBlocks", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 7359, + "graphTooltip": 1, + "id": 10, + "links": [ + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 117, + "panels": [], + "title": "Summary", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 119, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count(sum by(namespace)(mongodb_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}))", + "format": "time_series", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Total Namespaces", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 121, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count(sum by(namespace, app_kubernetes_io_instance)(mongodb_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}))", + "format": "time_series", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{label_name}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Total Clusters", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 123, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count(mongodb_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} > 0)", + "format": "time_series", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Total Ups", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + }, + { + "color": "dark-red", + "value": 0.5 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 1 + }, + "id": 125, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "(count(mongodb_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} <= 0)) or vector(0)", + "format": "time_series", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Total Downs", + "transformations": [], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "filterable": false, + "inspect": false + }, + "mappings": [ + { + "options": { + "0": { + "index": 2, + "text": "STARTUP" + }, + "1": { + "index": 1, + "text": "PRIMARY" + }, + "2": { + "index": 0, + "text": "SECONDARY" + }, + "3": { + "index": 3, + "text": "RECOVERING" + }, + "5": { + "index": 4, + "text": "STARTUP2" + }, + "6": { + "index": 5, + "text": "UNKNOWN" + }, + "7": { + "index": 6, + "text": "ARBITER" + }, + "8": { + "index": 7, + "text": "DOWN" + }, + "9": { + "index": 8, + "text": "ROLLBACK" + }, + "10": { + "index": 9, + "text": "REMOVED" + } + }, + "type": "value" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "instance" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "MongoDBInstance: ${__data.fields.cluster} | ${__data.fields.instance}", + "url": "/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-node=All&var-namespace=${__data.fields.namespace}&var-pod=${__data.fields.instance}&var-container=All" + } + ] + }, + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "dark-yellow", + "value": 60 + }, + { + "color": "dark-green", + "value": 120 + } + ] + } + }, + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 10, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 130, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": false, + "displayName": "instance" + } + ] + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum by(namespace, app_kubernetes_io_instance, pod) (mongodb_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "format": "table", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum by(namespace, app_kubernetes_io_instance, pod) (mongodb_instance_uptime_seconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "format": "table", + "hide": false, + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "B", + "step": 20 + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum by(namespace, app_kubernetes_io_instance, pod) (mongodb_mongod_replset_my_state{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "format": "table", + "hide": false, + "instant": true, + "legendFormat": "__auto", + "range": false, + "refId": "C" + } + ], + "title": "Instance Resource", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "pod", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true, + "Time 3": true, + "Time 4": true, + "Value #A": true, + "Value #B": false, + "app_kubernetes_io_instance 2": true, + "app_kubernetes_io_instance 3": true, + "app_kubernetes_io_instance 4": true, + "namespace 2": true, + "namespace 3": true, + "namespace 4": true + }, + "indexByName": { + "Time 1": 3, + "Time 2": 5, + "Time 3": 10, + "Value #A": 4, + "Value #B": 9, + "Value #C": 8, + "app_kubernetes_io_instance 1": 1, + "app_kubernetes_io_instance 2": 6, + "app_kubernetes_io_instance 3": 11, + "namespace 1": 0, + "namespace 2": 7, + "namespace 3": 12, + "pod": 2 + }, + "renameByName": { + "Value #B": "uptime", + "Value #C": "ReplSet State", + "app_kubernetes_io_instance 1": "cluster", + "namespace 1": "namespace", + "pod": "instance" + } + } + }, + { + "id": "convertFieldType", + "options": { + "conversions": [], + "fields": {} + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "instances" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 0, + "y": 5 + }, + "id": 127, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count by(mongodb)(mongodb_version_info{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "format": "table", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{short_version}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Cluster Versions", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": {}, + "renameByName": { + "Value": "instances", + "short_version": "version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "instances" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 6, + "x": 6, + "y": 5 + }, + "id": 128, + "interval": "1m", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count by(engine)(mongodb_mongod_storage_engine{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "format": "table", + "instant": true, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{short_version}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Cluster Engines", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": {}, + "renameByName": { + "Value": "instances", + "short_version": "version" + } + } + } + ], + "type": "table" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 11 + }, + "hiddenSeries": false, + "id": 155, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "max by(namespace, app_kubernetes_io_instance, pod) (mongodb_mongod_replset_member_replication_lag{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "ReplSet Lag", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "ns", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 11 + }, + "hiddenSeries": false, + "id": 156, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "time() - mongodb_mongod_replset_member_election_date{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "ReplSet Last Election", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "s", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 104, + "panels": [], + "title": "Memory & Network", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "mbytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 19 + }, + "hiddenSeries": false, + "id": 75, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "mongodb_memory{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", type=~\"resident|virtual\"}", + "hide": false, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}-{{type}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "MongoDB Memory", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "mbytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 19 + }, + "hiddenSeries": false, + "id": 105, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_network_metrics_num_requests_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval])", + "hide": false, + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "MongoDB NetworkRequests", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 26 + }, + "hiddenSeries": false, + "id": 131, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_network_bytesOut{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_network_bytesOut{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "$__rate_interval", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "MongoDB NetWorkOut", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 26 + }, + "hiddenSeries": false, + "id": 132, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_network_bytesIn{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_network_bytesIn{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "$__rate_interval", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "MongoDB NetworkIn", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 93, + "panels": [], + "title": "Operations", + "type": "row" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 34 + }, + "hiddenSeries": false, + "id": 27, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_op_counters_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_op_counters_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{type}}", + "metric": "", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Query Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 34 + }, + "hiddenSeries": false, + "id": 133, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_op_counters_repl_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_op_counters_repl_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{type}}", + "metric": "", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Replica Query Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 41 + }, + "hiddenSeries": false, + "id": 76, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_document_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{state}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Document Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ops" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 41 + }, + "hiddenSeries": false, + "id": 163, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_query_executor_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_query_executor_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{state}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Document Query Executor", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ops" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 48 + }, + "hiddenSeries": false, + "id": 134, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_op_latencies_ops_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_op_latencies_ops_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{type}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "OpLatencies Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 48 + }, + "hiddenSeries": false, + "id": 135, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_op_latencies_latency_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{type}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "OpLatencies Latency", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "ms", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "mongodb_mongod_metrics_ttl_deleted_documents_total", + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ops" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 55 + }, + "hiddenSeries": false, + "id": 160, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "rightSide": false, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_ttl_deleted_documents_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_ttl_deleted_documents_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "TTL Delete Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "short", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 137, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 21 + }, + "hiddenSeries": false, + "id": 81, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_connections{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", state=~\"(current|available|totalCreated)\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{state}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Collections", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 21 + }, + "hiddenSeries": false, + "id": 138, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_mongod_metrics_cursor_open{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - {{state}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Cursors", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "Connections & Cursors", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 63 + }, + "id": 97, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 80 + }, + "hiddenSeries": false, + "id": 98, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_dataSize{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "DataBase Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 80 + }, + "hiddenSeries": false, + "id": 101, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_indexSize{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Index Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 87 + }, + "hiddenSeries": false, + "id": 99, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_storageSize{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Storage Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 87 + }, + "hiddenSeries": false, + "id": 102, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_totalSize{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Total Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 94 + }, + "hiddenSeries": false, + "id": 100, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_indexes{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Indexes Num", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 94 + }, + "hiddenSeries": false, + "id": 139, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_dbstats_collections{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\", database=~\"$database\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{database}}", + "metric": "mongodb_mongod_metrics_repl_preload_indexes_num_total", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Collections Num", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "DB Stat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 64 + }, + "id": 95, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 23 + }, + "hiddenSeries": false, + "id": 85, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_oplog_stats_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "s" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 23 + }, + "hiddenSeries": false, + "id": 162, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_replset_oplog_head_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_replset_oplog_head_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Lag", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "s", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 30 + }, + "hiddenSeries": false, + "id": 80, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_mongod_metrics_repl_buffer_size_bytes{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - Used", + "range": true, + "refId": "A", + "step": 300 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_mongod_metrics_repl_buffer_max_size_bytes{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} - Max", + "range": true, + "refId": "B", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Buffer Capacity", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 10, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 30 + }, + "hiddenSeries": false, + "id": 161, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_repl_buffer_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_repl_buffer_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Buffered Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 37 + }, + "hiddenSeries": false, + "id": 84, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_replset_oplog_head_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_repl_preload_docs_total_milliseconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Processing Time", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ms", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ms" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 37 + }, + "hiddenSeries": false, + "id": 79, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_repl_network_getmores_total_milliseconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_repl_network_getmores_total_milliseconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Getmore Time", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ms", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Shows the time range in the oplog and the oldest backed up operation.", + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 44 + }, + "hiddenSeries": false, + "id": 165, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "time()-mongodb_mongod_replset_oplog_tail_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | Now to End", + "metric": "", + "range": true, + "refId": "J", + "step": 300 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_mongod_replset_oplog_head_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}-mongodb_mongod_replset_oplog_tail_timestamp{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | Oplog Range", + "metric": "", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Oplog Recovery Window", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "s", + "short" + ], + "yaxes": [ + { + "format": "s", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "Oplog", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 65 + }, + "id": 107, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 40 + }, + "hiddenSeries": false, + "id": 110, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_asserts_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_asserts_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{type}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Assert Events", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "Assert", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 66 + }, + "id": 141, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 41 + }, + "hiddenSeries": false, + "id": 112, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_globalLock_activeClients_readers{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_globalLock_activeClients_readers{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | readers", + "range": true, + "refId": "A", + "step": 300 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_globalLock_activeClients_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_globalLock_activeClients_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | total", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_globalLock_activeClients_writers{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_globalLock_activeClients_writers{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "interval": "", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | writers", + "range": true, + "refId": "C" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Active Clients", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 41 + }, + "hiddenSeries": false, + "id": 144, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_ss_globalLock_currentQueue{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_ss_globalLock_currentQueue{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{count_type}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Current Queue", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "GlobalLock", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 67 + }, + "id": 143, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "This is useful for write-heavy workloads to understand how long it takes to verify writes and how many concurrent writes are occurring.", + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 42 + }, + "hiddenSeries": false, + "id": 146, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_get_last_error_wtime_total_milliseconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_get_last_error_wtime_total_milliseconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Write Time", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "This is useful for write-heavy workloads to understand how long it takes to verify writes and how many concurrent writes are occurring.", + "editable": true, + "error": false, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 42 + }, + "hiddenSeries": false, + "id": 159, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_get_last_error_wtimeouts_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_get_last_error_wtimeouts_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | Timeouts", + "range": true, + "refId": "A", + "step": 300 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_metrics_get_last_error_wtime_num_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_metrics_get_last_error_wtime_num_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "hide": false, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | Total", + "range": true, + "refId": "B" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Write Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "GetLastError", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 68 + }, + "id": 148, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 43 + }, + "hiddenSeries": false, + "id": 113, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "mongodb_mongod_wiredtiger_cache_bytes{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Cache UsedSize", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "bytes" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 43 + }, + "hiddenSeries": false, + "id": 115, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_wiredtiger_cache_bytes_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_wiredtiger_cache_bytes_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{type}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Cache R/W Size", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "bytes", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "none" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 50 + }, + "hiddenSeries": false, + "id": 153, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate (mongodb_mongod_wiredtiger_cache_evicted_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Cache Evicted Page Num", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "none", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ops" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 50 + }, + "hiddenSeries": false, + "id": 149, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_mongod_wiredtiger_log_operations_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_mongod_wiredtiger_log_operations_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{type}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Log Operations", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "WiredTiger", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 69 + }, + "id": 151, + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editable": true, + "error": false, + "fieldConfig": { + "defaults": { + "unit": "ops" + }, + "overrides": [] + }, + "fill": 2, + "fillGradient": 0, + "grid": { + "leftLogBase": 1, + "leftMin": 0, + "rightLogBase": 1 + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 44 + }, + "hiddenSeries": false, + "id": 152, + "legend": { + "alignAsTable": true, + "avg": true, + "current": false, + "max": true, + "min": true, + "show": true, + "sort": "avg", + "sortDesc": true, + "total": false, + "values": true + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "9.2.4", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "rate(mongodb_extra_info_page_faults_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[$__rate_interval]) or irate(mongodb_extra_info_page_faults_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\", pod=~\"$instance\"}[5m])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{type}}", + "range": true, + "refId": "A", + "step": 300 + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Page Faults", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "x-axis": true, + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "y-axis": true, + "y_formats": [ + "ms", + "short" + ], + "yaxes": [ + { + "format": "ops", + "label": "", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "short", + "logBase": 1, + "show": true + } + ], + "yaxis": { + "align": false + } + } + ], + "title": "Extra Info", + "type": "row" + } + ], + "refresh": "1m", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "MongoDB", + "KubeBlocks" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(mongodb_mongod_replset_my_state, namespace)", + "hide": 0, + "includeAll": true, + "label": "Namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(mongodb_mongod_replset_my_state, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "type": "query" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(mongodb_mongod_replset_my_state, app_kubernetes_io_instance)", + "hide": 0, + "includeAll": true, + "label": "Cluster", + "multi": true, + "name": "cluster", + "options": [], + "query": { + "query": "label_values(mongodb_mongod_replset_my_state, app_kubernetes_io_instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(mongodb_mongod_replset_my_state{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(mongodb_mongod_replset_my_state{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "type": "query" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(mongodb_dbstats_ok{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}, database)", + "hide": 0, + "includeAll": true, + "label": "Database", + "multi": true, + "name": "database", + "options": [], + "query": { + "query": "label_values(mongodb_dbstats_ok{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}, database)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "type": "query" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "hidden": false, + "now": true, + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "browser", + "title": "MongoDB-ReplSet-KubeBlocks", + "uid": "7lzrQGNikKB", + "version": 1, + "weekStart": "" +} diff --git a/deploy/helm/dashboards/mysql-overview.json b/deploy/helm/dashboards/mysql-deprecated.json similarity index 99% rename from deploy/helm/dashboards/mysql-overview.json rename to deploy/helm/dashboards/mysql-deprecated.json index 0dc70dbdc..3ce638bd8 100644 --- a/deploy/helm/dashboards/mysql-overview.json +++ b/deploy/helm/dashboards/mysql-deprecated.json @@ -26,7 +26,6 @@ "fiscalYearStartMonth": 0, "gnetId": 11323, "graphTooltip": 1, - "id": 3, "links": [ { "asDropdown": false, @@ -150,13 +149,15 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, + "exemplar": false, "expr": "sum(mysql_global_status_uptime{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", "format": "time_series", + "instant": true, "interval": "1m", "intervalFactor": 1, "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", "metric": "", - "range": true, + "range": false, "refId": "A", "step": 300 } @@ -166,6 +167,7 @@ }, { "datasource": { + "type": "prometheus", "uid": "$datasource" }, "description": "**Current QPS**\n\nBased on the queries reported by MySQL's ``SHOW STATUS`` command, it is the number of statements executed by the server within the last second. This variable includes statements executed within stored programs, unlike the Questions variable. It does not count \n``COM_PING`` or ``COM_STATISTICS`` commands.", @@ -237,13 +239,17 @@ "uid": "$datasource" }, "datasourceErrors": {}, + "editorMode": "code", "errors": {}, + "exemplar": false, "expr": "sum(rate(mysql_global_status_queries{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", "format": "time_series", + "instant": true, "interval": "1m", "intervalFactor": 1, "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", "metric": "", + "range": false, "refId": "A", "step": 20 } @@ -253,6 +259,7 @@ }, { "datasource": { + "type": "prometheus", "uid": "$datasource" }, "description": "**InnoDB Buffer Pool Size**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", @@ -324,13 +331,17 @@ "uid": "$datasource" }, "datasourceErrors": {}, + "editorMode": "code", "errors": {}, + "exemplar": false, "expr": "sum(mysql_global_variables_innodb_buffer_pool_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", "format": "time_series", + "instant": true, "interval": "1m", "intervalFactor": 1, "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", "metric": "", + "range": false, "refId": "A", "step": 300 } @@ -1069,8 +1080,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1205,8 +1215,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1401,8 +1410,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1556,8 +1564,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1688,8 +1695,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -1812,8 +1818,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2347,8 +2352,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2461,8 +2465,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2570,8 +2573,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2702,8 +2704,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2807,8 +2808,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -2964,8 +2964,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -3137,8 +3136,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -3290,8 +3288,7 @@ "mode": "absolute", "steps": [ { - "color": "green", - "value": null + "color": "green" }, { "color": "red", @@ -4129,8 +4126,8 @@ "type": "timepicker" }, "timezone": "", - "title": "MySQL", - "uid": "549c2bf8936f7767ea6ac47c47b00f2a", + "title": "[Deprecated] MySQL", + "uid": "549c2bf8936f7767ea6ac47c47b00f2a_depr", "version": 1, "weekStart": "" } diff --git a/deploy/helm/dashboards/mysql.json b/deploy/helm/dashboards/mysql.json new file mode 100644 index 000000000..a04036d30 --- /dev/null +++ b/deploy/helm/dashboards/mysql.json @@ -0,0 +1,4974 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 11323, + "graphTooltip": 0, + "id": 20, + "links": [ + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 382, + "panels": [], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Summary", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**Uptime**\n\nThe amount of time since the last restart of the MySQL server process.", + "fieldConfig": { + "defaults": { + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(245, 54, 54, 0.9)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 300 + }, + { + "color": "dark-green", + "value": 3600 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 12, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "text": {}, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "min(sum(mysql_global_status_uptime{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod))", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "A", + "step": 300 + } + ], + "title": "Min Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**Current QPS**\n\nBased on the queries reported by MySQL's ``SHOW STATUS`` command, it is the number of statements executed by the server within the last second. This variable includes statements executed within stored programs, unlike the Questions variable. It does not count \n``COM_PING`` or ``COM_STATISTICS`` commands.", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 13, + "interval": "", + "links": [ + { + "targetBlank": true, + "title": "MySQL Server Status Variables", + "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Queries" + } + ], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum(rate(mysql_global_status_queries{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]))", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Total QPS", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**InnoDB Buffer Pool Size**\n\nInnoDB maintains a storage area called the buffer pool for caching data and indexes in memory. Knowing how the InnoDB buffer pool works, and taking advantage of it to keep frequently accessed data in memory, is one of the most important aspects of MySQL tuning. The goal is to keep the working set in memory. In most cases, this should be between 60%-90% of available memory on a dedicated database host, but depends on many factors.", + "fieldConfig": { + "defaults": { + "decimals": 0, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 51, + "interval": "", + "links": [ + { + "targetBlank": true, + "title": "Tuning the InnoDB Buffer Pool Size", + "url": "https://www.percona.com/blog/2015/06/02/80-ram-tune-innodb_buffer_pool_size/" + } + ], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "mean" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "min(sum(mysql_global_variables_innodb_buffer_pool_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod))", + "format": "time_series", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "A", + "step": 300 + } + ], + "title": "InnoDB Buffer Pool", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "auto", + "displayMode": "auto", + "inspect": false + }, + "decimals": 1, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "uptime" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "current qps" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "unit", + "value": "short" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "innodb buffer size" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "unit", + "value": "bytes" + } + ] + } + ] + }, + "gridPos": { + "h": 5, + "w": 15, + "x": 9, + "y": 1 + }, + "id": 403, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum(mysql_global_status_uptime{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "A", + "step": 300 + }, + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum(rate(mysql_global_status_queries{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "B", + "step": 300 + }, + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum(mysql_global_variables_innodb_buffer_pool_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": false, + "refId": "C", + "step": 300 + } + ], + "title": "Clusters && Instances", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "pod", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true, + "Time 3": true, + "app_kubernetes_io_instance 2": true, + "app_kubernetes_io_instance 3": true, + "namespace 2": true, + "namespace 3": true + }, + "indexByName": { + "Time 1": 3, + "Time 2": 5, + "Time 3": 9, + "Value #A": 4, + "Value #B": 8, + "Value #C": 12, + "app_kubernetes_io_instance 1": 1, + "app_kubernetes_io_instance 2": 6, + "app_kubernetes_io_instance 3": 10, + "namespace 1": 0, + "namespace 2": 7, + "namespace 3": 11, + "pod": 2 + }, + "renameByName": { + "Value #A": "uptime", + "Value #B": "current qps", + "Value #C": "innodb buffer size", + "app_kubernetes_io_instance 1": "cluster", + "namespace 1": "namespace", + "pod": "instance" + } + } + } + ], + "type": "table" + }, + { + "collapsed": false, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 6 + }, + "id": 383, + "panels": [], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Connections", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**Max Connections** \n\nMax Connections is the maximum permitted number of simultaneous client connections. By default, this is 151. Increasing this value increases the number of file descriptors that mysqld requires. If the required number of descriptors are not available, the server reduces the value of Max Connections.\n\nmysqld actually permits Max Connections + 1 clients to connect. The extra connection is reserved for use by accounts that have the SUPER privilege, such as root.\n\nMax Used Connections is the maximum number of connections that have been in use simultaneously since the server started.\n\nConnections is the number of connection attempts (successful or not) to the MySQL server.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Max Connections" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 7 + }, + "id": 92, + "links": [ + { + "targetBlank": true, + "title": "MySQL Server System Variables", + "url": "https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html#sysvar_max_connections" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(max_over_time(mysql_global_status_threads_connected{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Connections | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_status_max_used_connections{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max Used Connections | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20, + "target": "" + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_variables_max_connections{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Max Connections | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20, + "target": "" + } + ], + "title": "MySQL Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Active Threads**\n\nThreads Connected is the number of open connections, while Threads Running is the number of threads not sleeping.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "Threads", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Peak Threads Running" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + }, + { + "id": "custom.lineWidth", + "value": 0 + }, + { + "id": "custom.pointSize", + "value": 4 + }, + { + "id": "custom.showPoints", + "value": "always" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Peak Threads Connected" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#1F78C1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Avg Threads Running" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 7 + }, + "id": 10, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(max_over_time(mysql_global_status_threads_connected{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Peak Threads Connected | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(max_over_time(mysql_global_status_threads_running{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Peak Threads Running | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(avg_over_time(mysql_global_status_threads_running{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Avg Threads Running | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "C", + "step": 20 + } + ], + "title": "MySQL Client Thread Activity", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Thread Cache**\n\nThe thread_cache_size variable sets how many threads the server should cache to reuse. When a client disconnects, the client's threads are put in the cache if the cache is not full. It is autosized in MySQL 5.6.8 and above (capped to 100). Requests for threads are satisfied by reusing threads taken from the cache if possible, and only when the cache is empty is a new thread created.\n\n* *Threads_created*: The number of threads created to handle connections.\n* *Threads_cached*: The number of threads in the thread cache.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Threads Created" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 7 + }, + "id": 11, + "links": [ + { + "title": "Tuning information", + "url": "https://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_thread_cache_size" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_variables_thread_cache_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Thread Cache Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_status_threads_cached{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Threads Cached | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(mysql_global_status_threads_created{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Threads Created | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "MySQL Thread Cache", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**Aborted Connections**\n\nWhen a given host connects to MySQL and the connection is interrupted in the middle (for example due to bad credentials), MySQL keeps that info in a system table (since 5.6 this table is exposed in performance_schema).\n\nIf the amount of failed requests without a successful connection reaches the value of max_connect_errors, mysqld assumes that something is wrong and blocks the host from further connection.\n\nTo allow connections from that host again, you need to issue the ``FLUSH HOSTS`` statement.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 14 + }, + "id": 47, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_aborted_connects{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Aborted Connects (attempts) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_aborted_clients{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Aborted Clients (timeout) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20, + "target": "" + } + ], + "title": "MySQL Aborted Connections", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 384, + "panels": [], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Queries", + "type": "row" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**MySQL Questions**\n\nThe number of statements executed by the server. This includes only statements sent to the server by clients and not statements executed within stored programs, unlike the Queries used in the QPS calculation. \n\nThis variable does not count the following commands:\n* ``COM_PING``\n* ``COM_STATISTICS``\n* ``COM_STMT_PREPARE``\n* ``COM_STMT_CLOSE``\n* ``COM_STMT_RESET``", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 53, + "links": [ + { + "targetBlank": true, + "title": "MySQL Queries and Questions", + "url": "https://www.percona.com/blog/2014/05/29/how-mysql-queries-and-questions-are-measured/" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_questions{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + } + ], + "title": "MySQL Questions", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**MySQL Slow Queries**\n\nSlow queries are defined as queries being slower than the long_query_time setting. For example, if you have long_query_time set to 3, all queries that take longer than 3 seconds to complete will show on this graph.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 48, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_slow_queries{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Slow Queries | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + } + ], + "title": "MySQL Slow Queries", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**MySQL Select Types**\n\nAs with most relational databases, selecting based on indexes is more efficient than scanning an entire table's data. Here we see the counters for selects not done with indexes.\n\n* ***Select Scan*** is how many queries caused full table scans, in which all the data in the table had to be read and either discarded or returned.\n* ***Select Range*** is how many queries used a range scan, which means MySQL scanned all rows in a given range.\n* ***Select Full Join*** is the number of joins that are not joined on an index, this is usually a huge performance hit.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 311, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_select_full_join{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Select Full Join | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_select_full_range_join{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Select Full Range Join | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_select_range{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Select Range | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_select_range_check{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Select Range Check | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "D", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_select_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Select Scan | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "E", + "step": 20 + } + ], + "title": "MySQL Select Types", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**MySQL Sorts**\n\nDue to a query's structure, order, or other requirements, MySQL sorts the rows before returning them. For example, if a table is ordered 1 to 10 but you want the results reversed, MySQL then has to sort the rows to return 10 to 1.\n\nThis graph also shows when sorts had to scan a whole table or a given range of a table in order to return the results and which could not have been sorted via an index.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 30, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_sort_rows{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sort Rows | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_sort_range{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sort Range | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_sort_merge_passes{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sort Merge Passes | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_sort_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sort Scan | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "D", + "step": 20 + } + ], + "title": "MySQL Sort Rows", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 22, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_created_tmp_tables{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Created Tmp Tables | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_created_tmp_disk_tables{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Created Tmp Disk Tables | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_created_tmp_files{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Created Tmp Files | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + } + ], + "title": "MySQL Temporary Objects", + "type": "timeseries" + }, + { + "collapsed": true, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 36 + }, + "id": 390, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**Top Command Counters**\n\nThe Com_{{xxx}} statement counter variables indicate the number of times each xxx statement has been executed. There is one status variable for each type of statement. For example, Com_delete and Com_update count [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements, respectively. Com_delete_multi and Com_update_multi are similar but apply to [``DELETE``](https://dev.mysql.com/doc/refman/5.7/en/delete.html) and [``UPDATE``](https://dev.mysql.com/doc/refman/5.7/en/update.html) statements that use multiple-table syntax.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 14, + "links": [ + { + "title": "Server Status Variables (Com_xxx)", + "url": "https://dev.mysql.com/doc/refman/5.7/en/server-status-variables.html#statvar_Com_xxx" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "topk(15, rate(mysql_global_status_commands_total{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])>0)", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Com_{{ command }} | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Top Command Counters", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Handlers**\n\nHandler statistics are internal statistics on how MySQL is selecting, updating, inserting, and modifying rows, tables, and indexes.\n\nThis is in fact the layer between the Storage Engine and MySQL.\n\n* `read_rnd_next` is incremented when the server performs a full table scan and this is a counter you don't really want to see with a high value.\n* `read_key` is incremented when a read is done with an index.\n* `read_next` is incremented when the storage engine is asked to 'read the next index entry'. A high value means a lot of index scans are being done.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 8, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(mysql_global_status_handlers_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",handler!~\"commit|rollback|savepoint.*|prepare\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod,handler)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ handler }} | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "J", + "step": 20 + } + ], + "title": "MySQL Handlers", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 28, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(mysql_global_status_handlers_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",handler=~\"commit|rollback|savepoint.*|prepare\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod,handler)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{ handler }} | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "MySQL Transaction Handlers", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Command, Handlers, Processes", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 405, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Network Traffic**\n\nHere we can see how much network traffic is generated by MySQL. Outbound is network traffic sent from MySQL and Inbound is network traffic MySQL has received.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 59 + }, + "id": 9, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "right", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(mysql_global_status_bytes_received{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Inbound | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(mysql_global_status_bytes_sent{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Outbound | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Network Traffic", + "type": "timeseries" + } + ], + "title": "Traffic", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 388, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 39 + }, + "id": 407, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_innodb_buffer_pool_read_requests{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "Buffers Pool Read", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 39 + }, + "id": 408, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_innodb_buffer_pool_reads{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "Disk Read", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 39 + }, + "id": 409, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_innodb_buffer_pool_write_requests{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "Buffers Pool Write", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 0, + "y": 47 + }, + "id": 50, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "mysql_global_status_innodb_page_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} * on (namespace,app_kubernetes_io_instance,pod) mysql_global_status_buffer_pool_pages{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\", state=\"data\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Data | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "mysql_global_status_innodb_page_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} * on (namespace,app_kubernetes_io_instance,pod) mysql_global_status_buffer_pool_pages{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\", state=\"free\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Free | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "B", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "mysql_global_status_innodb_page_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} * on (namespace,app_kubernetes_io_instance,pod) mysql_global_status_buffer_pool_pages{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\", state=\"dirty\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Dirty | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "C", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "mysql_global_status_innodb_page_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} * on (namespace,app_kubernetes_io_instance,pod) mysql_global_status_buffer_pool_pages{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\", state=\"misc\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Misc | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "mysql_global_status_innodb_page_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} * on (namespace,app_kubernetes_io_instance,pod) mysql_global_status_buffer_pool_dirty_pages{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Dirty | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "E", + "step": 20 + } + ], + "title": "Buffer Pool Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 47 + }, + "id": 406, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(mysql_global_variables_innodb_log_buffer_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InnoDB Log Buffer Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_variables_innodb_additional_mem_pool_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "InnoDB Additional Memory Pool Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "H", + "step": 40 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(mysql_global_status_innodb_mem_dictionary{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InnoDB Dictionary Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "F", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_variables_key_buffer_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Key Buffer Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "B", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_variables_query_cache_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Query Cache Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "C", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_status_innodb_mem_adaptive_hash{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Adaptive Hash Index Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "E", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_variables_tokudb_cache_size{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TokuDB Cache Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "I", + "step": 20 + } + ], + "title": "Other Buffers Size", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Buffers", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 413, + "panels": [ + { + "datasource": { + "uid": "$datasource" + }, + "description": "**Table Locks**\n\nMySQL takes a number of different locks for varying reasons. In this graph we see how many Table level locks MySQL has requested from the storage engine. In the case of InnoDB, many times the locks could actually be row locks as it only takes table level locks in a few specific cases.\n\nIt is most useful to compare Locks Immediate and Locks Waited. If Locks waited is rising, it means you have lock contention. Otherwise, Locks Immediate rising and falling is normal activity.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 32, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_table_locks_immediate{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Table Locks Immediate | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(rate(mysql_global_status_table_locks_waited{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Table Locks Waited | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + } + ], + "title": "MySQL Table Locks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 410, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_innodb_row_lock_waits{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "Innodb Row Lock Waits", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 60, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "System Memory" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 411, + "links": [ + { + "title": "Detailed descriptions about metrics", + "url": "https://www.percona.com/doc/percona-monitoring-and-management/dashboard.mysql-overview.html#mysql-internal-memory-overview" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_innodb_row_lock_time{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod) / sum(rate(mysql_global_status_innodb_row_lock_waits{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "Innodb Row Lock Avg Time", + "type": "timeseries" + } + ], + "title": "Locks", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 392, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 41, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_global_status_open_files{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open Files | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_variables_open_files_limit{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open Files Limit | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "D", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(mysql_global_status_innodb_num_open_files{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InnoDB Open Files | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "B", + "step": 20 + } + ], + "title": "MySQL Open Files", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Open Tables**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 42, + "links": [ + { + "title": "Server Status Variables (table_open_cache)", + "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_global_status_open_tables{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open Tables | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_variables_table_open_cache{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Table Open Cache | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + } + ], + "title": "MySQL Open Tables", + "type": "timeseries" + }, + { + "datasource": { + "uid": "$datasource" + }, + "description": "**MySQL Table Definition Cache**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 54, + "links": [ + { + "title": "Server Status Variables (table_open_cache)", + "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_status_open_table_definitions{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open Table Definitions | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "errors": {}, + "expr": "sum(mysql_global_variables_table_definition_cache{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Table Definitions Cache Size | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "refId": "C", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "expr": "sum(rate(mysql_global_status_opened_table_definitions{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Opened Table Definitions | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "A", + "step": 20 + } + ], + "title": "MySQL Table Definition Cache", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "**MySQL Table Open Cache**\n\nThe recommendation is to set the `table_open_cache_instances` to a loose correlation to virtual CPUs, keeping in mind that more instances means the cache is split more times. If you have a cache set to 500 but it has 10 instances, each cache will only have 50 cached.\n\nThe `table_definition_cache` and `table_open_cache` can be left as default as they are auto-sized MySQL 5.6 and above (ie: do not set them to any value).", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 44, + "links": [ + { + "title": "Server Status Variables (table_open_cache)", + "url": "http://dev.mysql.com/doc/refman/5.6/en/server-system-variables.html#sysvar_table_open_cache" + } + ], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_table_open_cache_hits{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Hits | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "B", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_table_open_cache_misses{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Misses | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "C", + "step": 20 + }, + { + "datasource": { + "uid": "$datasource" + }, + "editorMode": "code", + "expr": "sum(rate(mysql_global_status_table_open_cache_overflows{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Misses due to Overflows | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D", + "step": 20 + } + ], + "title": "MySQL Table Open Cache", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Files and Tables", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 396, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 0, + "y": 42 + }, + "id": 401, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_info_schema_wesql_consensus_commit_index{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod,instance_role)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{instance_role}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Commit Index", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 8, + "y": 42 + }, + "id": 402, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_info_schema_wesql_consensus_last_log_index{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod,instance_role)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{instance_role}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Last Log Index", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 8, + "x": 16, + "y": 42 + }, + "id": 400, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_info_schema_wesql_consensus_last_apply_index{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod,instance_role)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{instance_role}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Last Apply Index", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 49 + }, + "id": 399, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_info_schema_wesql_consensus_current_term{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod,instance_role)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{instance_role}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Current Term", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 49 + }, + "id": 398, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(mysql_info_schema_wesql_consensus_last_log_term{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod,instance_role)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{instance_role}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Last Log Term", + "type": "timeseries" + } + ], + "title": "ApeCloud MySQL Consensus", + "type": "row" + } + ], + "refresh": "", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "mysql", + "db" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(mysql_up{job=\"$job\"}, namespace)", + "hide": 0, + "includeAll": true, + "label": "namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(mysql_up{job=\"$job\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(mysql_up{job=\"$job\"}, app_kubernetes_io_instance)", + "hide": 0, + "includeAll": true, + "label": "cluster", + "multi": true, + "name": "cluster", + "options": [], + "query": { + "query": "label_values(mysql_up{job=\"$job\"}, app_kubernetes_io_instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(mysql_up{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "hide": 0, + "includeAll": true, + "label": "instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(mysql_up{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "agamotto", + "value": "agamotto" + }, + "hide": 0, + "includeAll": false, + "label": "job", + "multi": false, + "name": "job", + "options": [ + { + "selected": true, + "text": "agamotto", + "value": "agamotto" + } + ], + "query": "agamotto", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "hidden": false, + "notice": false, + "now": true, + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "", + "title": "MySQL", + "uid": "549c2bf8936f7767ea6ac47c47b00f2a", + "version": 1, + "weekStart": "" +} diff --git a/deploy/helm/dashboards/node-deprecated.json b/deploy/helm/dashboards/node-deprecated.json new file mode 100644 index 000000000..5c7eab299 --- /dev/null +++ b/deploy/helm/dashboards/node-deprecated.json @@ -0,0 +1,22429 @@ +{ + "annotations": { + "list": [ + { + "$$hashKey": "object:1058", + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 1860, + "graphTooltip": 1, + "id": 16, + "links": [ + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + }, + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 322, + "panels": [], + "title": "Summary", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total number of CPU cores", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 14, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "count(count(node_cpu_seconds_total{instance=\"$node\"}) by (cpu))", + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "refId": "A", + "step": 240 + } + ], + "title": "CPU Cores", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total RAM", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 75, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_MemTotal_bytes{instance=\"$node\"}", + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "RAM Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total SWAP", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 18, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_SwapTotal_bytes{instance=\"$node\"}", + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Total Disk", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 2, + "w": 3, + "x": 9, + "y": 1 + }, + "id": 323, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "topk(1,node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'})", + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Basic CPU info", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy System" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy User" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Other" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 9, + "x": 12, + "y": 1 + }, + "id": 382, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Busy System", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Busy User", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\",mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy IOWait", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=~\".*irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy IRQs", + "range": true, + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy Other", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "Idle", + "range": true, + "refId": "F", + "step": 240 + } + ], + "title": "CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "System uptime", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 21, + "y": 1 + }, + "hideTimeOverride": true, + "id": 15, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_time_seconds{instance=\"$node\"} - node_boot_time_seconds{instance=\"$node\"}", + "intervalFactor": 1, + "legendFormat": "The node has been on for how many time", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "System Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Busy state of all CPU cores together", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 3 + }, + "id": 20, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "(sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode!=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))) * 100", + "hide": false, + "intervalFactor": 1, + "legendFormat": "", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Busy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Non available RAM memory", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 3 + }, + "hideTimeOverride": false, + "id": 16, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "((node_memory_MemTotal_bytes{instance=\"$node\"} - node_memory_MemFree_bytes{instance=\"$node\"}) / (node_memory_MemTotal_bytes{instance=\"$node\"} )) * 100", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "100 - ((node_memory_MemAvailable_bytes{instance=\"$node\"} * 100) / node_memory_MemTotal_bytes{instance=\"$node\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "refId": "B", + "step": 240 + } + ], + "title": "RAM Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Used Swap", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 3 + }, + "id": 21, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "((node_memory_SwapTotal_bytes{instance=\"$node\"} - node_memory_SwapFree_bytes{instance=\"$node\"}) / (node_memory_SwapTotal_bytes{instance=\"$node\"} )) * 100", + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Used Disk", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 9, + "y": 3 + }, + "id": 324, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "((topk(1,node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'})- topk(1,node_filesystem_free_bytes{instance=\"$node\",device!~'rootfs'})) / topk(1,node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'})) * 100", + "intervalFactor": 1, + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "system load", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 3, + "x": 21, + "y": 4 + }, + "hideTimeOverride": true, + "id": 325, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_load5{instance=\"$node\"} ", + "format": "time_series", + "instant": false, + "intervalFactor": 1, + "legendFormat": "jobs waiting and running", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "System Load", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Basic memory usage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "SWAP Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Cache + Buffer" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Available" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#DEDAF7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 7 + }, + "id": 78, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Total", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\"} - node_memory_MemFree_bytes{instance=\"$node\"} - (node_memory_Cached_bytes{instance=\"$node\"} + node_memory_Buffers_bytes{instance=\"$node\"} + node_memory_SReclaimable_bytes{instance=\"$node\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Used", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Cached_bytes{instance=\"$node\"} + node_memory_Buffers_bytes{instance=\"$node\"} + node_memory_SReclaimable_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Cache + Buffer", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_MemFree_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Free", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\"} - node_memory_SwapFree_bytes{instance=\"$node\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SWAP Used", + "refId": "E", + "step": 240 + } + ], + "title": "Memory ", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 7 + }, + "id": 431, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Data", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Disk space used of all filesystems mounted", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 7 + }, + "id": 152, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "100 - ((node_filesystem_avail_bytes{instance=\"$node\",device!~'rootfs'} * 100) / node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Disk Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Basic network info per interface", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 7 + }, + "id": 74, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_bytes_total{instance=\"$node\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "recv {{device}}", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_bytes_total{instance=\"$node\"}[$__rate_interval])*8", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "trans {{device}} ", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 14 + }, + "id": 327, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Idle - Waiting for something to happen" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Iowait - Waiting for I/O to complete" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Irq - Servicing interrupts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Nice - Niced processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Softirq - Servicing softirqs" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Steal - Time spent in other operating systems when running in a virtualized environment" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCE2DE", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "System - Processes executing in kernel mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "User - Normal processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5195CE", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 3, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "System - Processes executing in kernel mode", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "User - Normal processes executing in user mode", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"nice\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Nice - Niced processes executing in user mode", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Iowait - Waiting for I/O to complete", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Irq - Servicing interrupts", + "range": true, + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"softirq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Softirq - Servicing softirqs", + "range": true, + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"steal\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Steal - Time spent in other operating systems when running in a virtualized environment", + "range": true, + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{instance=\"$node\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Idle - Waiting for something to happen", + "range": true, + "refId": "J", + "step": 240 + } + ], + "title": "CPU Modes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in system mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 341, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"system\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU System Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in IDLE mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 334, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"idle\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Idle Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in system mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 368, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"user\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU User Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in iowait mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 335, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"iowait\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU IOWait time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in iqr mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 338, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"irq\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Irq Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in nice mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 337, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"nice\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Nice time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in softirq mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 27 + }, + "id": 336, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"softirq\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Softirq time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "the time each CPU spend in steal mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 27 + }, + "id": 342, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{instance=\"$node\",mode=\"steal\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Steal time", + "type": "timeseries" + } + ], + "title": "CPU per Core", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 266, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap - Swap memory usage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused - Free memory unassigned" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Hardware Corrupted - *./" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 3 + }, + "id": 24, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_MemTotal_bytes{instance=\"$node\"} - node_memory_MemFree_bytes{instance=\"$node\"} - node_memory_Buffers_bytes{instance=\"$node\"} - node_memory_Cached_bytes{instance=\"$node\"} - node_memory_Slab_bytes{instance=\"$node\"} - node_memory_PageTables_bytes{instance=\"$node\"} - node_memory_SwapCached_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Apps - Memory used by user-space applications", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_PageTables_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "PageTables - Memory used to map between virtual and physical memory addresses", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_SwapCached_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Slab_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Cached_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Cache - Parked file data (file content) cache", + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Buffers_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Buffers - Block device (e.g. harddisk) cache", + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_MemFree_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Unused - Free memory unassigned", + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "(node_memory_SwapTotal_bytes{instance=\"$node\"} - node_memory_SwapFree_bytes{instance=\"$node\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Swap - Swap space used", + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_HardwareCorrupted_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working", + "refId": "I", + "step": 240 + } + ], + "title": "Memory Stack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 3 + }, + "id": 136, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Inactive_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Inactive - Memory which has been less recently used. It is more eligible to be reclaimed for other purposes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Active_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Active - Memory that has been used more recently and usually not reclaimed unless absolutely necessary", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Active / Inactive", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 3 + }, + "id": 430, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Inactive_file_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_file - File-backed memory on inactive LRU list", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Inactive_anon_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_anon - Anonymous and swap cache on inactive LRU list, including tmpfs (shmem)", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Active_file_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_file - File-backed memory on active LRU list", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Active_anon_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_anon - Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Active / Inactive Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 9 + }, + "id": 128, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_DirectMap1G_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "DirectMap1G - Amount of pages mapped as this size", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_DirectMap2M_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap2M - Amount of pages mapped as this size", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_DirectMap4k_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap4K - Amount of pages mapped as this size", + "refId": "C", + "step": 240 + } + ], + "title": "Memory DirectMap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*CommitLimit - *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 9 + }, + "id": 135, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Committed_AS_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Committed_AS - Amount of memory presently allocated on the system", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_CommitLimit_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "CommitLimit - Amount of memory currently available to be allocated on the system", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Committed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 9 + }, + "id": 138, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Mapped_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Mapped - Used memory in mapped pages files which have been mapped, such as libraries", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Shmem_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Shmem - Used shared memory (shared between several processes, thus including RAM disks)", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_ShmemHugePages_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_ShmemPmdMapped_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages", + "refId": "D", + "step": 240 + } + ], + "title": "Memory Shared and Mapped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 15 + }, + "id": 137, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Unevictable_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Unevictable - Amount of unevictable memory that can't be swapped out for a variety of reasons", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Mlocked_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "MLocked - Size of pages locked to memory using the mlock() system call", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Unevictable and MLocked", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "KernelStack - Kernel memory stack. This is not reclaimable" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 15 + }, + "id": 160, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_KernelStack_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "KernelStack - Kernel memory stack. This is not reclaimable", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Percpu_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PerCPU - Per CPU memory allocated dynamically by loadable modules", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Kernel per CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 15 + }, + "id": 130, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Writeback_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Writeback - Memory which is actively being written back to disk", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_WritebackTmp_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "WritebackTmp - Memory used by FUSE for temporary writeback buffers", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Dirty_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Dirty - Memory which is waiting to get written back to the disk", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_NFS_Unstable_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "NFS Unstable - NFS memory blocks waiting to be written into storage", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Writeback and Dirty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 131, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_SUnreclaim_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SUnreclaim - Part of Slab, that cannot be reclaimed on memory pressure", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_SReclaimable_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SReclaimable - Part of Slab, that might be reclaimed, such as caches", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Slab_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Slab Total - total size of slab memory ", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory Slab", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 70, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_VmallocChunk_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocChunk - Largest contiguous block of vmalloc area which is free", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_VmallocTotal_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocTotal - Total size of vmalloc memory area", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_VmallocUsed_bytes{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocUsed - Amount of vmalloc area which is used", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Vmalloc", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "=", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Inactive *./" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 129, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_AnonHugePages_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonHugePages - Memory in anonymous huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_AnonPages_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonPages - Memory in user pages not backed by files", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Anonymous", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 71, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_HugePages_Total{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages - Total size of the pool of huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_memory_Hugepagesize_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Hugepagesize - Huge Page size", + "refId": "B", + "step": 240 + } + ], + "title": "Memory HugePages Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 27 + }, + "id": 428, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Free{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Free - Huge pages in the pool that are not yet allocated", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Rsvd{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Surp{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Surp - Huge pages in the pool above the value in /proc/sys/vm/nr_hugepages", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory HugePages Counter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 27 + }, + "id": 426, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_memory_Bounce_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Bounce - Memory used for block device bounce buffers", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Memory Bounce", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "Memory Meminfo", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 370, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 41 + }, + "id": 176, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pgpgin{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesin - Page in operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pgpgout{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesout - Page out operations", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 41 + }, + "id": 22, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pswpin{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpin - Pages swapped in", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pswpout{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpout - Pages swapped out", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages Swap In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pgfault - Page major and minor fault operations" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 41 + }, + "id": 175, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pgfault{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgfault - Page major and minor fault operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pgmajfault{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgmajfault - Major page fault operations", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_pgfault{instance=\"$node\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgminfault - Minor page fault operations", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Page Faults", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 47 + }, + "id": 307, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_vmstat_oom_kill{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "oom killer invocations ", + "refId": "A", + "step": 240 + } + ], + "title": "OOM Killer", + "type": "timeseries" + } + ], + "title": "Memory Vmstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 386, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 260, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_timex_estimated_error_seconds{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Estimated error in seconds", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_offset_seconds{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time offset in between local system and reference clock", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_maxerror_seconds{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum error in seconds", + "refId": "C", + "step": 240 + } + ], + "title": "Time Synchronized Drift", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 291, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_loop_time_constant{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Phase-locked loop time adjust", + "refId": "A", + "step": 240 + } + ], + "title": "Time PLL Adjust", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 168, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_sync_status{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Is clock synchronized to a reliable server (1 = yes, 0 = no)", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_frequency_adjustment_ratio{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Local clock frequency adjustment", + "refId": "B", + "step": 240 + } + ], + "title": "Time Synchronized Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 35 + }, + "id": 294, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_tick_seconds{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Seconds between clock ticks", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_timex_tai_offset_seconds{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "International Atomic Time (TAI) offset", + "refId": "B", + "step": 240 + } + ], + "title": "Time Misc", + "type": "timeseries" + } + ], + "title": "System Time Synchronize", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 376, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 30 + }, + "id": 62, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_procs_running{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Processes in runnable state", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_procs_blocked{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes blocked waiting for I/O to complete", + "refId": "C", + "step": 240 + } + ], + "title": "Processes Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 30 + }, + "id": 345, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "process_virtual_memory_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Processes virtual memory size", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "process_resident_memory_max_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of virtual memory available", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "process_virtual_memory_max_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of virtual memory available", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Processes Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 30 + }, + "id": 148, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_forks_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes forks second", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Forks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 379, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_processes_state{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes in {{state}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 378, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_processes_pids{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes Pids", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Pid Number", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 36 + }, + "id": 377, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_processes_max_threads{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum number of threads", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_processes_threads{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Allocated threads in system", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Threads Number and Limit", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 42 + }, + "id": 380, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_processes_threads_state{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Threads in {{thread_state}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Threads States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*waiting.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 42 + }, + "id": 388, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_running_seconds_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent running a process", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_waiting_seconds_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent by processing waiting for this CPU", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Process Schedule Stats Running / Waiting", + "type": "timeseries" + } + ], + "title": "System Processes", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 333, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 31 + }, + "id": 7, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_load1{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 1m", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_load5{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 5m", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_load15{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 15m", + "refId": "C", + "step": 240 + } + ], + "title": "System Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 31 + }, + "id": 64, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "process_max_fds{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum open file descriptors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "process_open_fds{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open file descriptors", + "refId": "B", + "step": 240 + } + ], + "title": "Process File Descriptors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 31 + }, + "id": 8, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_context_switches_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Context switches", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_intr_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Interrupts", + "refId": "B", + "step": 240 + } + ], + "title": "Context Switches / Interrupts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 37 + }, + "id": 306, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_timeslices_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Schedule timeslices executed by each cpu", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 37 + }, + "id": 390, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(process_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time spend", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Process CPU Time Spend", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 37 + }, + "id": 151, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_entropy_available_bits{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Entropy available to random number generators", + "refId": "A", + "step": 240 + } + ], + "title": "Entropy", + "type": "timeseries" + } + ], + "title": "System Misc", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 329, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number (after merges) of I/O requests completed per second for the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 9, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_reads_completed_total{instance=\"$node\"}[$__rate_interval])", + "intervalFactor": 4, + "legendFormat": "{{device}} - Reads completed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_writes_completed_total{instance=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Writes completed", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Completed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 433, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Data", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of bytes read from or written to the device per IO", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 33, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{instance=\"$node\"}[$__rate_interval])/ irate(node_disk_reads_completed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_written_bytes_total{instance=\"$node\"}[$__rate_interval])/ irate(node_disk_writes_completed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average R/W Data", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 37, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_time_seconds_total{instance=\"$node\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{instance=\"$node\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read wait time avg", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_disk_write_time_seconds_total{instance=\"$node\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{instance=\"$node\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write wait time avg", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of read and write requests merged per second that were queued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 27 + }, + "id": 133, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_reads_merged_total{instance=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Read merged", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_writes_merged_total{instance=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 27 + }, + "id": 301, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_discards_completed_total{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Discards completed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_discards_merged_total{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Discards merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Discards completed / merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Percentage of elapsed time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100% for devices serving requests serially. But for devices serving requests in parallel, such as RAID arrays and modern SSDs, this number does not reflect their performance limits.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 33 + }, + "id": 36, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_io_time_seconds_total{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_discard_time_seconds_total{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - discard", + "refId": "B", + "step": 240 + } + ], + "title": "Time Spent Doing I/Os", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The average queue length of the requests that were issued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 33 + }, + "id": 35, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_disk_io_time_weighted_seconds_total{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}}", + "refId": "A", + "step": 240 + } + ], + "title": "Average Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "The number of outstanding requests at the instant the sample was taken. Incremented as requests are given to appropriate struct request_queue and decremented as they finish.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 33 + }, + "id": 34, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_disk_io_now{instance=\"$node\"}", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO now", + "refId": "A", + "step": 240 + } + ], + "title": "Instantaneous Queue Size", + "type": "timeseries" + } + ], + "title": "Disk", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 372, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 156, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'} - node_filesystem_avail_bytes{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Filesystem Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 43, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_avail_bytes{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Available", + "metric": "", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_free_bytes{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_size_bytes{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Size", + "refId": "C", + "step": 240 + } + ], + "title": "Filesystem Space Available", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 28, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filefd_maximum{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Max open files", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filefd_allocated{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Open files", + "refId": "B", + "step": 240 + } + ], + "title": "File Descriptor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 219, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_filesystem_files{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - File nodes total", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Total Inodes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 41, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_filesystem_files_free{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free file nodes", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Free Inodes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "/ ReadOnly" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 44, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_readonly{instance=\"$node\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - ReadOnly", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_filesystem_device_error{instance=\"$node\",device!~'rootfs',fstype!~'tmpfs'}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Device error", + "refId": "B", + "step": 240 + } + ], + "title": "Filesystem in ReadOnly / Error", + "type": "timeseries" + } + ], + "title": "File System", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 272, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "receive_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "receive_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 88 + }, + "id": 60, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_packets_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_packets_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic by Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 88 + }, + "id": 142, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_errs_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive errors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_network_transmit_errs_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit errors", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 88 + }, + "id": 290, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_softnet_processed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Processed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_softnet_dropped_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Dropped", + "refId": "B", + "step": 240 + } + ], + "title": "Softnet Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 94 + }, + "id": 144, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_fifo_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive fifo", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_fifo_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit fifo", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Fifo", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 94 + }, + "id": 141, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_compressed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive compressed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_compressed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit compressed", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Compressed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 94 + }, + "id": 143, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_drop_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive drop", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_drop_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit drop", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Drop", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 100 + }, + "id": 232, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_colls_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit colls", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Colls", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 100 + }, + "id": 145, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_frame_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive frame", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Frame", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 100 + }, + "id": 146, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_receive_multicast_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive multicast", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Multicast", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 106 + }, + "id": 280, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_network_speed_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Speed", + "refId": "A", + "step": 240 + } + ], + "title": "Speed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NF conntrack limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 106 + }, + "id": 61, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_nf_conntrack_entries{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack entries - Number of currently allocated flow entries for connection tracking", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_nf_conntrack_entries_limit{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack limit - Maximum size of connection tracking table", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "NF Contrack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 106 + }, + "id": 231, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_network_transmit_carrier_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Statistic transmit_carrier", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Carrier", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 112 + }, + "id": 310, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_softnet_times_squeezed_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Squeezed", + "refId": "A", + "step": 240 + } + ], + "title": "Softnet Out of Quota", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 112 + }, + "id": 289, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_network_transmit_queue_length{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Interface transmit queue length", + "refId": "A", + "step": 240 + } + ], + "title": "Queue Length", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 112 + }, + "id": 424, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_arp_entries{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - ARP entries", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ARP Entries", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 118 + }, + "id": 309, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_network_up{operstate=\"up\",instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{interface}} - Operational state UP", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_network_carrier{instance=\"$node\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{device}} - Physical link state", + "refId": "B" + } + ], + "title": "Network Operational Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 118 + }, + "id": 288, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_network_mtu_bytes{instance=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Bytes", + "refId": "A", + "step": 240 + } + ], + "title": "MTU", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "Network Traffic", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 374, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 125 + }, + "id": 394, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_sockstat_TCP_alloc{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_alloc - Allocated sockets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_TCP_inuse{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_inuse - Tcp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_TCP_mem{instance=\"$node\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_mem - Used memory for tcp", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_TCP_orphan{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_orphan - Orphan sockets", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_TCP_tw{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_tw - Sockets waiting close", + "refId": "E", + "step": 240 + } + ], + "title": "Sockstat TCP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 125 + }, + "id": 396, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_sockstat_UDPLITE_inuse{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDPLITE_inuse - Udplite sockets currently in use", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_UDP_inuse{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_inuse - Udp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_UDP_mem{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_mem - Used memory for udp", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat UDP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 125 + }, + "id": 392, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_sockstat_FRAG_inuse{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_inuse - Frag sockets currently in use", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_RAW_inuse{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RAW_inuse - Raw sockets currently in use", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat FRAG / RAW", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 131 + }, + "id": 220, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_TCP_mem_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - TCP sockets in that state", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_UDP_mem_bytes{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - UDP sockets in that state", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_FRAG_memory{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_memory - Used memory for frag", + "refId": "C" + } + ], + "title": "Sockstat Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 131 + }, + "id": 126, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_sockstat_sockets_used{instance=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sockets_used - Sockets currently in use", + "refId": "A", + "step": 240 + } + ], + "title": "Sockstat Used", + "type": "timeseries" + } + ], + "title": "Network Sockstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 398, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 138 + }, + "id": 406, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Ip_Forwarding{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Forwarding - IP forwarding", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Netstat IP Forwarding", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 138 + }, + "id": 412, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_ActiveOpens{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ActiveOpens - TCP connections that have made a direct transition to the SYN-SENT state from the CLOSED state", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_PassiveOpens{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PassiveOpens - TCP connections that have made a direct transition to the SYN-RCVD state from the LISTEN state", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Direct Transition", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*MaxConn *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 138 + }, + "id": 410, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_netstat_Tcp_CurrEstab{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "CurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_netstat_Tcp_MaxConn{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 144 + }, + "id": 416, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InSegs{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "InSegs - Segments received, including those received in error. This count includes segments received on currently established connections", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutSegs{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutSegs - Segments sent, including those on current connections but excluding those containing only retransmitted octets", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 144 + }, + "id": 402, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InMsgs{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InMsgs - Messages which the entity received. Note that this counter includes all those counted by icmpInErrors", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "irate(node_netstat_Icmp_OutMsgs{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutMsgs - Messages which this entity attempted to send. Note that this counter includes all those counted by icmpOutErrors", + "refId": "B", + "step": 240 + } + ], + "title": "ICMP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 144 + }, + "id": 422, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InDatagrams{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InDatagrams - Datagrams received", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_OutDatagrams{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutDatagrams - Datagrams sent", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "UDP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "octets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 150 + }, + "id": 408, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_InOctets{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InOctets - Received octets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_OutOctets{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "OutOctets - Sent octets", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Netstat IP In / Out Octets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 150 + }, + "id": 414, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenOverflows{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenOverflows - Times the listen queue of a socket overflowed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenDrops{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenDrops - SYNs to LISTEN sockets ignored", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPSynRetrans{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCPSynRetrans - SYN-SYN/ACK retransmits to break down retransmissions in SYN, fast/timeout retransmits", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_RetransSegs{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InErrs{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrs - Segments received in error (e.g., bad TCP checksums)", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutRsts{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "OutRsts - Segments sent with RST flag", + "range": true, + "refId": "F" + } + ], + "title": "TCP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 150 + }, + "id": 400, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InErrors{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - Messages which the entity received but determined as having ICMP-specific errors (bad ICMP checksums, bad length, etc.)", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ICMP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 156 + }, + "id": 420, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InErrors{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - UDP Datagrams that could not be delivered to an application", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_NoPorts{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "NoPorts - UDP Datagrams received on a port with no listener", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_UdpLite_InErrors{instance=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrors Lite - UDPLite Datagrams that could not be delivered to an application", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_RcvbufErrors{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RcvbufErrors - UDP buffer errors received", + "range": true, + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_SndbufErrors{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "SndbufErrors - UDP buffer errors send", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "UDP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Sent.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 156 + }, + "id": 418, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesFailed{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesFailed - Invalid SYN cookies received", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesRecv{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesRecv - SYN cookies received", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesSent{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesSent - SYN cookies sent", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "TCP SynCookie", + "type": "timeseries" + } + ], + "title": "Network Netstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 363, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": -8, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bool_yes_no" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*error.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 139 + }, + "id": 365, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_scrape_collector_success{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape success", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "expr": "node_textfile_scrape_error{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape textfile error (1 = true)", + "refId": "B", + "step": 240 + } + ], + "title": "Node Exporter Scrape", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green" + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 139 + }, + "id": 367, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "node_scrape_collector_duration_seconds{instance=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape duration", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Node Exporter Scrape Time", + "type": "timeseries" + } + ], + "title": "Node Exporter", + "type": "row" + } + ], + "refresh": false, + "revision": 1, + "schemaVersion": 37, + "style": "dark", + "tags": [ + "linux", + "node" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "172.21.0.3:9100", + "value": "172.21.0.3:9100" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(node_uname_info{node=\"$Node\"}, instance)", + "hide": 2, + "includeAll": false, + "label": "Host:", + "multi": false, + "name": "node", + "options": [], + "query": { + "query": "label_values(node_uname_info{node=\"$Node\"}, instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 1, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + }, + "hide": 2, + "includeAll": false, + "multi": false, + "name": "diskdevices", + "options": [ + { + "selected": true, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + } + ], + "query": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "Prometheus", + "value": "Prometheus" + }, + "hide": 0, + "includeAll": false, + "label": "datasource", + "multi": false, + "name": "DataSource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "k3d-k3s-default-server-0", + "value": "k3d-k3s-default-server-0" + }, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "definition": "label_values(node_uname_info{}, node)", + "hide": 0, + "includeAll": false, + "label": "node", + "multi": false, + "name": "Node", + "options": [], + "query": { + "query": "label_values(node_uname_info{}, node)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "type": "query" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "[Deprecated] Node Exporter", + "uid": "nodeexporter1af4132_deprecated", + "version": 1, + "weekStart": "" +} diff --git a/deploy/helm/dashboards/node.json b/deploy/helm/dashboards/node.json new file mode 100644 index 000000000..7fc59970c --- /dev/null +++ b/deploy/helm/dashboards/node.json @@ -0,0 +1,23129 @@ +{ + "annotations": { + "list": [ + { + "$$hashKey": "object:1058", + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 1860, + "graphTooltip": 0, + "id": 17, + "links": [ + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 322, + "panels": [], + "title": "Summary", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total number of CPU cores", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 0, + "y": 1 + }, + "id": 14, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "count(count(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}) by (cpu))", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Cores", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total RAM", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 2, + "y": 1 + }, + "id": 75, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"}", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "RAM Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total SWAP", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 4, + "y": 1 + }, + "id": 18, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_memory_SwapTotal_bytes{job=\"$job\",node=\"$node\"}", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Total Disk", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 6, + "y": 1 + }, + "id": 323, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "topk(1,node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'})", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Total", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "System uptime", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 8, + "y": 1 + }, + "hideTimeOverride": true, + "id": 436, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "none", + "graphMode": "none", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_time_seconds{job=\"$job\",node=\"$node\"} - node_boot_time_seconds{job=\"$job\",node=\"$node\"}", + "instant": true, + "intervalFactor": 1, + "legendFormat": "The node has been on for how many time", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "System Uptime", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "system load", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "min": 0, + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": 90 + }, + { + "color": "red", + "value": 120 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 10, + "y": 1 + }, + "hideTimeOverride": true, + "id": 325, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_load5{job=\"$job\",node=\"$node\"} ", + "format": "time_series", + "instant": true, + "intervalFactor": 1, + "legendFormat": "jobs waiting and running", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "System Load", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Processes blocked waiting for I/O to complete", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "noValue": "N/A", + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": 80 + }, + { + "color": "red", + "value": 120 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 3, + "w": 2, + "x": 12, + "y": 1 + }, + "hideTimeOverride": true, + "id": 434, + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "node_procs_blocked{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "instant": true, + "intervalFactor": 1, + "legendFormat": "__auto", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Process Blocked", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic CPU info", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Iowait" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Idle" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy System" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy User" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Busy Other" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 10, + "x": 14, + "y": 1 + }, + "id": 382, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Busy System", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Busy User", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy IOWait", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=~\".*irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy IRQs", + "range": true, + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode!='idle',mode!='user',mode!='system',mode!='iowait',mode!='irq',mode!='softirq'}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Busy Other", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "Idle", + "range": true, + "refId": "F", + "step": 240 + } + ], + "title": "CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Busy state of all CPU cores together", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 0, + "y": 4 + }, + "id": 20, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode!=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))) * 100", + "hide": false, + "instant": true, + "intervalFactor": 1, + "legendFormat": "", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Busy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Non available RAM memory", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 0, + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 2, + "y": 4 + }, + "hideTimeOverride": false, + "id": 16, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "((node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_MemFree_bytes{job=\"$job\",node=\"$node\"}) / (node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"} )) * 100", + "format": "time_series", + "hide": true, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "100 - ((node_memory_MemAvailable_bytes{job=\"$job\",node=\"$node\"} * 100) / node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"})", + "format": "time_series", + "hide": false, + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "B", + "step": 240 + } + ], + "title": "RAM Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Used Swap", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 4, + "y": 4 + }, + "id": 21, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "((node_memory_SwapTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_SwapFree_bytes{job=\"$job\",node=\"$node\"}) / (node_memory_SwapTotal_bytes{job=\"$job\",node=\"$node\"} )) * 100", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "SWAP Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Used Disk", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 60 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 85 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 6, + "y": 4 + }, + "id": 324, + "links": [], + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "((topk(1,node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'})- topk(1,node_filesystem_free_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'})) / topk(1,node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'})) * 100", + "instant": true, + "intervalFactor": 1, + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Used", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": 10 + }, + { + "color": "dark-red", + "value": 30 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 8, + "y": 4 + }, + "hideTimeOverride": true, + "id": 435, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "irate(node_netstat_Tcp_RetransSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval]) / (irate(node_netstat_Tcp_InSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval]) + irate(node_netstat_Tcp_OutSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval]))", + "format": "time_series", + "instant": true, + "intervalFactor": 1, + "legendFormat": "RetransSegs", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Tcp Retrans", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": 50 + }, + { + "color": "dark-red", + "value": 90 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 10, + "y": 4 + }, + "hideTimeOverride": true, + "id": 437, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(irate(node_disk_io_time_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval]))", + "instant": true, + "intervalFactor": 1, + "legendFormat": "__auto", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Disk Busy", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "decimals": 1, + "mappings": [ + { + "options": { + "match": "null", + "result": { + "text": "N/A" + } + }, + "type": "special" + } + ], + "max": 1, + "min": 0, + "thresholds": { + "mode": "percentage", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "semi-dark-orange", + "value": 30 + }, + { + "color": "red", + "value": 60 + }, + { + "color": "dark-red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 2, + "x": 12, + "y": 4 + }, + "hideTimeOverride": true, + "id": 15, + "links": [], + "maxDataPoints": 100, + "options": { + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "exemplar": false, + "expr": "(sum(irate(node_network_transmit_bytes_total{job=\"$job\",node=\"$node\",device=~\"$netdevices\"}[$__rate_interval])) + sum(irate(node_network_receive_bytes_total{job=\"$job\",node=\"$node\",device=~\"$netdevices\"}[$__rate_interval])))/(sum(node_network_speed_bytes{job=\"$job\",node=\"$node\",device=~\"$netdevices\"})*2)", + "instant": true, + "intervalFactor": 1, + "legendFormat": "__auto", + "range": false, + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "gauge" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic memory usage", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "SWAP Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap Used" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Cache + Buffer" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Available" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#DEDAF7", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 8 + }, + "id": 78, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Total", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_MemFree_bytes{job=\"$job\",node=\"$node\"} - (node_memory_Cached_bytes{job=\"$job\",node=\"$node\"} + node_memory_Buffers_bytes{job=\"$job\",node=\"$node\"} + node_memory_SReclaimable_bytes{job=\"$job\",node=\"$node\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "RAM Used", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Cached_bytes{job=\"$job\",node=\"$node\"} + node_memory_Buffers_bytes{job=\"$job\",node=\"$node\"} + node_memory_SReclaimable_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Cache + Buffer", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemFree_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "RAM Free", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "(node_memory_SwapTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_SwapFree_bytes{job=\"$job\",node=\"$node\"})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SWAP Used", + "refId": "E", + "step": 240 + } + ], + "title": "Memory ", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 8 + }, + "id": 431, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_written_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Disk space used of all filesystems mounted", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 8 + }, + "id": 152, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "100 - ((node_filesystem_avail_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'} * 100) / node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'})", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Disk Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Basic network info per interface", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_eth2" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CCA300", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "recv_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_bytes_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_drop_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#967302", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "trans_errs_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 8 + }, + "id": 74, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_receive_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "recv {{device}}", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_transmit_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "trans {{device}} ", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 15 + }, + "id": 327, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 70, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Idle - Waiting for something to happen" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Iowait - Waiting for I/O to complete" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Irq - Servicing interrupts" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Nice - Niced processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Softirq - Servicing softirqs" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Steal - Time spent in other operating systems when running in a virtualized environment" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCE2DE", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "System - Processes executing in kernel mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "User - Normal processes executing in user mode" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#5195CE", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 16 + }, + "id": 3, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "sortBy": "Mean", + "sortDesc": true, + "width": 250 + }, + "tooltip": { + "mode": "multi", + "sort": "desc" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"system\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "System - Processes executing in kernel mode", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"user\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "User - Normal processes executing in user mode", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"nice\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Nice - Niced processes executing in user mode", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"iowait\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Iowait - Waiting for I/O to complete", + "range": true, + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"irq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Irq - Servicing interrupts", + "range": true, + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"softirq\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Softirq - Servicing softirqs", + "range": true, + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"steal\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Steal - Time spent in other operating systems when running in a virtualized environment", + "range": true, + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "sum by(instance) (irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\", mode=\"idle\"}[$__rate_interval])) / on(instance) group_left sum by (instance)((irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])))", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Idle - Waiting for something to happen", + "range": true, + "refId": "J", + "step": 240 + } + ], + "title": "CPU Modes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in system mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 16 + }, + "id": 341, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"system\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU System Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in IDLE mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 16 + }, + "id": 334, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"idle\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Idle Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in system mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 368, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"user\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU User Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in iowait mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 335, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"iowait\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU IOWait Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in iqr mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 338, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"irq\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Irq Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in nice mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 337, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"nice\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Nice Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in softirq mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 336, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"softirq\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Softirq Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "the time each CPU spend in steal mode in every default interval", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 342, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_cpu_seconds_total{job=\"$job\",node=\"$node\",mode=\"steal\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "CPU Steal Time", + "type": "timeseries" + } + ], + "title": "CPU per Core", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 266, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap - Swap memory usage" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused - Free memory unassigned" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Hardware Corrupted - *./" + }, + "properties": [ + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 17 + }, + "id": 24, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_MemFree_bytes{job=\"$job\",node=\"$node\"} - node_memory_Buffers_bytes{job=\"$job\",node=\"$node\"} - node_memory_Cached_bytes{job=\"$job\",node=\"$node\"} - node_memory_Slab_bytes{job=\"$job\",node=\"$node\"} - node_memory_PageTables_bytes{job=\"$job\",node=\"$node\"} - node_memory_SwapCached_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Apps - Memory used by user-space applications", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_PageTables_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "PageTables - Memory used to map between virtual and physical memory addresses", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SwapCached_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SwapCache - Memory that keeps track of pages that have been fetched from swap but not yet been modified", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Slab_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Slab - Memory used by the kernel to cache data structures for its own use (caches like inode, dentry, etc)", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Cached_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Cache - Parked file data (file content) cache", + "refId": "E", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Buffers_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Buffers - Block device (e.g. harddisk) cache", + "refId": "F", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_MemFree_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Unused - Free memory unassigned", + "refId": "G", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "(node_memory_SwapTotal_bytes{job=\"$job\",node=\"$node\"} - node_memory_SwapFree_bytes{job=\"$job\",node=\"$node\"})", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Swap - Swap space used", + "refId": "H", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HardwareCorrupted_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working", + "refId": "I", + "step": 240 + } + ], + "title": "Memory Stack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 17 + }, + "id": 136, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Inactive_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Inactive - Memory which has been less recently used. It is more eligible to be reclaimed for other purposes", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Active_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Active - Memory that has been used more recently and usually not reclaimed unless absolutely necessary", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Active / Inactive", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 17 + }, + "id": 430, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Inactive_file_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_file - File-backed memory on inactive LRU list", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Inactive_anon_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Inactive_anon - Anonymous and swap cache on inactive LRU list, including tmpfs (shmem)", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Active_file_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_file - File-backed memory on active LRU list", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Active_anon_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Active_anon - Anonymous and swap cache on active least-recently-used (LRU) list, including tmpfs", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Active / Inactive Detail", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 128, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap1G_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "DirectMap1G - Amount of pages mapped as this size", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap2M_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap2M - Amount of pages mapped as this size", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_DirectMap4k_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "DirectMap4K - Amount of pages mapped as this size", + "refId": "C", + "step": 240 + } + ], + "title": "Memory DirectMap", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*CommitLimit - *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 135, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Committed_AS_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Committed_AS - Amount of memory presently allocated on the system", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_CommitLimit_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "CommitLimit - Amount of memory currently available to be allocated on the system", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Committed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 138, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Mapped_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Mapped - Used memory in mapped pages files which have been mapped, such as libraries", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Shmem_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Shmem - Used shared memory (shared between several processes, thus including RAM disks)", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_ShmemHugePages_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemHugePages - Memory used by shared memory (shmem) and tmpfs allocated with huge pages", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_ShmemPmdMapped_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ShmemPmdMapped - Amount of shared (shmem/tmpfs) memory backed by huge pages", + "refId": "D", + "step": 240 + } + ], + "title": "Memory Shared and Mapped", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 137, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Unevictable_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Unevictable - Amount of unevictable memory that can't be swapped out for a variety of reasons", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Mlocked_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "MLocked - Size of pages locked to memory using the mlock() system call", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Unevictable and MLocked", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "KernelStack - Kernel memory stack. This is not reclaimable" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 160, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_KernelStack_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "KernelStack - Kernel memory stack. This is not reclaimable", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Percpu_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PerCPU - Per CPU memory allocated dynamically by loadable modules", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Kernel per CPU", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 130, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Writeback_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Writeback - Memory which is actively being written back to disk", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_WritebackTmp_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "WritebackTmp - Memory used by FUSE for temporary writeback buffers", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Dirty_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Dirty - Memory which is waiting to get written back to the disk", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_NFS_Unstable_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "NFS Unstable - NFS memory blocks waiting to be written into storage", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Memory Writeback and Dirty", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 35 + }, + "id": 131, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SUnreclaim_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SUnreclaim - Part of Slab, that cannot be reclaimed on memory pressure", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_SReclaimable_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "SReclaimable - Part of Slab, that might be reclaimed, such as caches", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Slab_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Slab Total - total size of slab memory ", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory Slab", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 35 + }, + "id": 70, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocChunk_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocChunk - Largest contiguous block of vmalloc area which is free", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocTotal_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocTotal - Total size of vmalloc memory area", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_VmallocUsed_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "VmallocUsed - Amount of vmalloc area which is used", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Vmalloc", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "=", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Inactive *./" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 35 + }, + "id": 129, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_AnonHugePages_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonHugePages - Memory in anonymous huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_AnonPages_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "AnonPages - Memory in user pages not backed by files", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Anonymous", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 41 + }, + "id": 71, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_HugePages_Total{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages - Total size of the pool of huge pages", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_memory_Hugepagesize_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Hugepagesize - Huge Page size", + "refId": "B", + "step": 240 + } + ], + "title": "Memory HugePages Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made" + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 41 + }, + "id": 428, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Free{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Free - Huge pages in the pool that are not yet allocated", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Rsvd{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Rsvd - Huge pages for which a commitment to allocate from the pool has been made, but no allocation has yet been made", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_HugePages_Surp{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "HugePages_Surp - Huge pages in the pool above the value in /proc/sys/vm/nr_hugepages", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Memory HugePages Counter", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 41 + }, + "id": 426, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_memory_Bounce_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Bounce - Memory used for block device bounce buffers", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Memory Bounce", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Memory Meminfo", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 370, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 18 + }, + "id": 176, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgpgin_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesin - Page in operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgpgout_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pagesout - Page out operations", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*out/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 18 + }, + "id": 22, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pswpin_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpin - Pages swapped in", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pswpout_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pswpout - Pages swapped out", + "refId": "B", + "step": 240 + } + ], + "title": "Memory Pages Swap In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Apps" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#629E51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A437C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Hardware Corrupted - Amount of RAM that the kernel identified as corrupted / not working" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#CFFAFF", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "RAM_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#806EB7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#2F575E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Unused" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Pgfault - Page major and minor fault operations" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + }, + { + "id": "custom.stacking", + "value": { + "group": false, + "mode": "normal" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 18 + }, + "id": 175, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 350 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgfault_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgfault - Page major and minor fault operations", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgmajfault_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgmajfault - Major page fault operations", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_pgfault_total{job=\"$job\",node=\"$node\"}[$__rate_interval]) - irate(node_vmstat_pgmajfault_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Pgminfault - Minor page fault operations", + "refId": "C", + "step": 240 + } + ], + "title": "Memory Page Faults", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "Active" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#99440A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Buffers" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#58140C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6D1F62", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Cached" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Committed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#508642", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Dirty" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Free" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#B7DBAB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Inactive" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Mapped" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "PageTables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Page_Tables" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Slab_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Swap_Cache" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#C15C17", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#511749", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total RAM + Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#052B51", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "Total Swap" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "VmallocUsed" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 307, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_vmstat_oom_kill_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "oom killer invocations ", + "refId": "A", + "step": 240 + } + ], + "title": "OOM Killer", + "type": "timeseries" + } + ], + "title": "Memory Vmstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 18 + }, + "id": 386, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 19 + }, + "id": 260, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_timex_estimated_error_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Estimated error in seconds", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_offset_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time offset in between local system and reference clock", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_maxerror_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum error in seconds", + "refId": "C", + "step": 240 + } + ], + "title": "Time Synchronized Drift", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 19 + }, + "id": 291, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_loop_time_constant{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Phase-locked loop time adjust", + "refId": "A", + "step": 240 + } + ], + "title": "Time PLL Adjust", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Variation*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 19 + }, + "id": 168, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_sync_status{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Is clock synchronized to a reliable server (1 = yes, 0 = no)", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_frequency_adjustment_ratio{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Local clock frequency adjustment", + "refId": "B", + "step": 240 + } + ], + "title": "Time Synchronized Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 25 + }, + "id": 294, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_tick_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Seconds between clock ticks", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_timex_tai_offset_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "International Atomic Time (TAI) offset", + "refId": "B", + "step": 240 + } + ], + "title": "Time Misc", + "type": "timeseries" + } + ], + "title": "System Time Synchronize", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 19 + }, + "id": 376, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 20 + }, + "id": 62, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_procs_running{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Processes in runnable state", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_procs_blocked{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes blocked waiting for I/O to complete", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "Processes Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "decbytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max.*/" + }, + "properties": [ + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 20 + }, + "id": 345, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "process_virtual_memory_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Processes virtual memory size", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "process_resident_memory_max_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of resident memory available", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "process_virtual_memory_max_bytes{instance=\"$node\"}", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum amount of virtual memory available", + "range": true, + "refId": "D", + "step": 240 + } + ], + "title": "Processes Memory", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 20 + }, + "id": 148, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_forks_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes forks second", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Forks", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 26 + }, + "id": 379, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_processes_state{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes in {{state}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 26 + }, + "id": 378, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_processes_pids{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Processes Pids", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Processes Pid Number", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 26 + }, + "id": 377, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_processes_max_threads{job=\"$job\",node=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum number of threads", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_processes_threads{job=\"$job\",node=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Allocated threads in system", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Threads Number and Limit", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 32 + }, + "id": 380, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_processes_threads_state{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Threads in {{thread_state}}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Threads States", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "seconds", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*waiting.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 32 + }, + "id": 388, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_running_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent running a process", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_waiting_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }} - seconds spent by processing waiting for this CPU", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Process Schedule Stats Running / Waiting", + "type": "timeseries" + } + ], + "title": "System Processes", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 20 + }, + "id": 333, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 21 + }, + "id": 7, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load1{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 1m", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load5{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 5m", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_load15{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Load 15m", + "refId": "C", + "step": 240 + } + ], + "title": "System Load", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Max*./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 21 + }, + "id": 64, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "process_max_fds{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Maximum open file descriptors", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "process_open_fds{instance=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Open file descriptors", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Process File Descriptors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 21 + }, + "id": 8, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_context_switches_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Context switches", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_intr_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "Interrupts", + "refId": "B", + "step": 240 + } + ], + "title": "Context Switches / Interrupts", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 27 + }, + "id": 306, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_schedstat_timeslices_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{ cpu }}", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Schedule Timeslices Executed by each cpu", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 27 + }, + "id": 390, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(process_cpu_seconds_total{instance=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Time spend", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Process CPU Time Spend", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 27 + }, + "id": 151, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_entropy_available_bits{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Entropy available to random number generators", + "refId": "A", + "step": 240 + } + ], + "title": "Entropy", + "type": "timeseries" + } + ], + "title": "System Misc", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 21 + }, + "id": 329, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number (after merges) of I/O requests completed per second for the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 22 + }, + "id": 9, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_reads_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "intervalFactor": 4, + "legendFormat": "{{device}} - Reads completed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_writes_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Writes completed", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Completed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of bytes read from or written to the device per second", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 22 + }, + "id": 433, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_written_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of bytes read from or written to the device per IO", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 22 + }, + "id": 33, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])/ irate(node_disk_reads_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read bytes", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_written_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])/ irate(node_disk_writes_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Written bytes", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average R/W Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The average time for requests issued to the device to be served. This includes the time spent by the requests in queue and the time spent servicing them.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 28 + }, + "id": 37, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_read_time_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval]) / irate(node_disk_reads_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Read wait time avg", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_write_time_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval]) / irate(node_disk_writes_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write wait time avg", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Disk Average Wait Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of read and write requests merged per second that were queued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Read.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 28 + }, + "id": 133, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_reads_merged_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Read merged", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_writes_merged_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "intervalFactor": 1, + "legendFormat": "{{device}} - Write merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk R/W Merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "iops" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 28 + }, + "id": 301, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discards_completed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - Discards completed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discards_merged_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Discards merged", + "refId": "B", + "step": 240 + } + ], + "title": "Disk IOps Discards completed / merged", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Percentage of elapsed time during which I/O requests were issued to the device (bandwidth utilization for the device). Device saturation occurs when this value is close to 100% for devices serving requests serially. But for devices serving requests in parallel, such as RAID arrays and modern SSDs, this number does not reflect their performance limits.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 30, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 34 + }, + "id": 36, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_disk_io_time_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_discard_time_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - discard", + "refId": "B", + "step": 240 + } + ], + "title": "Time Spent Doing I/Os", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The average queue length of the requests that were issued to the device", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 34 + }, + "id": 35, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_disk_io_time_weighted_seconds_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}}", + "refId": "A", + "step": 240 + } + ], + "title": "Average Queue Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "The number of outstanding requests at the instant the sample was taken. Incremented as requests are given to appropriate struct request_queue and decremented as they finish.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EAB839", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#6ED0E0", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EF843C", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#584477", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda2_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BA43A9", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sda3_.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F4D598", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#0A50A1", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#BF1B00", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdb3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0752D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#962D82", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#614D93", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdc3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#9AC48A", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#65C5DB", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9934E", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#EA6460", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde1.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E0F9D7", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sdd2.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#FCEACA", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*sde3.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F9E2D2", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 34 + }, + "id": 34, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_disk_io_now{job=\"$job\",node=\"$node\"}", + "interval": "", + "intervalFactor": 4, + "legendFormat": "{{device}} - IO now", + "refId": "A", + "step": 240 + } + ], + "title": "Instantaneous Queue Size", + "type": "timeseries" + } + ], + "title": "Disk", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 22 + }, + "id": 372, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 40, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 23 + }, + "id": 156, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'} - node_filesystem_avail_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}}", + "refId": "A", + "step": 240 + } + ], + "title": "Filesystem Space Used", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 23 + }, + "id": 43, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_avail_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Available", + "metric": "", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_free_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_size_bytes{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": true, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Size", + "refId": "C", + "step": 240 + } + ], + "title": "Filesystem Space Available", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 23 + }, + "id": 28, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filefd_maximum{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 4, + "legendFormat": "Max open files", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filefd_allocated{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "Open files", + "refId": "B", + "step": 240 + } + ], + "title": "File Descriptor", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 29 + }, + "id": 219, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_filesystem_files{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - File nodes total", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Total Inodes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 29 + }, + "id": 41, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_filesystem_files_free{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Free file nodes", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Free Inodes", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "/ ReadOnly" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 29 + }, + "id": 44, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_readonly{job=\"$job\",node=\"$node\",device!~'rootfs'}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - ReadOnly", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_filesystem_device_error{job=\"$job\",node=\"$node\",device!~'rootfs',fstype!~'tmpfs'}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{mountpoint}} - Device error", + "refId": "B", + "step": 240 + } + ], + "title": "Filesystem in ReadOnly / Error", + "type": "timeseries" + } + ], + "title": "File System", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 272, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "binBps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "receive_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "receive_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 24 + }, + "id": 438, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_receive_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_transmit_bytes_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "receive_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "receive_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_eth0" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#7EB26D", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "transmit_packets_lo" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#E24D42", + "mode": "fixed" + } + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 24 + }, + "id": 60, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_packets_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_transmit_packets_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic by Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Dropped.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 24 + }, + "id": 290, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_processed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Processed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_dropped_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Dropped", + "refId": "B", + "step": 240 + } + ], + "title": "Softnet Packets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 30 + }, + "id": 232, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_colls_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit colls", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Colls", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 30 + }, + "id": 144, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_fifo_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive fifo", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_fifo_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit fifo", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic FIFO", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 30 + }, + "id": 142, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_errs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive errors", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_network_transmit_errs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit errors", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 36 + }, + "id": 141, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_compressed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive compressed", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_compressed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit compressed", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Compressed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 36 + }, + "id": 145, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_frame_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive frame", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Frame", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 36 + }, + "id": 143, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_drop_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive drop", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_drop_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Transmit drop", + "refId": "B", + "step": 240 + } + ], + "title": "Network Traffic Drop", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "pps" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Trans.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 42 + }, + "id": 146, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_receive_multicast_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Receive multicast", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Multicast", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 42 + }, + "id": 231, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_network_transmit_carrier_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{device}} - Statistic transmit_carrier", + "refId": "A", + "step": 240 + } + ], + "title": "Network Traffic Carrier", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 42 + }, + "id": 310, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_softnet_times_squeezed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "CPU {{cpu}} - Squeezed", + "refId": "A", + "step": 240 + } + ], + "title": "Softnet Out of Quota", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 48 + }, + "id": 309, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_up{operstate=\"up\",job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{interface}} - Operational state UP", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_carrier{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "instant": false, + "legendFormat": "{{device}} - Physical link state", + "refId": "B" + } + ], + "title": "Network Operational Status", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "NF conntrack limit" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 48 + }, + "id": 61, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_nf_conntrack_entries{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack entries - Number of currently allocated flow entries for connection tracking", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_nf_conntrack_entries_limit{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "NF conntrack limit - Maximum size of connection tracking table", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "NF Contrack", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 48 + }, + "id": 424, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_arp_entries{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - ARP entries", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ARP Entries", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "none" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 54 + }, + "id": 289, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_transmit_queue_length{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Interface transmit queue length", + "refId": "A", + "step": 240 + } + ], + "title": "Queue Length", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 54 + }, + "id": 280, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_speed_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Speed", + "refId": "A", + "step": 240 + } + ], + "title": "Speed", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "decimals": 0, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 54 + }, + "id": 288, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_network_mtu_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "{{ device }} - Bytes", + "refId": "A", + "step": 240 + } + ], + "title": "MTU", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "refId": "A" + } + ], + "title": "Network Traffic", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 374, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 25 + }, + "id": 394, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_sockstat_TCP_alloc{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_alloc - Allocated sockets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_inuse{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_inuse - Tcp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_mem{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_mem - Used memory for tcp", + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_orphan{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_orphan - Orphan sockets", + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_tw{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCP_tw - Sockets waiting close", + "refId": "E", + "step": 240 + } + ], + "title": "Sockstat TCP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 25 + }, + "id": 396, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_sockstat_UDPLITE_inuse{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDPLITE_inuse - Udplite sockets currently in use", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_inuse{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_inuse - Udp sockets currently in use", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_mem{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "UDP_mem - Used memory for udp", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat UDP", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 25 + }, + "id": 392, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_sockstat_FRAG_inuse{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_inuse - Frag sockets currently in use", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_RAW_inuse{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RAW_inuse - Raw sockets currently in use", + "refId": "C", + "step": 240 + } + ], + "title": "Sockstat FRAG / RAW", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 31 + }, + "id": 220, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_TCP_mem_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - TCP sockets in that state", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_UDP_mem_bytes{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "mem_bytes - UDP sockets in that state", + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_FRAG_memory{job=\"$job\",node=\"$node\"}", + "interval": "", + "intervalFactor": 1, + "legendFormat": "FRAG_memory - Used memory for frag", + "refId": "C" + } + ], + "title": "Sockstat Memory Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 31 + }, + "id": 126, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_sockstat_sockets_used{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Sockets_used - Sockets currently in use", + "refId": "A", + "step": 240 + } + ], + "title": "Sockstat Used", + "type": "timeseries" + } + ], + "title": "Network Sockstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 25 + }, + "id": 398, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 26 + }, + "id": 406, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Ip_Forwarding_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Forwarding - IP forwarding", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Netstat IP Forwarding", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 26 + }, + "id": 412, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_ActiveOpens_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "ActiveOpens - TCP connections that have made a direct transition to the SYN-SENT state from the CLOSED state", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_PassiveOpens_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "PassiveOpens - TCP connections that have made a direct transition to the SYN-RCVD state from the LISTEN state", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Direct Transition", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "connections", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*MaxConn *./" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#890F02", + "mode": "fixed" + } + }, + { + "id": "custom.fillOpacity", + "value": 0 + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 26 + }, + "id": 410, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_netstat_Tcp_CurrEstab{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "CurrEstab - TCP connections for which the current state is either ESTABLISHED or CLOSE- WAIT", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_netstat_Tcp_MaxConn{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "MaxConn - Limit on the total number of TCP connections the entity can support (Dynamic is \"-1\")", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 32 + }, + "id": 416, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "InSegs - Segments received, including those received in error. This count includes segments received on currently established connections", + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutSegs - Segments sent, including those on current connections but excluding those containing only retransmitted octets", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "TCP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 32 + }, + "id": 402, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InMsgs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InMsgs - Messages which the entity received. Note that this counter includes all those counted by icmpInErrors", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "irate(node_netstat_Icmp_OutMsgs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutMsgs - Messages which this entity attempted to send. Note that this counter includes all those counted by icmpOutErrors", + "refId": "B", + "step": 240 + } + ], + "title": "ICMP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + }, + { + "matcher": { + "id": "byRegexp", + "options": "/.*Snd.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 32 + }, + "id": 422, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InDatagrams_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InDatagrams - Datagrams received", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_OutDatagrams_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "OutDatagrams - Datagrams sent", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "UDP In / Out", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "octets out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 38 + }, + "id": 408, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true, + "width": 300 + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_InOctets_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InOctets - Received octets", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_IpExt_OutOctets_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "intervalFactor": 1, + "legendFormat": "OutOctets - Sent octets", + "range": true, + "refId": "B", + "step": 240 + } + ], + "title": "Netstat IP In / Out Octets", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 38 + }, + "id": 414, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenOverflows_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenOverflows - Times the listen queue of a socket overflowed", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_ListenDrops_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "ListenDrops - SYNs to LISTEN sockets ignored", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_TCPSynRetrans_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "TCPSynRetrans - SYN-SYN/ACK retransmits to break down retransmissions in SYN, fast/timeout retransmits", + "range": true, + "refId": "C", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_RetransSegs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "RetransSegs - Segments retransmitted - that is, the number of TCP segments transmitted containing one or more previously transmitted octets", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_InErrs_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrs - Segments received in error (e.g., bad TCP checksums)", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Tcp_OutRsts_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "OutRsts - Segments sent with RST flag", + "range": true, + "refId": "F" + } + ], + "title": "TCP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "messages out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Out.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 16, + "y": 38 + }, + "id": 400, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Icmp_InErrors_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - Messages which the entity received but determined as having ICMP-specific errors (bad ICMP checksums, bad length, etc.)", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "ICMP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "datagrams", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 44 + }, + "id": 420, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_InErrors_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "InErrors - UDP Datagrams that could not be delivered to an application", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_NoPorts_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "NoPorts - UDP Datagrams received on a port with no listener", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_UdpLite_InErrors_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "interval": "", + "legendFormat": "InErrors Lite - UDPLite Datagrams that could not be delivered to an application", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_RcvbufErrors_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "RcvbufErrors - UDP buffer errors received", + "range": true, + "refId": "D", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_Udp_SndbufErrors_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "SndbufErrors - UDP buffer errors send", + "range": true, + "refId": "E", + "step": 240 + } + ], + "title": "UDP Errors", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "counter out (-) / in (+)", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*Sent.*/" + }, + "properties": [ + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 44 + }, + "id": 418, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesFailed_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesFailed - Invalid SYN cookies received", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesRecv_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesRecv - SYN cookies received", + "range": true, + "refId": "B", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "irate(node_netstat_TcpExt_SyncookiesSent_total{job=\"$job\",node=\"$node\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "SyncookiesSent - SYN cookies sent", + "range": true, + "refId": "C", + "step": 240 + } + ], + "title": "TCP SynCookie", + "type": "timeseries" + } + ], + "title": "Network Netstat", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 26 + }, + "id": 363, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": -8, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bool_yes_no" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/.*error.*/" + }, + "properties": [ + { + "id": "color", + "value": { + "fixedColor": "#F2495C", + "mode": "fixed" + } + }, + { + "id": "custom.transform", + "value": "negative-Y" + } + ] + } + ] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 0, + "y": 69 + }, + "id": 365, + "links": [], + "options": { + "legend": { + "calcs": [ + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_scrape_collector_success{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape success", + "range": true, + "refId": "A", + "step": 240 + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "expr": "node_textfile_scrape_error{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape textfile error (1 = true)", + "refId": "B", + "step": 240 + } + ], + "title": "Node Exporter Scrape", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 6, + "w": 8, + "x": 8, + "y": 69 + }, + "id": 367, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.0", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "editorMode": "code", + "expr": "node_scrape_collector_duration_seconds{job=\"$job\",node=\"$node\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{collector}} - Scrape duration", + "range": true, + "refId": "A", + "step": 240 + } + ], + "title": "Node Exporter Scrape Time", + "type": "timeseries" + } + ], + "title": "Node Exporter", + "type": "row" + } + ], + "refresh": false, + "revision": 1, + "schemaVersion": 37, + "style": "dark", + "tags": [ + "linux", + "node" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "current": { + "selected": false, + "text": "k3d-k3s-default-server-0", + "value": "k3d-k3s-default-server-0" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(node_uname_info{job=\"$job\"}, node)", + "hide": 0, + "includeAll": false, + "label": "node", + "multi": false, + "name": "node", + "options": [], + "query": { + "query": "label_values(node_uname_info{job=\"$job\"}, node)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + }, + "hide": 2, + "includeAll": false, + "multi": false, + "name": "diskdevices", + "options": [ + { + "selected": true, + "text": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "value": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+" + } + ], + "query": "[a-z]+|nvme[0-9]+n[0-9]+|mmcblk[0-9]+", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "agamotto", + "value": "agamotto" + }, + "hide": 0, + "includeAll": false, + "label": "job", + "multi": false, + "name": "job", + "options": [ + { + "selected": true, + "text": "agamotto", + "value": "agamotto" + } + ], + "query": "agamotto", + "skipUrlSync": false, + "type": "custom" + }, + { + "current": { + "selected": false, + "text": "et.+|en.+", + "value": "et.+|en.+" + }, + "hide": 2, + "includeAll": false, + "label": "netdevices", + "multi": false, + "name": "netdevices", + "options": [ + { + "selected": true, + "text": "et.+|en.+", + "value": "et.+|en.+" + } + ], + "query": "et.+|en.+", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "", + "title": "Node", + "uid": "node6fb1af4132", + "version": 1, + "weekStart": "" +} diff --git a/deploy/helm/dashboards/postgresql-overview.json b/deploy/helm/dashboards/postgresql-deprecated.json similarity index 89% rename from deploy/helm/dashboards/postgresql-overview.json rename to deploy/helm/dashboards/postgresql-deprecated.json index e4276ff32..187ac5454 100644 --- a/deploy/helm/dashboards/postgresql-overview.json +++ b/deploy/helm/dashboards/postgresql-deprecated.json @@ -26,7 +26,7 @@ "fiscalYearStartMonth": 0, "gnetId": 11323, "graphTooltip": 1, - "id": 13, + "id": 14, "links": [ { "asDropdown": false, @@ -141,7 +141,7 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, - "expr": "count(sum by(namespace)(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}))", + "expr": "count(sum by(namespace)(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})) or vector(0)", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -217,7 +217,7 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, - "expr": "count(sum by(namespace, app_kubernetes_io_instance)(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}))", + "expr": "count(sum by(namespace, app_kubernetes_io_instance)(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})) or vector(0)", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -293,7 +293,7 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, - "expr": "sum(rate(pg_stat_database_xact_commit{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) + rate(pg_stat_database_xact_rollback{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]))", + "expr": "sum(rate(pg_stat_database_xact_commit{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) + rate(pg_stat_database_xact_rollback{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) or vector(0)", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -704,7 +704,7 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, - "expr": "count(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} > 0)", + "expr": "count(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} > 0) or vector(0)", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -785,14 +785,14 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "count(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} <= 0)", + "expr": "count(pg_up{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"} <= 0) or vector(0)", "format": "time_series", - "instant": true, + "instant": false, "interval": "1m", "intervalFactor": 1, "legendFormat": "__auto", "metric": "", - "range": false, + "range": true, "refId": "A", "step": 20 } @@ -863,7 +863,7 @@ "datasourceErrors": {}, "editorMode": "code", "errors": {}, - "expr": "sum(rate(pg_stat_statements_calls{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]))", + "expr": "sum(rate(pg_stat_statements_calls{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) or vector(0)", "format": "time_series", "interval": "1m", "intervalFactor": 1, @@ -1417,7 +1417,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "topk(5, pg_stat_statements_calls{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "expr": "topk(5, pg_stat_statements_calls{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"})", "format": "table", "instant": true, "interval": "1m", @@ -1579,7 +1579,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "topk(5, pg_stat_statements_mean_exec_time_seconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "expr": "topk(5, pg_stat_statements_mean_exec_time_seconds{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"})", "format": "table", "instant": true, "interval": "1m", @@ -1693,7 +1693,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1802,7 +1803,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -1925,7 +1927,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2034,7 +2037,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2143,7 +2147,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2252,7 +2257,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2361,7 +2367,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2484,7 +2491,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2532,7 +2540,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "rate(pg_stat_user_tables_idx_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) > 0", + "expr": "rate(pg_stat_user_tables_idx_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "1m", @@ -2595,7 +2603,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2643,7 +2652,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "rate(pg_stat_user_tables_idx_tup_fetch{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) / rate(pg_stat_user_tables_idx_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) > 0", + "expr": "rate(pg_stat_user_tables_idx_tup_fetch{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) / rate(pg_stat_user_tables_idx_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "1m", @@ -2706,7 +2715,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2754,7 +2764,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "rate(pg_stat_user_tables_seq_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) > 0", + "expr": "rate(pg_stat_user_tables_seq_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "1m", @@ -2817,7 +2827,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2865,7 +2876,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "rate(pg_stat_user_tables_seq_tup_read{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) / rate(pg_stat_user_tables_seq_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) > 0", + "expr": "rate(pg_stat_user_tables_seq_tup_read{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval]) / rate(pg_stat_user_tables_seq_scan{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}[$__rate_interval])", "format": "time_series", "instant": false, "interval": "1m", @@ -2928,7 +2939,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -2976,7 +2988,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "pg_stat_user_tables_n_live_tup{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"} > 0", + "expr": "pg_stat_user_tables_n_live_tup{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}", "format": "time_series", "instant": false, "interval": "1m", @@ -3039,7 +3051,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3087,7 +3100,7 @@ "editorMode": "code", "errors": {}, "exemplar": false, - "expr": "pg_stat_user_tables_n_dead_tup{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"} > 0", + "expr": "pg_stat_user_tables_n_dead_tup{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\"}", "format": "time_series", "instant": false, "interval": "1m", @@ -3164,7 +3177,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3273,7 +3287,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3382,7 +3397,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3491,7 +3507,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3636,7 +3653,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3745,7 +3763,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3854,7 +3873,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -3963,7 +3983,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4170,7 +4191,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4279,7 +4301,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4388,7 +4411,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4497,7 +4521,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4606,7 +4631,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4791,7 +4817,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -4919,7 +4946,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5061,7 +5089,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5170,7 +5199,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5293,7 +5323,8 @@ "mode": "absolute", "steps": [ { - "color": "green" + "color": "green", + "value": null }, { "color": "red", @@ -5357,6 +5388,533 @@ ], "title": "Database Size", "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 459, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 486, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(pg_replication_is_master{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace, app_kubernetes_io_instance, pod)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Master Role", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 39 + }, + "id": 484, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum (pg_replication_slots_pg_wal_lsn_diff{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\"}) by (namespace, app_kubernetes_io_instance, slot_name)", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{slot_name}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Lag Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 47 + }, + "id": 483, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(pg_replication_lag{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace, app_kubernetes_io_instance, pod) * 1000", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Lag Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 47 + }, + "id": 463, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(pg_replication_slots_active{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\"}) by (namespace, app_kubernetes_io_instance)", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Replication Slots", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 55 + }, + "id": 485, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "(time() - sum(pg_stat_replication_reply_time{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\"}) by (namespace, app_kubernetes_io_instance, application_name)) < bool 2000", + "format": "time_series", + "interval": "1m", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{application_name}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Status", + "type": "timeseries" + } + ], + "title": "Replication", + "type": "row" } ], "refresh": "", @@ -5565,8 +6123,8 @@ "type": "timepicker" }, "timezone": "", - "title": "PostgreSQL", - "uid": "5UxloIJVk", + "title": "[Deprecated] PostgreSQL", + "uid": "5UxloIJVk_deprecated", "version": 1, "weekStart": "" } diff --git a/deploy/helm/dashboards/postgresql.json b/deploy/helm/dashboards/postgresql.json new file mode 100644 index 000000000..c1724d0f1 --- /dev/null +++ b/deploy/helm/dashboards/postgresql.json @@ -0,0 +1,8276 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 11323, + "graphTooltip": 0, + "id": 25, + "links": [ + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 382, + "panels": [], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Summary", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 1 + }, + "id": 13, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "count(sum by(namespace)(pg_up{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})) or vector(0)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Namespaces", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 1 + }, + "id": 424, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "count(sum by(namespace,app_kubernetes_io_instance)(pg_up{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})) or vector(0)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{label_name}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Clusters", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 1 + }, + "id": 442, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum(rate(pg_stat_database_xact_commit_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval]) + rate(pg_stat_database_xact_rollback_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])) or vector(0)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{label_name}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Tps", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "filterable": false, + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "instances" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 5, + "x": 9, + "y": 1 + }, + "id": 422, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count by(short_version)(pg_static{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{short_version}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Cluster Versions", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": {}, + "renameByName": { + "Value": "instances", + "short_version": "version" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "filterable": false, + "inspect": false + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + } + ] + } + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "instance" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "PostgreSQL Instance: ${__data.fields.cluster} | ${__data.fields.instance}", + "url": "/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-node=All&var-namespace=${__data.fields.namespace}&var-pod=${__data.fields.instance}&var-container=All" + } + ] + }, + { + "id": "custom.align", + "value": "center" + }, + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "uptime" + }, + "properties": [ + { + "id": "unit", + "value": "s" + }, + { + "id": "thresholds", + "value": { + "mode": "absolute", + "steps": [ + { + "color": "dark-red", + "value": null + }, + { + "color": "dark-yellow", + "value": 60 + }, + { + "color": "dark-green", + "value": 120 + } + ] + } + }, + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "namespace" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "cluster" + }, + "properties": [ + { + "id": "custom.filterable", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 10, + "x": 14, + "y": 1 + }, + "id": 428, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "enablePagination": false, + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "uptime" + } + ] + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod) (pg_up{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "avg by(namespace,app_kubernetes_io_instance,pod) (time() - pg_postmaster_start_time_seconds{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "table", + "hide": false, + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": false, + "refId": "B", + "step": 20 + } + ], + "title": "Cluster Instances", + "transformations": [ + { + "id": "joinByField", + "options": { + "byField": "pod", + "mode": "outer" + } + }, + { + "id": "organize", + "options": { + "excludeByName": { + "Time 1": true, + "Time 2": true, + "Value #A": true, + "app_kubernetes_io_instance 2": true, + "namespace 2": true + }, + "indexByName": { + "Time 1": 3, + "Time 2": 5, + "Value #A": 4, + "Value #B": 8, + "app_kubernetes_io_instance 1": 1, + "app_kubernetes_io_instance 2": 6, + "namespace 1": 0, + "namespace 2": 7, + "pod": 2 + }, + "renameByName": { + "Value #B": "uptime", + "app_kubernetes_io_instance 1": "cluster", + "namespace 1": "namespace", + "pod": "instance" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 0, + "y": 5 + }, + "id": 421, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "count(pg_up{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"} > 0) or vector(0)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Ups", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + }, + { + "color": "dark-red", + "value": 0.5 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 3, + "y": 5 + }, + "id": 420, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "count(pg_up{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"} <= 0) or vector(0)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "__auto", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Downs", + "transformations": [], + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 4, + "w": 3, + "x": 6, + "y": 5 + }, + "id": 443, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "colorMode": "value", + "fieldOptions": { + "calcs": [ + "lastNotNull" + ] + }, + "graphMode": "area", + "justifyMode": "auto", + "orientation": "horizontal", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(rate(pg_stat_statements_stats_calls_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])) or vector(0)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{label_name}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Total Qps", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "tps" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 0, + "y": 9 + }, + "id": 427, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(5, sum by(namespace,app_kubernetes_io_instance) (rate(pg_stat_database_xact_commit_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval]) + rate(pg_stat_database_xact_rollback_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])))", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top5 Transactions Clusters", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value": 3, + "app_kubernetes_io_instance": 2, + "namespace": 1 + }, + "renameByName": { + "Value": "tps", + "app_kubernetes_io_instance": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "qps" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 6, + "y": 9 + }, + "id": 438, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(5, sum by(namespace,app_kubernetes_io_instance) (rate(pg_stat_statements_stats_calls_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])))", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top5 Queries Clusters", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value": 3, + "app_kubernetes_io_instance": 2, + "namespace": 1 + }, + "renameByName": { + "Value": "qps", + "app_kubernetes_io_instance": "cluster" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "connections" + }, + "properties": [ + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 12, + "y": 9 + }, + "id": 429, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(5, sum by(namespace,app_kubernetes_io_instance) (pg_stat_database_numbackends{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}))", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top5 Connections Clusters", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value": 3, + "app_kubernetes_io_instance": 2, + "namespace": 1 + }, + "renameByName": { + "Value": "connections", + "app_kubernetes_io_instance": "cluster", + "namespace": "namespace" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "size" + }, + "properties": [ + { + "id": "unit", + "value": "bytes" + }, + { + "id": "custom.displayMode", + "value": "lcd-gauge" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 440, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(5, sum by(namespace,app_kubernetes_io_instance) (pg_database_size_bytes{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}))", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top5 Disk Size Clusters", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true + }, + "indexByName": { + "Time": 0, + "Value": 3, + "app_kubernetes_io_instance": 2, + "namespace": 1 + }, + "renameByName": { + "Value": "size", + "app_kubernetes_io_instance": "cluster", + "namespace": "namespace" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "queryid" + }, + "properties": [ + { + "id": "unit", + "value": "long" + }, + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "avg exec time" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "instance" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "", + "url": "/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-node=All&var-namespace=${__data.fields.namespace}&var-pod=${__data.fields.instance}&var-container=All" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "query" + }, + "properties": [ + { + "id": "custom.minWidth", + "value": 500 + }, + { + "id": "custom.align", + "value": "left" + }, + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.inspect", + "value": true + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 441, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [] + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(10, pg_stat_statements_by_mean_exec_time_mean_exec_time_seconds{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"})", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top10 Avg Exec Time Statements", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "app_kubernetes_io_component": true, + "app_kubernetes_io_component_name": true, + "app_kubernetes_io_managed_by": true, + "app_kubernetes_io_name": true, + "app_kubernetes_io_version": true, + "apps_kubeblocks_io_component_name": true, + "helm_sh_chart": true, + "instance": true, + "job": true, + "node": true, + "receiver": true, + "rolname": true, + "server": true, + "service": true + }, + "indexByName": { + "Time": 1, + "Value": 12, + "__name__": 2, + "app_kubernetes_io_component": 15, + "app_kubernetes_io_instance": 3, + "app_kubernetes_io_managed_by": 4, + "app_kubernetes_io_name": 5, + "app_kubernetes_io_version": 16, + "apps_kubeblocks_io_component_name": 17, + "datname": 7, + "helm_sh_chart": 18, + "instance": 8, + "job": 9, + "namespace": 0, + "node": 19, + "pod": 6, + "query": 13, + "queryid": 14, + "receiver": 20, + "rolname": 10, + "server": 11, + "service": 21 + }, + "renameByName": { + "Value": "avg exec time", + "app_kubernetes_io_instance": "cluster", + "pod": "instance", + "rolname": "tablename" + } + } + } + ], + "type": "table" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "custom": { + "align": "center", + "displayMode": "auto", + "inspect": false + }, + "decimals": 2, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "dark-green", + "value": null + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "matcher": { + "id": "byName", + "options": "queryid" + }, + "properties": [ + { + "id": "unit", + "value": "long" + }, + { + "id": "custom.inspect", + "value": true + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "instance" + }, + "properties": [ + { + "id": "links", + "value": [ + { + "targetBlank": true, + "title": "", + "url": "/d/pMEd7m0Mz/cadvisor-exporter?orgId=1&var-node=All&var-namespace=${__data.fields.namespace}&var-pod=${__data.fields.instance}&var-container=All" + } + ] + } + ] + }, + { + "matcher": { + "id": "byName", + "options": "query" + }, + "properties": [ + { + "id": "custom.inspect", + "value": true + }, + { + "id": "custom.displayMode", + "value": "color-text" + }, + { + "id": "custom.align", + "value": "left" + }, + { + "id": "custom.minWidth", + "value": 500 + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 23 + }, + "id": 439, + "interval": "", + "links": [], + "maxDataPoints": 100, + "options": { + "footer": { + "fields": "", + "reducer": [ + "sum" + ], + "show": false + }, + "showHeader": true, + "sortBy": [ + { + "desc": true, + "displayName": "count" + } + ] + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "10m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "topk(10, increase(pg_stat_statements_by_calls_calls_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[10m]))", + "format": "table", + "instant": true, + "interval": "", + "intervalFactor": 1, + "legendFormat": "", + "metric": "", + "range": false, + "refId": "A", + "step": 20 + } + ], + "title": "Top10 Most Called Statements Within 10 Minites", + "transformations": [ + { + "id": "organize", + "options": { + "excludeByName": { + "Time": true, + "__name__": true, + "app_kubernetes_io_component": true, + "app_kubernetes_io_component_name": true, + "app_kubernetes_io_managed_by": true, + "app_kubernetes_io_name": true, + "app_kubernetes_io_version": true, + "apps_kubeblocks_io_component_name": true, + "helm_sh_chart": true, + "instance": true, + "job": true, + "node": true, + "receiver": true, + "rolname": true, + "server": true, + "service": true + }, + "indexByName": { + "Time": 1, + "Value": 11, + "app_kubernetes_io_component": 14, + "app_kubernetes_io_instance": 2, + "app_kubernetes_io_managed_by": 3, + "app_kubernetes_io_name": 4, + "app_kubernetes_io_version": 15, + "apps_kubeblocks_io_component_name": 16, + "datname": 6, + "helm_sh_chart": 17, + "instance": 7, + "job": 8, + "namespace": 0, + "node": 18, + "pod": 5, + "query": 12, + "queryid": 13, + "receiver": 19, + "rolname": 9, + "server": 10, + "service": 20 + }, + "renameByName": { + "Value": "count", + "app_kubernetes_io_instance": "cluster", + "pod": "instance", + "rolname": "tablename" + } + } + } + ], + "type": "table" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 30 + }, + "id": 412, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 31 + }, + "id": 413, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "pg_stat_database_numbackends{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Current Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 31 + }, + "id": 414, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "pg_stat_activity_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",state=\"active\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Active Connections", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 39 + }, + "id": 512, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod) (pg_stat_database_numbackends{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}) / on(namespace,app_kubernetes_io_instance,pod) pg_settings_max_connections{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Used Connections Ratio", + "type": "timeseries" + } + ], + "title": "Connections", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 431, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 48 + }, + "id": 432, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "rate(pg_stat_user_tables_idx_scan_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Index Scans Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 48 + }, + "id": 434, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "rate(pg_stat_user_tables_idx_tup_fetch_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval]) / rate(pg_stat_user_tables_idx_scan_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Average Index Tuples Fetch", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 56 + }, + "id": 433, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "rate(pg_stat_user_tables_seq_scan_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Seq Scans Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 56 + }, + "id": 435, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "rate(pg_stat_user_tables_seq_tup_read_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval]) / rate(pg_stat_user_tables_seq_scan_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Average Seq Tuples Read", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 64 + }, + "id": 436, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "pg_stat_user_tables_n_live_tup{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Estimated Live Tuples", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 64 + }, + "id": 437, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "exemplar": false, + "expr": "pg_stat_user_tables_n_dead_tup{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Estimated Dead Tuples", + "type": "timeseries" + } + ], + "title": "Queries Per Table", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 32 + }, + "id": 393, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 73 + }, + "id": 394, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_tup_fetched_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Fetched Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 73 + }, + "id": 395, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_tup_returned_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Returned Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 81 + }, + "id": 396, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_tup_inserted_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Inserted Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 81 + }, + "id": 397, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_tup_updated_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Updated Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 89 + }, + "id": 398, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_tup_deleted_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Deleted Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 89 + }, + "id": 407, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_temp_bytes_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Data Written to Temp Files Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 97 + }, + "id": 406, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_temp_files_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Temp Files Per Second", + "type": "timeseries" + } + ], + "title": "Tuples Per Database", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 33 + }, + "id": 514, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 106 + }, + "id": 389, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_xact_commit_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Commits Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 106 + }, + "id": 390, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_xact_rollback_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Rollbacks Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 114 + }, + "id": 391, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "max without(state) (max_over_time(pg_stat_activity_max_tx_duration{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__interval]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Max Duration of Transactions", + "type": "timeseries" + } + ], + "title": "Transactions Per Database", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 34 + }, + "id": 494, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 123 + }, + "id": 408, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,state) (pg_stat_activity_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{state}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Server Process By State", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 123 + }, + "id": 489, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,backend_type) (pg_stat_activity_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{backend_type}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Server Process By Backend Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 131 + }, + "id": 490, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,wait_event_type) (pg_stat_activity_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{wait_event_type}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Server Process By Wait Event Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 131 + }, + "id": 491, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,wait_event_type,wait_event) (pg_stat_activity_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{wait_event_type}} | {{wait_event}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Server Process By Wait Event", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 139 + }, + "id": 492, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,datname) (pg_stat_activity_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Server Process By Database", + "type": "timeseries" + } + ], + "title": "Server Processes", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 35 + }, + "id": 511, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 148 + }, + "id": 423, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_archiver_archived_count_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "success: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_archiver_failed_count_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "fail: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "WAL Files Per Second (archiver)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 148 + }, + "id": 487, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "pg_wal_log_file_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"} * pg_settings_wal_segment_size_bytes{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "total wal disk size: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "pg_settings_wal_segment_size_bytes{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "wal_segment_size: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "WAL Files Disk Size (archiver)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 156 + }, + "id": 410, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_checkpoint_write_time_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Written Files to disk: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_checkpoint_sync_time_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Files Synchronization to disk: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Checkpoints Time Per Second (bgwriter)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 156 + }, + "id": 415, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_checkpoints_timed_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Scheduled: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_checkpoints_req_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Requested: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Checkpoints Per Second (bgwriter)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 164 + }, + "id": 403, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_buffers_alloc_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Allocated: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_buffers_backend_fsync_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Fsync calls by a backend: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_buffers_backend_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Written directly by backend: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "C", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_buffers_clean_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Written by the background writer: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "D", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_buffers_checkpoint_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Written during checkpoints: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "E", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_bgwriter_maxwritten_clean_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Written stopped by the background writer: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "F", + "step": 20 + } + ], + "title": "Buffers Written Per Second (bgwriter)", + "type": "timeseries" + } + ], + "title": "Background Process", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 36 + }, + "id": 496, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 173 + }, + "id": 499, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,locktype) (pg_locks_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{locktype}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Locks By Lock Type", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 173 + }, + "id": 498, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,mode) (pg_locks_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{mode}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Locks By Mode", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 181 + }, + "id": 501, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod,datname,relation) (pg_locks_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\",locktype=\"relation\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}} | {{relation}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Locks By Relation(Table)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [ + { + "__systemRef": "hideSeriesFrom", + "matcher": { + "id": "byNames", + "options": { + "mode": "exclude", + "names": [ + "Lock Held: default | pg14 | pg14-postgresql-0 " + ], + "prefix": "All except:", + "readOnly": true + } + }, + "properties": [ + { + "id": "custom.hideFrom", + "value": { + "legend": false, + "tooltip": false, + "viz": true + } + } + ] + } + ] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 181 + }, + "id": 500, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod) (pg_locks_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\",granted=\"1\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Lock Held: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum by(namespace,app_kubernetes_io_instance,pod) (pg_locks_detail_count{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\",granted=\"0\"})", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Lock Awaited: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Locks Held or Awaited", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 189 + }, + "id": 497, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "max by(namespace,app_kubernetes_io_instance,pod) (max_over_time(pg_locks_detail_max_wait_age_seconds{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__interval]))", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Max Lock Wait Time (Version >= 14)", + "type": "timeseries" + } + ], + "title": "Locks", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 37 + }, + "id": 503, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 198 + }, + "id": 385, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_records_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "WAL Records Generated Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 198 + }, + "id": 505, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_fpi_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "WAL Full Page Images Generated Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 206 + }, + "id": 506, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_bytes_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "WAL Generated Size Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 206 + }, + "id": 507, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_buffers_full_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "WAL Buffer Full Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 214 + }, + "id": 508, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_write_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "XLogWrite: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_sync_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "XLogFsync: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "WAL Written To Disk Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 214 + }, + "id": 509, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_write_time_seconds_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "XLogWrite: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_wal_wal_sync_time_seconds_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "XLogFsync: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + } + ], + "title": "Time of WAL Written To Disk Per Second", + "type": "timeseries" + } + ], + "title": "WAL (Version >= 14)", + "type": "row" + }, + { + "collapsed": true, + "datasource": { + "uid": "$datasource" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 38 + }, + "id": 383, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 223 + }, + "id": 386, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_confl_bufferpin_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "Pinned buffers: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_confl_deadlock_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Deadlocks: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "B", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_confl_lock_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Lock timeouts: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "C", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_confl_snapshot_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Old snapshots: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "D", + "step": 20 + }, + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_confl_tablespace_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "hide": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "Dropped tablespaces: {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "E", + "step": 20 + } + ], + "title": "Canceled Queries Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 223 + }, + "id": 504, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_conflicts_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Conflicts Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 231 + }, + "id": 384, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_deadlocks_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Deadlocks Per Second", + "type": "timeseries" + } + ], + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "refId": "A" + } + ], + "title": "Conflicts & Deadlocks", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 39 + }, + "id": 400, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 240 + }, + "id": 488, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_blks_hit_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Buffer Cache Read Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 240 + }, + "id": 409, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_blks_read_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Disk Block Read Per Second", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 248 + }, + "id": 401, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_blk_read_time_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Block Read Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 248 + }, + "id": 402, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "rate(pg_stat_database_blk_write_time_total{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}[$__rate_interval])", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Block Write Time", + "type": "timeseries" + } + ], + "title": "Shared Buffers & Blocks", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 40 + }, + "id": 417, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 241 + }, + "id": 418, + "links": [], + "options": { + "legend": { + "calcs": [ + "mean", + "lastNotNull", + "max", + "min" + ], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "pg_database_size_bytes{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",datname=~\"$database\",job=\"$job\"}", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} | {{datname}}", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Disk Size", + "type": "timeseries" + } + ], + "title": "Database Size", + "type": "row" + }, + { + "collapsed": true, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 41 + }, + "id": 459, + "panels": [ + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 266 + }, + "id": 486, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(pg_replication_is_master{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}) by(namespace,app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Master Role", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 266 + }, + "id": 484, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum (pg_replication_slots_pg_wal_lsn_diff{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",job=\"$job\"}) by(namespace,app_kubernetes_io_instance,slot_name)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{slot_name}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Lag Size", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "s" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 274 + }, + "id": 483, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "sum(pg_replication_lag{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\",job=\"$job\"}) by(namespace, app_kubernetes_io_instance,pod)", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Lag Time", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 274 + }, + "id": 463, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "editorMode": "code", + "expr": "sum(pg_replication_slots_active{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",job=\"$job\"}) by(namespace,app_kubernetes_io_instance)", + "interval": "", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}}", + "range": true, + "refId": "A" + } + ], + "title": "Replication Slots", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 20, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 2, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "links": [], + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 282 + }, + "id": 485, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "calculatedInterval": "2m", + "datasource": { + "uid": "$datasource" + }, + "datasourceErrors": {}, + "editorMode": "code", + "errors": {}, + "expr": "(time() - sum(pg_stat_replication_reply_time{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",job=\"$job\"}) by(namespace,app_kubernetes_io_instance,application_name)) < bool 2000", + "format": "time_series", + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{application_name}} ", + "metric": "", + "range": true, + "refId": "A", + "step": 20 + } + ], + "title": "Replication Status", + "type": "timeseries" + } + ], + "title": "Replication", + "type": "row" + } + ], + "refresh": false, + "schemaVersion": 37, + "style": "dark", + "tags": [ + "postgres", + "db" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "data source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(pg_up{job=\"$job\"}, namespace)", + "hide": 0, + "includeAll": true, + "label": "namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(pg_up{job=\"$job\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(pg_up{job=\"$job\"}, app_kubernetes_io_instance)", + "hide": 0, + "includeAll": true, + "label": "cluster", + "multi": true, + "name": "cluster", + "options": [], + "query": { + "query": "label_values(pg_up{job=\"$job\"}, app_kubernetes_io_instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(pg_up{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "hide": 0, + "includeAll": true, + "label": "instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(pg_up{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": true, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(pg_database_size_bytes{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}, datname)", + "hide": 0, + "includeAll": true, + "label": "database", + "multi": true, + "name": "database", + "options": [], + "query": { + "query": "label_values(pg_database_size_bytes{job=\"$job\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}, datname)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "agamotto", + "value": "agamotto" + }, + "hide": 0, + "includeAll": false, + "label": "job", + "multi": false, + "name": "job", + "options": [ + { + "selected": true, + "text": "agamotto", + "value": "agamotto" + } + ], + "query": "agamotto", + "queryValue": "", + "skipUrlSync": false, + "type": "custom" + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "hidden": false, + "notice": false, + "now": true, + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "", + "title": "PostgreSQL", + "uid": "5UxloIJVk", + "version": 1, + "weekStart": "" +} diff --git a/deploy/helm/dashboards/redis-overview.json b/deploy/helm/dashboards/redis.json similarity index 100% rename from deploy/helm/dashboards/redis-overview.json rename to deploy/helm/dashboards/redis.json diff --git a/deploy/helm/dashboards/weaviate-overview.json b/deploy/helm/dashboards/weaviate-overview.json new file mode 100644 index 000000000..44d097394 --- /dev/null +++ b/deploy/helm/dashboards/weaviate-overview.json @@ -0,0 +1,2175 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "datasource", + "uid": "grafana" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "description": "", + "editable": true, + "fiscalYearStartMonth": 0, + "gnetId": 11323, + "graphTooltip": 0, + "id": 10, + "links": [ + { + "asDropdown": false, + "icon": "cloud", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "ApeCloud", + "tooltip": "Improved productivity, cost-efficiency and business continuity.", + "type": "link", + "url": "https://kubeblocks.io/" + }, + { + "asDropdown": false, + "icon": "external link", + "includeVars": false, + "keepTime": false, + "tags": [], + "targetBlank": true, + "title": "KubeBlocks", + "tooltip": "An open-source and cloud-neutral DBaaS with Kubernetes.", + "type": "link", + "url": "https://github.com/apecloud/kubeblocks" + } + ], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 44, + "panels": [], + "title": "Startup Times", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 0, + "y": 1 + }, + "id": 40, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(startup_durations_ms_sum{operation=\"hnsw_read_all_commitlogs\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "interval": "", + "legendFormat": "", + "range": true, + "refId": "A" + } + ], + "title": "Startup all HNSW indexes", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "These times do not represent wall time. Some init processes are running in parallel, so this is CPU time. For a wall clock measurement look at total shard startup time.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 6, + "x": 6, + "y": 1 + }, + "id": 38, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(startup_durations_ms_sum{operation=\"lsm_startup_bucket\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "interval": "", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "Startup all LSM buckets", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "This represents the total wall time per shard.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 42, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(startup_durations_ms_sum{operation=\"shard_total_init\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"})", + "interval": "", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Total shard Startup", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 9 + }, + "id": 46, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(startup_durations_ms_sum{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(startup_durations_ms_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "{{operation}} ({{class_name}} / {{shard_name}})", + "range": true, + "refId": "A" + } + ], + "title": "Startup durations", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "Bps" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 9 + }, + "id": 48, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(startup_diskio_throughput_sum{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(startup_diskio_throughput_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "{{operation}} ({{class_name}}/{{shard_name}})", + "range": true, + "refId": "A" + } + ], + "title": "Startup Disk I/O ", + "type": "timeseries" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 17 + }, + "id": 36, + "title": "Query", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Number of queries per second for each class", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlBl" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 0, + "y": 18 + }, + "id": 34, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_durations_ms_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Query Rate", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Average query latency per time interval for each class.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 9, + "w": 12, + "x": 12, + "y": 18 + }, + "id": 32, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_durations_ms_sum{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(queries_durations_ms_count{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "legendFormat": "{{class_name}} ", + "range": true, + "refId": "A" + } + ], + "title": "Average Query Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Note the percentiles are calculated from histogram buckets so have an associated error see https://prometheus.io/docs/practices/histograms/#errors-of-quantile-estimation", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-YlRd" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 27 + }, + "id": 30, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "histogram_quantile(0.95, sum(rate(queries_durations_ms_bucket{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod,le))", + "legendFormat": "__auto", + "range": true, + "refId": "A" + } + ], + "title": "95th Percentile Query Latency (Estimated)", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Break down of filtered query latency by operation", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-GrYlRd" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 27 + }, + "id": 28, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_filtered_vector_durations_ms_sum{operation=\"filter\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(queries_filtered_vector_durations_ms_count{operation=\"filter\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "legendFormat": "Filter | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "Filter" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_filtered_vector_durations_ms_sum{operation=\"sort\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod) / avg(rate(queries_filtered_vector_durations_ms_count{operation=\"sort\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Sort | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "Sort" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_filtered_vector_durations_ms_sum{operation=\"vector\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod) / avg(rate(queries_filtered_vector_durations_ms_count{operation=\"vector\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Vector | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "Vector" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(queries_filtered_vector_durations_ms_sum{operation=\"objects\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod) / avg(rate(queries_filtered_vector_durations_ms_count{operation=\"objects\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Objects | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "Objects" + } + ], + "title": "Filtered Query Latency", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 35 + }, + "id": 26, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(concurrent_queries_count{query_type=\"aggregate\", namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "Aggregate", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(concurrent_queries_count{query_type=\"get_graphql\", namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "interval": "", + "legendFormat": "Get (GraphQL)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"get_object\", namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Get (Objects)", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"head_object\", namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Head (Objects)", + "range": true, + "refId": "D" + } + ], + "title": "Concurrent Read Requests", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "axisSoftMin": 0, + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 15, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "smooth", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 35 + }, + "id": 24, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"batch\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Batch (Objects) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"batch_references\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Batch (References) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "D" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(concurrent_queries_count{query_type=\"add_object\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "Add (Objects) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(concurrent_queries_count{query_type=\"update_object\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "interval": "", + "legendFormat": "Update (Objects) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"merge_object\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Merge (Objects) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "E" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"delete_object\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Delete (Objects) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "F" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"add_reference\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Add (Reference) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "G" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"update_reference\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Update (Reference) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "H" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(concurrent_queries_count{query_type=\"delete_reference\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "legendFormat": "Delete (Reference) | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "I" + } + ], + "title": "Concurrent Write Requests", + "type": "timeseries" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 43 + }, + "id": 20, + "title": "Object Operations", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "continuous-RdYlGr" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "ms" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 24, + "x": 0, + "y": 44 + }, + "id": 22, + "maxDataPoints": 25, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.3.6", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(objects_durations_ms_sum{step=\"total\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(objects_durations_ms_count{step=\"total\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod,class_name,shard_name)", + "format": "time_series", + "interval": "", + "legendFormat": "Total | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(objects_durations_ms_sum{step=\"upsert_object_store\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(objects_durations_ms_count{step=\"upsert_object_store\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod,class_name,shard_name)", + "format": "time_series", + "hide": false, + "interval": "", + "legendFormat": "Upsert Object | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "avg(rate(objects_durations_ms_sum{step=\"inverted_total\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval]) / rate(objects_durations_ms_count{step=\"inverted_total\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod,class_name,shard_name)", + "format": "time_series", + "hide": false, + "interval": "", + "legendFormat": "Inverted Index | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "C" + } + ], + "title": "PUT Object (Total)", + "type": "timeseries" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 51 + }, + "id": 12, + "title": "LSM", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Replace is used primarly for object storage.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 52 + }, + "id": 14, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(max_over_time(lsm_segment_size{strategy=\"replace\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by(namespace,app_kubernetes_io_instance,pod,level, unit)", + "legendFormat": "Level {{level}} / {{unit}} ", + "range": true, + "refId": "A" + } + ], + "title": "LSM Strategy Replace by Level (Object Storage)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "MapCollections are used in the inverted index for anything that has a frequency, which is mainly text and string props. Those need a frequency for BM25 calculations.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 57 + }, + "id": 18, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(max_over_time(lsm_segment_size{strategy=\"mapcollection\", namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by(namespace,app_kubernetes_io_instance,pod,level, unit)", + "legendFormat": "Level {{level}} / {{unit}} ", + "range": true, + "refId": "A" + } + ], + "title": "LSM Strategy MapCollection by Level (Inverted with frequency)", + "type": "stat" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "SetCollections are used in the inverted index for anything that does not have a frequency, e.g. numbers, bools, etc.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "thresholds" + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + }, + "unit": "decbytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 5, + "w": 24, + "x": 0, + "y": 62 + }, + "id": 16, + "options": { + "colorMode": "value", + "graphMode": "area", + "justifyMode": "auto", + "orientation": "auto", + "reduceOptions": { + "calcs": [ + "lastNotNull" + ], + "fields": "", + "values": false + }, + "textMode": "auto" + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "expr": "sum(max_over_time(lsm_segment_size{strategy=\"setcollection\",namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by(namespace,app_kubernetes_io_instance,pod,level,unit)", + "legendFormat": "Level {{level}} / {{unit}} ", + "range": true, + "refId": "A" + } + ], + "title": "LSM Strategy SetCollection by Level (Inverted without frequency)", + "type": "stat" + }, + { + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 67 + }, + "id": 2, + "title": "Importing", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 0, + "y": 68 + }, + "id": 4, + "interval": "1", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(vector_index_tombstones{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "instant": false, + "interval": "", + "intervalFactor": 1, + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "refId": "A" + } + ], + "title": "Active Tombstones in HNSW index", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 8, + "w": 12, + "x": 12, + "y": 68 + }, + "id": 6, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(vector_index_tombstone_cleanup_threads{namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "{{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + } + ], + "title": "Active Tombstone Cleanup Threads", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "text", + "value": null + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 76 + }, + "id": 8, + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "pluginVersion": "9.2.4", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": false, + "expr": "sum(max_over_time(vector_index_operations{operation=\"create\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "instant": false, + "interval": "", + "legendFormat": "Vectors Inserted | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(vector_index_operations{operation=\"delete\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "interval": "", + "legendFormat": "Vectors Deleted | {{namespace}} | {{app_kubernetes_io_instance}} | {{pod}}", + "range": true, + "refId": "B" + } + ], + "title": "Vector Index Statistics", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "description": "Count of active segments across all classes and shards, shown by type.", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisCenteredZero": false, + "axisColorMode": "text", + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 0, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineStyle": { + "fill": "solid" + }, + "lineWidth": 1, + "pointSize": 9, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "auto", + "spanNulls": false, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + } + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 76 + }, + "id": 10, + "interval": "1", + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom", + "showLegend": true + }, + "tooltip": { + "mode": "single", + "sort": "none" + } + }, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(lsm_active_segments{strategy=\"replace\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "interval": "", + "legendFormat": "Total \"Replace\" (Object Storage)", + "range": true, + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(lsm_active_segments{strategy=\"mapcollection\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "interval": "", + "legendFormat": "Total \"Map\" (Inverted storage with term frequency)", + "range": true, + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "Prometheus" + }, + "editorMode": "code", + "exemplar": true, + "expr": "sum(max_over_time(lsm_active_segments{strategy=\"setcollection\",namespace=~\"$namespace\", app_kubernetes_io_instance=~\"$cluster\",pod=~\"$instance\"}[$__rate_interval])) by (namespace,app_kubernetes_io_instance,pod)", + "hide": false, + "interval": "", + "legendFormat": "Total \"Set\" (Inverted storage without frequency)", + "range": true, + "refId": "C" + } + ], + "title": "Active LSM Segments", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 37, + "style": "dark", + "tags": [ + "weaviate", + "db" + ], + "templating": { + "list": [ + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "Data Source", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + }, + { + "allValue": ".+", + "current": { + "selected": false, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(go_info{app_kubernetes_io_name=\"weaviate\"}, namespace)", + "hide": 0, + "includeAll": true, + "label": "namespace", + "multi": true, + "name": "namespace", + "options": [], + "query": { + "query": "label_values(go_info{app_kubernetes_io_name=\"weaviate\"}, namespace)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": false, + "text": [ + "All" + ], + "value": [ + "$__all" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(go_info{app_kubernetes_io_name=\"weaviate\"}, app_kubernetes_io_instance)", + "hide": 0, + "includeAll": true, + "label": "cluster", + "multi": true, + "name": "cluster", + "options": [], + "query": { + "query": "label_values(go_info{app_kubernetes_io_name=\"weaviate\"}, app_kubernetes_io_instance)", + "refId": "StandardVariableQuery" + }, + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "allValue": ".+", + "current": { + "selected": false, + "text": [ + "All" + ], + "value": [ + "All" + ] + }, + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "definition": "label_values(go_info{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",app_kubernetes_io_name=\"weaviate\"}, pod)", + "hide": 0, + "includeAll": true, + "label": "instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(go_info{namespace=~\"$namespace\",app_kubernetes_io_instance=~\"$cluster\",app_kubernetes_io_name=\"weaviate\"}, pod)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 5, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + } + ] + }, + "time": { + "from": "now-30m", + "to": "now" + }, + "timepicker": { + "collapse": false, + "enable": true, + "hidden": false, + "notice": false, + "now": true, + "refresh_intervals": [ + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "status": "Stable", + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ], + "type": "timepicker" + }, + "timezone": "", + "title": "Weaviate", + "uid": "dlhv5WQVx", + "version": 6, + "weekStart": "" +} \ No newline at end of file diff --git a/deploy/helm/depend-charts/alertmanager-webhook-adaptor-0.1.4.tgz b/deploy/helm/depend-charts/alertmanager-webhook-adaptor-0.1.4.tgz deleted file mode 100644 index 154b07fd7..000000000 Binary files a/deploy/helm/depend-charts/alertmanager-webhook-adaptor-0.1.4.tgz and /dev/null differ diff --git a/deploy/helm/depend-charts/aws-load-balancer-controller-1.4.8.tgz b/deploy/helm/depend-charts/aws-load-balancer-controller-1.4.8.tgz deleted file mode 100644 index 321f4ef91..000000000 Binary files a/deploy/helm/depend-charts/aws-load-balancer-controller-1.4.8.tgz and /dev/null differ diff --git a/deploy/helm/depend-charts/grafana-6.43.5.tgz b/deploy/helm/depend-charts/grafana-6.43.5.tgz deleted file mode 100644 index 0cca01193..000000000 Binary files a/deploy/helm/depend-charts/grafana-6.43.5.tgz and /dev/null differ diff --git a/deploy/helm/depend-charts/prometheus-15.16.1.tgz b/deploy/helm/depend-charts/prometheus-15.16.1.tgz deleted file mode 100644 index 1275c5c8f..000000000 Binary files a/deploy/helm/depend-charts/prometheus-15.16.1.tgz and /dev/null differ diff --git a/deploy/helm/templates/_helpers.tpl b/deploy/helm/templates/_helpers.tpl index a80c28041..bae173956 100644 --- a/deploy/helm/templates/_helpers.tpl +++ b/deploy/helm/templates/_helpers.tpl @@ -261,4 +261,18 @@ Define addon alertmanager-webhook-adaptor name */}} {{- define "addon.alertmanager-webhook-adaptor.name" -}} {{- print "alertmanager-webhook-adaptor" }} +{{- end }} + +{{/* +Define addon loki name +*/}} +{{- define "addon.loki.name" -}} +{{- print "loki" }} +{{- end }} + +{{/* +Define addon agamotto name +*/}} +{{- define "addon.agamotto.name" -}} +{{- print "agamotto" }} {{- end }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/agamotto-addon.yaml b/deploy/helm/templates/addons/agamotto-addon.yaml new file mode 100644 index 000000000..f268a7002 --- /dev/null +++ b/deploy/helm/templates/addons/agamotto-addon.yaml @@ -0,0 +1,46 @@ +apiVersion: extensions.kubeblocks.io/v1alpha1 +kind: Addon +metadata: + name: agamotto + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + "kubeblocks.io/provider": community + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + description: Agamotto is a high-performance data collection agent with luxuriant function, which inspired by OpenTelemetry. + type: Helm + + helm: + chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/agamotto-0.1.0-beta.2.tgz + installValues: + configMapRefs: + - name: {{ include "addon.agamotto.name" . }}-chart-kubeblocks-values + key: values-kubeblocks-override.yaml + + valuesMapping: + valueMap: + + jsonMap: + tolerations: tolerations + + resources: + cpu: + requests: resources.requests.cpu + limits: resources.limits.cpu + memory: + requests: resources.requests.memory + limits: resources.limits.memory + + defaultInstallValues: + - enabled: true + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + installable: + autoInstall: {{ .Values.agamotto.enabled }} + + diff --git a/deploy/helm/templates/addons/agamotto-values.yaml b/deploy/helm/templates/addons/agamotto-values.yaml new file mode 100644 index 000000000..69ea480c1 --- /dev/null +++ b/deploy/helm/templates/addons/agamotto-values.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "addon.agamotto.name" . }}-chart-kubeblocks-values + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +data: + values-kubeblocks-override.yaml: |- + {{- get ( .Values | toYaml | fromYaml ) "agamotto" | toYaml | nindent 4 }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/alertmanager-webhook-adaptor-addon.yaml b/deploy/helm/templates/addons/alertmanager-webhook-adaptor-addon.yaml index de8ebd43f..56fd5a879 100644 --- a/deploy/helm/templates/addons/alertmanager-webhook-adaptor-addon.yaml +++ b/deploy/helm/templates/addons/alertmanager-webhook-adaptor-addon.yaml @@ -39,6 +39,9 @@ spec: defaultInstallValues: - replicas: 1 + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ .Values.prometheus.enabled }} diff --git a/deploy/helm/templates/addons/apecloud-mysql-addon.yaml b/deploy/helm/templates/addons/apecloud-mysql-addon.yaml index 133523602..e2a812881 100644 --- a/deploy/helm/templates/addons/apecloud-mysql-addon.yaml +++ b/deploy/helm/templates/addons/apecloud-mysql-addon.yaml @@ -10,12 +10,8 @@ metadata: helm.sh/resource-policy: keep {{- end }} spec: - description: 'ApeCloud MySQL is fully compatible with MySQL syntax and supports single-availability - zone deployment, double-availability zone deployment, and multiple-availability zone deployment. - Based on the Paxos consensus protocol, ApeCloud MySQL realizes automatic leader election, log - synchronization, and strict consistency. ApeCloud MySQL is the optimum choice for the production - environment since it can automatically perform a high-availability switch to maintain business continuity - when container exceptions, server exceptions, or availability zone exceptions occur.' + description: 'ApeCloud MySQL is a database that is compatible with MySQL syntax and achieves high availability + through the utilization of the RAFT consensus protocol.' type: Helm diff --git a/deploy/helm/templates/addons/aws-loadbalancer-controller-addon.yaml b/deploy/helm/templates/addons/aws-loadbalancer-controller-addon.yaml index 14137410d..066470ebd 100644 --- a/deploy/helm/templates/addons/aws-loadbalancer-controller-addon.yaml +++ b/deploy/helm/templates/addons/aws-loadbalancer-controller-addon.yaml @@ -1,7 +1,7 @@ apiVersion: extensions.kubeblocks.io/v1alpha1 kind: Addon metadata: - name: aws-loadbalancer-controller + name: aws-load-balancer-controller labels: {{- include "kubeblocks.labels" . | nindent 4 }} {{- if .Values.keepAddons }} @@ -18,11 +18,11 @@ spec: installValues: configMapRefs: - - name: aws-loadbalancer-controller-chart-kubeblocks-values + - name: aws-load-balancer-controller-chart-kubeblocks-values key: values-kubeblocks-override.yaml setValues: - - clusterName={{ index .Values "aws-loadbalancer-controller" "clusterName" }} + - clusterName={{ index .Values "aws-load-balancer-controller" "clusterName" }} valuesMapping: valueMap: @@ -41,9 +41,12 @@ spec: defaultInstallValues: - replicas: 1 + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: - autoInstall: {{ index .Values "aws-loadbalancer-controller" "enabled" }} + autoInstall: {{ index .Values "aws-load-balancer-controller" "enabled" }} selectors: - key: KubeGitVersion operator: Contains diff --git a/deploy/helm/templates/addons/aws-loadbalancer-controller-values.yaml b/deploy/helm/templates/addons/aws-loadbalancer-controller-values.yaml index 8c272438d..4a37b0e7f 100644 --- a/deploy/helm/templates/addons/aws-loadbalancer-controller-values.yaml +++ b/deploy/helm/templates/addons/aws-loadbalancer-controller-values.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: aws-loadbalancer-controller-chart-kubeblocks-values + name: aws-load-balancer-controller-chart-kubeblocks-values labels: {{- include "kubeblocks.labels" . | nindent 4 }} {{- if .Values.keepAddons }} @@ -10,4 +10,4 @@ metadata: {{- end }} data: values-kubeblocks-override.yaml: |- - {{- get ( .Values | toYaml | fromYaml ) "aws-loadbalancer-controller" | toYaml | nindent 4 }} \ No newline at end of file + {{- get ( .Values | toYaml | fromYaml ) "aws-load-balancer-controller" | toYaml | nindent 4 }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/chaos-mesh-addon.yaml b/deploy/helm/templates/addons/chaos-mesh-addon.yaml new file mode 100644 index 000000000..1da50304c --- /dev/null +++ b/deploy/helm/templates/addons/chaos-mesh-addon.yaml @@ -0,0 +1,84 @@ +apiVersion: extensions.kubeblocks.io/v1alpha1 +kind: Addon +metadata: + name: chaos-mesh + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + "kubeblocks.io/provider": community + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + description: 'Chaos Mesh is an open-source chaos engineering tool that facilitates testing the resiliency and reliability of distributed systems by introducing various failure scenarios in a controlled manner.' + + type: Helm + + helm: + chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/chaos-mesh-2.5.2.tgz + + installValues: + setValues: + - "version=2.5.2" + - "chaosDaemon.privileged=true" + - "dnsServer.create=true" + - "chaosDaemon.runtime=containerd" + - "chaosDaemon.socketPath=/run/containerd/containerd.sock" + + valuesMapping: + valueMap: + replicaCount: controllerManager.replicaCount + jsonMap: + tolerations: controllerManager.tolerations + resources: + cpu: + requests: controllerManager.resources.requests.cpu + memory: + requests: controllerManager.resources.requests.memory + + extras: + - name: chaosDaemon + jsonMap: + tolerations: chaosDaemon.tolerations + + - name: dashboard + valueMap: + replicaCount: dashboard.replicaCount + jsonMap: + tolerations: dashboard.tolerations + resources: + cpu: + requests: dashboard.resources.requests.cpu + memory: + requests: dashboard.resources.requests.memory + + - name: dnsServer + jsonMap: + tolerations: dnsServer.tolerations + resources: + cpu: + requests: dnsServer.resources.requests.cpu + memory: + requests: dnsServer.resources.requests.memory + + installable: + autoInstall: false + + defaultInstallValues: + - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + extras: + - name: chaosDaemon + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + - name: dashboard + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + - name: dnsServer + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/csi-driver-addon.yaml b/deploy/helm/templates/addons/csi-driver-addon.yaml index 01705ca90..56da3de7a 100644 --- a/deploy/helm/templates/addons/csi-driver-addon.yaml +++ b/deploy/helm/templates/addons/csi-driver-addon.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "kubeblocks.labels" . | nindent 4 }} spec: - description: 'Kubeblocks CSI driver provides a container storage interface used by Container Orchestrators + description: 'KubeBlocks CSI driver provides a container storage interface used by Container Orchestrators to manage the lifecycle of block storage for cloud vendors.' type: Helm @@ -40,6 +40,15 @@ spec: defaultInstallValues: - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + extras: + - name: node + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + installable: autoInstall: {{ get ( get ( .Values | toYaml | fromYaml ) "kubeblocks-csi-driver" ) "enabled" }} diff --git a/deploy/helm/templates/addons/csi-hostpath-driver-addon.yaml b/deploy/helm/templates/addons/csi-hostpath-driver-addon.yaml index e98ffcf73..401dc8a7d 100644 --- a/deploy/helm/templates/addons/csi-hostpath-driver-addon.yaml +++ b/deploy/helm/templates/addons/csi-hostpath-driver-addon.yaml @@ -20,8 +20,15 @@ spec: - name: csi-hostpath-driver-chart-kubeblocks-values key: values-kubeblocks-override.yaml + valuesMapping: + jsonMap: + tolerations: tolerations + defaultInstallValues: - enabled: true + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ get ( get ( .Values | toYaml | fromYaml ) "csi-hostpath-driver" ) "enabled" }} diff --git a/deploy/helm/templates/addons/csi-s3-addon.yaml b/deploy/helm/templates/addons/csi-s3-addon.yaml index 1a5819226..4c37d28d9 100644 --- a/deploy/helm/templates/addons/csi-s3-addon.yaml +++ b/deploy/helm/templates/addons/csi-s3-addon.yaml @@ -17,21 +17,23 @@ spec: # chartLocationURL: https://raw.githubusercontent.com/cloudve/helm-charts/master/charts/csi-s3-0.31.3.tgz chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/csi-s3-{{ default .Chart.Version .Values.versionOverride }}.tgz installValues: - configMapRefs: - - name: csi-s3-chart-kubeblocks-values - key: values-kubeblocks-override.yaml + secretRefs: + - name: {{ include "kubeblocks.fullname" . }}-cloud-provider + key: csi-s3 + + valuesMapping: + jsonMap: + tolerations: tolerations + extras: + - name: daemonset + jsonMap: + tolerations: daemonsetTolerations defaultInstallValues: - enabled: true + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ get ( get ( .Values | toYaml | fromYaml ) "csi-s3" ) "enabled" }} - selectors: - - key: KubeGitVersion - operator: Contains - values: - - eks - - aliyun - - gke - - tke - - aks diff --git a/deploy/helm/templates/addons/grafana-addon.yaml b/deploy/helm/templates/addons/grafana-addon.yaml index beced7956..0cbc01839 100644 --- a/deploy/helm/templates/addons/grafana-addon.yaml +++ b/deploy/helm/templates/addons/grafana-addon.yaml @@ -45,6 +45,9 @@ spec: resources: requests: storage: 1Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} - selectors: - key: KubeGitVersion @@ -52,10 +55,12 @@ spec: values: - aliyun replicas: 1 - storageClass: alicloud-disk-efficiency resources: requests: storage: 20Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ .Values.grafana.enabled }} diff --git a/deploy/helm/templates/addons/loki-addon.yaml b/deploy/helm/templates/addons/loki-addon.yaml new file mode 100644 index 000000000..67abc422d --- /dev/null +++ b/deploy/helm/templates/addons/loki-addon.yaml @@ -0,0 +1,75 @@ +apiVersion: extensions.kubeblocks.io/v1alpha1 +kind: Addon +metadata: + name: loki + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + "kubeblocks.io/provider": community + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + description: Grafana Loki is a horizontally scalable, highly available, and multi-tenant log aggregation system, which inspired by Prometheus. + type: Helm + + helm: + chartLocationURL: "https://github.com/grafana/helm-charts/releases/download/helm-loki-5.5.8/loki-5.5.8.tgz" + # chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/... + installValues: + configMapRefs: + - name: {{ include "addon.loki.name" . }}-chart-kubeblocks-values + key: values-kubeblocks-override.yaml + + valuesMapping: + valueMap: + replicaCount: singleBinary.replicas + storageClass: singleBinary.persistence.storageClass + persistentVolumeEnabled: singleBinary.persistence.enabled + + jsonMap: + tolerations: singleBinary.tolerations + + resources: + storage: singleBinary.persistence.size + + defaultInstallValues: + - replicas: 1 + storageClass: + resources: + requests: + storage: 8Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + # for ACK, the smallest storage size is 20Gi, the format of GitVersion is v1.24.6-aliyun.1 + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - aliyun + replicas: 1 + resources: + requests: + storage: 20Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + # for TKE, the smallest storage size is 10Gi, the format of GitVersion is v1.24.4-tke.5 + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - tke + replicas: 1 + resources: + requests: + storage: 10Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + installable: + autoInstall: {{ .Values.loki.enabled }} + + diff --git a/deploy/helm/templates/addons/csi-s3-values.yaml b/deploy/helm/templates/addons/loki-values.yaml similarity index 67% rename from deploy/helm/templates/addons/csi-s3-values.yaml rename to deploy/helm/templates/addons/loki-values.yaml index 98f7b9bef..8e3094cf8 100644 --- a/deploy/helm/templates/addons/csi-s3-values.yaml +++ b/deploy/helm/templates/addons/loki-values.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: csi-s3-chart-kubeblocks-values + name: {{ include "addon.loki.name" . }}-chart-kubeblocks-values labels: {{- include "kubeblocks.labels" . | nindent 4 }} {{- if .Values.keepAddons }} @@ -10,4 +10,4 @@ metadata: {{- end }} data: values-kubeblocks-override.yaml: |- - {{- get ( .Values | toYaml | fromYaml ) "csi-s3" | toYaml | nindent 4 }} \ No newline at end of file + {{- .Values.loki | toYaml | nindent 4 }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/chatgpt-retrieval-plugin.yaml b/deploy/helm/templates/addons/migration-addon.yaml similarity index 64% rename from deploy/helm/templates/addons/chatgpt-retrieval-plugin.yaml rename to deploy/helm/templates/addons/migration-addon.yaml index de7826305..b684c3283 100644 --- a/deploy/helm/templates/addons/chatgpt-retrieval-plugin.yaml +++ b/deploy/helm/templates/addons/migration-addon.yaml @@ -1,21 +1,21 @@ apiVersion: extensions.kubeblocks.io/v1alpha1 kind: Addon metadata: - name: chatgpt-retrieval-plugin + name: migration labels: {{- include "kubeblocks.labels" . | nindent 4 }} - "kubeblocks.io/provider": apecloud + "kubeblocks.io/provider": community {{- if .Values.keepAddons }} annotations: helm.sh/resource-policy: keep {{- end }} spec: - description: 'Deploys a ChatGPT Retrieval Plugin application in a cluster. - ChatGPT Retrieval Plugin is an application for personalizing your ChatGPT dialogue through your private data.' + description: 'Migration is a tool for migrating data between two databases.' + type: Helm helm: - chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/chatgpt-retrieval-plugin-{{ default .Chart.Version .Values.versionOverride }}.tgz + chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/dt-platform-0.1.0.tgz valuesMapping: valueMap: replicaCount: replicaCount @@ -31,9 +31,11 @@ spec: requests: resources.requests.memory limits: resources.limits.memory - defaultInstallValues: - - replicas: 1 - installable: autoInstall: false + defaultInstallValues: + - enabled: true + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} diff --git a/deploy/helm/templates/addons/milvus-addon.yaml b/deploy/helm/templates/addons/milvus-addon.yaml index 40d9f022f..cd194ac85 100644 --- a/deploy/helm/templates/addons/milvus-addon.yaml +++ b/deploy/helm/templates/addons/milvus-addon.yaml @@ -21,4 +21,7 @@ spec: autoInstall: false defaultInstallValues: - - enabled: true + - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} diff --git a/deploy/helm/templates/addons/mongodb-addon.yaml b/deploy/helm/templates/addons/mongodb-addon.yaml new file mode 100644 index 000000000..7edbb59b4 --- /dev/null +++ b/deploy/helm/templates/addons/mongodb-addon.yaml @@ -0,0 +1,25 @@ +apiVersion: extensions.kubeblocks.io/v1alpha1 +kind: Addon +metadata: + name: mongodb + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + "kubeblocks.io/provider": community + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + description: 'MongoDB is a document database designed for ease of application development and scaling.' + + type: Helm + + helm: + # chartLocationURL: https://github.com/apecloud/helm-charts/releases/download/mongodb-{{ default .Chart.Version .Values.versionOverride }}/mongodb-{{ default .Chart.Version .Values.versionOverride }}.tgz + chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/mongodb-{{ default .Chart.Version .Values.versionOverride }}.tgz + + installable: + autoInstall: true + + defaultInstallValues: + - enabled: true diff --git a/deploy/helm/templates/addons/nyancat-addon.yaml b/deploy/helm/templates/addons/nyancat-addon.yaml index 4fde96f1e..d77f27244 100644 --- a/deploy/helm/templates/addons/nyancat-addon.yaml +++ b/deploy/helm/templates/addons/nyancat-addon.yaml @@ -11,7 +11,7 @@ metadata: {{- end }} spec: description: 'Deploys a nyancat application in a cluster. - Nyancat is a demo application for showing database cluster availibility.' + Nyancat is a demo application for showing database cluster availability.' type: Helm helm: @@ -33,7 +33,10 @@ spec: limits: resources.limits.memory defaultInstallValues: - - replicas: 2 + - replicas: 1 + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: false diff --git a/deploy/helm/templates/addons/opensearch-addon.yaml b/deploy/helm/templates/addons/opensearch-addon.yaml new file mode 100644 index 000000000..4d24c35dc --- /dev/null +++ b/deploy/helm/templates/addons/opensearch-addon.yaml @@ -0,0 +1,27 @@ +apiVersion: extensions.kubeblocks.io/v1alpha1 +kind: Addon +metadata: + name: opensearch + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} + "kubeblocks.io/provider": community + {{- if .Values.keepAddons }} + annotations: + helm.sh/resource-policy: keep + {{- end }} +spec: + description: 'OpenSearch is a scalable, flexible, and extensible open-source software suite for search, analytics, and observability applications licensed under Apache 2.0.' + + type: Helm + + helm: + chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/opensearch-{{ default .Chart.Version .Values.versionOverride }}.tgz + + installable: + autoInstall: false + + defaultInstallValues: + - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} diff --git a/deploy/helm/templates/addons/prometheus-addon.yaml b/deploy/helm/templates/addons/prometheus-addon.yaml index 56c0f2a23..a66f0f39b 100644 --- a/deploy/helm/templates/addons/prometheus-addon.yaml +++ b/deploy/helm/templates/addons/prometheus-addon.yaml @@ -65,12 +65,19 @@ spec: memory: 512Mi limits: memory: 4Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + extras: - name: alertmanager replicas: 1 resources: requests: storage: 4Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} # for ACK, the smallest storage size is 20Gi, the format of GitVersion is v1.24.6-aliyun.1 - selectors: @@ -79,20 +86,25 @@ spec: values: - aliyun replicas: 1 - storageClass: alicloud-disk-efficiency resources: requests: storage: 20Gi memory: 512Mi limits: memory: 4Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + extras: - name: alertmanager replicas: 1 - storageClass: alicloud-disk-efficiency resources: requests: storage: 20Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} # for TKE, the smallest storage size is 10Gi, the format of GitVersion is v1.24.4-tke.5 - selectors: @@ -107,12 +119,19 @@ spec: memory: 512Mi limits: memory: 4Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + extras: - name: alertmanager replicas: 1 resources: requests: storage: 10Gi + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ .Values.prometheus.enabled }} \ No newline at end of file diff --git a/deploy/helm/templates/addons/qdrant-addon.yaml b/deploy/helm/templates/addons/qdrant-addon.yaml index 9f3b8ab10..0c1930ae2 100644 --- a/deploy/helm/templates/addons/qdrant-addon.yaml +++ b/deploy/helm/templates/addons/qdrant-addon.yaml @@ -21,4 +21,7 @@ spec: autoInstall: false defaultInstallValues: - - enabled: true + - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} diff --git a/deploy/helm/templates/addons/redis-addon.yaml b/deploy/helm/templates/addons/redis-addon.yaml index 8a18ef5cf..da725257a 100644 --- a/deploy/helm/templates/addons/redis-addon.yaml +++ b/deploy/helm/templates/addons/redis-addon.yaml @@ -19,7 +19,7 @@ spec: chartLocationURL: https://jihulab.com/api/v4/projects/85949/packages/helm/stable/charts/redis-{{ default .Chart.Version .Values.versionOverride }}.tgz installable: - autoInstall: false + autoInstall: true defaultInstallValues: - enabled: true diff --git a/deploy/helm/templates/addons/snapshot-controller-addon.yaml b/deploy/helm/templates/addons/snapshot-controller-addon.yaml index 04a734ed9..db3851237 100644 --- a/deploy/helm/templates/addons/snapshot-controller-addon.yaml +++ b/deploy/helm/templates/addons/snapshot-controller-addon.yaml @@ -26,6 +26,7 @@ spec: valuesMapping: valueMap: replicaCount: replicaCount + storageClass: volumeSnapshotClasses[0].driver jsonMap: tolerations: tolerations @@ -39,8 +40,55 @@ spec: limits: resources.limits.memory defaultInstallValues: - - replicas: 1 + - enabled: {{ get ( get ( .Values | toYaml | fromYaml ) "snapshot-controller" ) "enabled" }} + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - eks + storageClass: ebs.csi.aws.com + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - aliyun + storageClass: diskplugin.csi.alibabacloud.com + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - gke + storageClass: pd.csi.storage.gke.io + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} + + - selectors: + - key: KubeGitVersion + operator: Contains + values: + - aks + storageClass: disk.csi.azure.com + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} installable: autoInstall: {{ get ( get ( .Values | toYaml | fromYaml ) "snapshot-controller" ) "enabled" }} - + selectors: + - key: KubeGitVersion + operator: DoesNotContain + values: + - tke \ No newline at end of file diff --git a/deploy/helm/templates/addons/weaviate-addon.yaml b/deploy/helm/templates/addons/weaviate-addon.yaml index 3d7891897..8197fd857 100644 --- a/deploy/helm/templates/addons/weaviate-addon.yaml +++ b/deploy/helm/templates/addons/weaviate-addon.yaml @@ -21,4 +21,7 @@ spec: autoInstall: false defaultInstallValues: - - enabled: true + - enabled: false + {{- with .Values.tolerations }} + tolerations: {{ toJson . | quote }} + {{- end }} diff --git a/deploy/helm/templates/admission/webhookconfiguration.yaml b/deploy/helm/templates/admission/webhookconfiguration.yaml index 25bad2588..89966ace7 100644 --- a/deploy/helm/templates/admission/webhookconfiguration.yaml +++ b/deploy/helm/templates/admission/webhookconfiguration.yaml @@ -8,7 +8,7 @@ kind: Secret metadata: name: {{ include "kubeblocks.fullname" . }}.{{ .Release.Namespace }}.svc.tls-ca labels: - {{- include "kubeblocks.selectorLabels" . | nindent 4 }} + {{- include "kubeblocks.labels" . | nindent 4 }} annotations: self-signed-cert: "true" type: kubernetes.io/tls @@ -33,6 +33,8 @@ apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration metadata: name: {{ include "kubeblocks.fullname" . }}-mutating-webhook-configuration + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} webhooks: - admissionReviewVersions: - v1 @@ -87,6 +89,8 @@ apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: name: {{ include "kubeblocks.fullname" . }}-validating-webhook-configuration + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} webhooks: - admissionReviewVersions: - v1 diff --git a/deploy/helm/templates/class/classfamily.yaml b/deploy/helm/templates/class/componentclassconstraint.yaml similarity index 56% rename from deploy/helm/templates/class/classfamily.yaml rename to deploy/helm/templates/class/componentclassconstraint.yaml index 14ad1038f..dedf0a341 100644 --- a/deploy/helm/templates/class/classfamily.yaml +++ b/deploy/helm/templates/class/componentclassconstraint.yaml @@ -1,11 +1,12 @@ apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClassFamily +kind: ComponentResourceConstraint metadata: - name: kb-class-family-general + name: kb-resource-constraint-general labels: - classfamily.kubeblocks.io/provider: kubeblocks + resourceconstraint.kubeblocks.io/provider: kubeblocks + {{- include "kubeblocks.labels" . | nindent 4 }} spec: - models: + constraints: - cpu: min: "0.5" max: 2 @@ -13,7 +14,7 @@ spec: memory: sizePerCPU: 1Gi - cpu: - min: 2 + min: "0.5" max: 2 memory: sizePerCPU: 2Gi @@ -25,13 +26,14 @@ spec: --- apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClassFamily +kind: ComponentResourceConstraint metadata: - name: kb-class-family-memory-optimized + name: kb-resource-constraint-memory-optimized labels: - classfamily.kubeblocks.io/provider: kubeblocks + resourceconstraint.kubeblocks.io/provider: kubeblocks + {{- include "kubeblocks.labels" . | nindent 4 }} spec: - models: + constraints: - cpu: slots: [2, 4, 8, 12, 24, 48] memory: diff --git a/deploy/helm/templates/cloud-provider-secret.yaml b/deploy/helm/templates/cloud-provider-secret.yaml new file mode 100644 index 000000000..cf1f87e9f --- /dev/null +++ b/deploy/helm/templates/cloud-provider-secret.yaml @@ -0,0 +1,30 @@ +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "kubeblocks.fullname" . }}-cloud-provider + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +stringData: +{{- if index .Values "cloudProvider" "accessKey" }} + accessKey: {{ index .Values "cloudProvider" "accessKey" }} +{{- end }} +{{- if index .Values "cloudProvider" "secretKey" }} + secretKey: {{ index .Values "cloudProvider" "secretKey" }} +{{- end }} +{{- if index .Values "cloudProvider" "region" }} + region: {{ index .Values "cloudProvider" "region" }} +{{- end }} +{{- if index .Values "cloudProvider" "name" }} + cloudProvider: {{ index .Values "cloudProvider" "name" }} +{{- end }} +{{- if index .Values "cloudProvider" "bucket" }} + bucket: {{ index .Values "cloudProvider" "bucket" }} +{{- end }} + csi-s3: | + secret: + accessKey: {{ index .Values "cloudProvider" "accessKey" }} + secretKey: {{ index .Values "cloudProvider" "secretKey" }} + region: {{ index .Values "cloudProvider" "region" }} + cloudProvider: {{ index .Values "cloudProvider" "name" }} + storageClass: + bucket: {{ index .Values "cloudProvider" "bucket" }} \ No newline at end of file diff --git a/deploy/helm/templates/configmap.yaml b/deploy/helm/templates/configmap.yaml new file mode 100644 index 000000000..89cf1cf62 --- /dev/null +++ b/deploy/helm/templates/configmap.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: {{ include "kubeblocks.fullname" . }}-manager-config + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +data: + config.yaml: | + # the global pvc name which persistent volume claim to store the backup data. + # will replace the pvc name when it is empty in the backup policy. + BACKUP_PVC_NAME: "{{ .Values.dataProtection.backupPVCName }}" + + # the init capacity of pvc for creating the pvc, e.g. 10Gi. + # will replace the init capacity when it is empty in the backup policy. + BACKUP_PVC_INIT_CAPACITY: "{{ .Values.dataProtection.backupPVCInitCapacity }}" + + # the pvc storage class name. + # will replace the storageClassName when it is nil in the backup policy. + BACKUP_PVC_STORAGE_CLASS: "{{ .Values.dataProtection.backupPVCStorageClassName }}" + + # the pvc create policy. + # if the storageClass supports dynamic provisioning, recommend "IfNotPresent" policy. + # otherwise, using "Never" policy. + # only affect the backupPolicy automatically created by KubeBs-locks. + BACKUP_PVC_CREATE_POLICY: "{{ .Values.dataProtection.backupPVCCreatePolicy }}" + + # the configmap name of the pv template. if the csi-driver not support dynamic provisioning, + # you can provide a configmap which contains key "persistentVolume" and value of the persistentVolume struct. + # only effective when storageClass is empty. + BACKUP_PV_CONFIGMAP_NAME: "{{ .Values.dataProtection.backupPVConfigMapName }}" + + # the configmap namespace of the pv template. + BACKUP_PV_CONFIGMAP_NAMESPACE: "{{ .Values.dataProtection.backupPVConfigMapNamespace }}" + + {{- with .Values.dataPlane }} + # data plane tolerations + DATA_PLANE_TOLERATIONS: {{ toJson .tolerations | squote }} + + # data plane affinity + DATA_PLANE_AFFINITY: {{ toJson .affinity | squote }} + {{- end }} \ No newline at end of file diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml index bac706b88..f2cf56f87 100644 --- a/deploy/helm/templates/deployment.yaml +++ b/deploy/helm/templates/deployment.yaml @@ -77,19 +77,15 @@ spec: - name: VOLUMESNAPSHOT value: "true" {{- end }} - {{- if .Values.dataProtection.backupSchedule }} - - name: DP_BACKUP_SCHEDULE - value: {{ .Values.dataProtection.backupSchedule }} - {{- end }} - {{- if .Values.dataProtection.backupTTL }} - - name: DP_BACKUP_TTL - value: {{ .Values.dataProtection.backupTTL }} + {{- if .Capabilities.APIVersions.Has "snapshot.storage.k8s.io/v1beta1" }} + - name: VOLUMESNAPSHOT_API_BETA + value: "true" {{- end }} {{- if .Values.admissionWebhooks.enabled }} - name: ENABLE_WEBHOOKS value: "true" {{- end }} - {{- if not ( include "kubeblocks.addonControllerEnabled" . ) }} + {{- if ( include "kubeblocks.addonControllerEnabled" . ) | deepEqual "false" }} - name: DISABLE_ADDON_CTRLER value: "true" {{- else }} @@ -100,6 +96,10 @@ spec: - name: KUBEBLOCKS_ADDON_SA_NAME value: {{ include "kubeblocks.addonSAName" . }} {{- end }} + {{- if .Values.enabledAlphaFeatureGates.recoverVolumeExpansionFailure }} + - name: RECOVER_VOLUME_EXPANSION_FAILURE + value: "true" + {{- end }} {{- with .Values.securityContext }} securityContext: {{- toYaml . | nindent 12 }} @@ -131,6 +131,8 @@ spec: resources: {{- toYaml .Values.resources | nindent 12 }} volumeMounts: + - mountPath: /etc/kubeblocks + name: manager-config {{- if .Values.admissionWebhooks.enabled }} - mountPath: /tmp/k8s-webhook-server/serving-certs name: cert @@ -162,7 +164,7 @@ spec: volumes: - name: manager-config configMap: - name: manager-config + name: {{ include "kubeblocks.fullname" . }}-manager-config {{- if .Values.admissionWebhooks.enabled }} - name: cert secret: diff --git a/deploy/helm/templates/grafana/configmaps-datasources.yaml b/deploy/helm/templates/grafana/configmaps-datasources.yaml index 592c21eee..bcf167d6e 100644 --- a/deploy/helm/templates/grafana/configmaps-datasources.yaml +++ b/deploy/helm/templates/grafana/configmaps-datasources.yaml @@ -30,5 +30,41 @@ data: isDefault: true jsonData: timeInterval: {{ $scrapeInterval }} + - name: Prometheus-15s + type: prometheus + uid: {{ .Values.grafana.sidecar.datasources.uid }}-15 + {{- if .Values.grafana.sidecar.datasources.url }} + url: {{ .Values.grafana.sidecar.datasources.url }} + {{- else }} + url: http://kb-addon-{{ include "addon.prometheus.name" . }}-server.{{ template "kubeblocks.prometheus.namespace" . }}:{{ .Values.prometheus.server.service.servicePort }}/{{ trimPrefix "/" .Values.prometheus.server.routePrefix }} + {{- end }} + access: proxy + isDefault: false + jsonData: + timeInterval: 15s + - name: Prometheus-5s + type: prometheus + uid: {{ .Values.grafana.sidecar.datasources.uid }}-5 + {{- if .Values.grafana.sidecar.datasources.url }} + url: {{ .Values.grafana.sidecar.datasources.url }} + {{- else }} + url: http://kb-addon-{{ include "addon.prometheus.name" . }}-server.{{ template "kubeblocks.prometheus.namespace" . }}:{{ .Values.prometheus.server.service.servicePort }}/{{ trimPrefix "/" .Values.prometheus.server.routePrefix }} + {{- end }} + access: proxy + isDefault: false + jsonData: + timeInterval: 5s + - name: Prometheus-1s + type: prometheus + uid: {{ .Values.grafana.sidecar.datasources.uid }}-1 + {{- if .Values.grafana.sidecar.datasources.url }} + url: {{ .Values.grafana.sidecar.datasources.url }} + {{- else }} + url: http://kb-addon-{{ include "addon.prometheus.name" . }}-server.{{ template "kubeblocks.prometheus.namespace" . }}:{{ .Values.prometheus.server.service.servicePort }}/{{ trimPrefix "/" .Values.prometheus.server.routePrefix }} + {{- end }} + access: proxy + isDefault: false + jsonData: + timeInterval: 1s {{- end }} {{- end }} diff --git a/deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_editor_role.yaml b/deploy/helm/templates/rbac/apps_backuppolicytemplate_editor_role.yaml similarity index 88% rename from deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_editor_role.yaml rename to deploy/helm/templates/rbac/apps_backuppolicytemplate_editor_role.yaml index 2153e9ac4..ae96ce974 100644 --- a/deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_editor_role.yaml +++ b/deploy/helm/templates/rbac/apps_backuppolicytemplate_editor_role.yaml @@ -7,7 +7,7 @@ metadata: {{- include "kubeblocks.labels" . | nindent 4 }} rules: - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates verbs: @@ -19,7 +19,7 @@ rules: - update - watch - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates/status verbs: diff --git a/deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml b/deploy/helm/templates/rbac/apps_backuppolicytemplate_viewer_role.yaml similarity index 86% rename from deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml rename to deploy/helm/templates/rbac/apps_backuppolicytemplate_viewer_role.yaml index 0a7bbc579..b8dac3a90 100644 --- a/deploy/helm/templates/rbac/dataprotection_backuppolicytemplate_viewer_role.yaml +++ b/deploy/helm/templates/rbac/apps_backuppolicytemplate_viewer_role.yaml @@ -7,7 +7,7 @@ metadata: {{- include "kubeblocks.labels" . | nindent 4 }} rules: - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates verbs: @@ -15,7 +15,7 @@ rules: - list - watch - apiGroups: - - dataprotection.kubeblocks.io + - apps.kubeblocks.io resources: - backuppolicytemplates/status verbs: diff --git a/deploy/helm/templates/rbac/clusterrole_binding.yaml b/deploy/helm/templates/rbac/clusterrole_binding.yaml index f52ad15ab..ffe21e1f4 100644 --- a/deploy/helm/templates/rbac/clusterrole_binding.yaml +++ b/deploy/helm/templates/rbac/clusterrole_binding.yaml @@ -12,7 +12,7 @@ subjects: - kind: ServiceAccount name: {{ include "kubeblocks.serviceAccountName" . }} namespace: {{ .Release.Namespace }} -{{- if ( include "kubeblocks.addonControllerEnabled" . ) }} +{{- if ( include "kubeblocks.addonControllerEnabled" . ) | deepEqual "true" }} --- apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding diff --git a/deploy/helm/templates/serviceaccount.yaml b/deploy/helm/templates/serviceaccount.yaml index df9b72f09..729bc3fe2 100644 --- a/deploy/helm/templates/serviceaccount.yaml +++ b/deploy/helm/templates/serviceaccount.yaml @@ -9,7 +9,7 @@ metadata: annotations: {{- toYaml . | nindent 4 }} {{- end }} - {{- if ( include "kubeblocks.addonControllerEnabled" . ) }} + {{- if ( include "kubeblocks.addonControllerEnabled" . ) | deepEqual "true" }} --- apiVersion: v1 kind: ServiceAccount diff --git a/deploy/helm/templates/storageclass.yaml b/deploy/helm/templates/storageclass.yaml new file mode 100644 index 000000000..db64de4ac --- /dev/null +++ b/deploy/helm/templates/storageclass.yaml @@ -0,0 +1,86 @@ +{{- if (.Capabilities.KubeVersion.GitVersion | contains "-eks") }} +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: kb-default-sc + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +allowVolumeExpansion: true +parameters: + ## parameters references: https://github.com/kubernetes-sigs/aws-ebs-csi-driver/blob/master/docs/parameters.md + type: {{ .Values.storageClass.provider.eks.volumeType }} # io2, io1, gp3, gp2 are all SSD variant + "csi.storage.k8s.io/fstype": {{ .Values.storageClass.provider.eks.fsType | default "xfs" }} +provisioner: ebs.csi.aws.com +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +--- +{{- else if (.Capabilities.KubeVersion.GitVersion | contains "-gke") }} +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: kb-default-sc + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +allowVolumeExpansion: true +parameters: + ## refer: https://github.com/kubernetes-sigs/gcp-compute-persistent-disk-csi-driver/issues/617 + type: {{ .Values.storageClass.provider.gke.volumeType }} + csi.storage.k8s.io/fstype: {{ .Values.storageClass.provider.gke.fsType | default "xfs" }} +provisioner: pd.csi.storage.gke.io +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +--- +{{- else if (.Capabilities.KubeVersion.GitVersion | contains "-aliyun") }} +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: kb-default-sc + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +allowVolumeExpansion: true +parameters: + ## parameters references: https://github.com/kubernetes-sigs/alibaba-cloud-csi-driver/blob/master/docs/disk.md + fstype: {{ .Values.storageClass.provider.aliyun.fsType | default "xfs" }} + type: {{ .Values.storageClass.provider.aliyun.volumeType }} +provisioner: diskplugin.csi.alibabacloud.com +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +--- +{{- else if (.Capabilities.KubeVersion.GitVersion | contains "-tke") }} +--- +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: kb-default-sc + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +parameters: + ## parameters references: https://cloud.tencent.com/document/product/457/44239, the fsType is not supported by tke. + type: {{ .Values.storageClass.provider.tke.volumeType }} +reclaimPolicy: Delete +provisioner: com.tencent.cloud.csi.cbs +volumeBindingMode: WaitForFirstConsumer +--- +{{- else if (.Capabilities.KubeVersion.GitVersion | contains "-aks") }} +--- +## it doesn't work here because aks does not support .Capabilities.KubeVersion.GitVersion to judge the provider. +## refer: https://github.com/Azure/AKS/issues/3375 +apiVersion: storage.k8s.io/v1 +kind: StorageClass +metadata: + name: kb-default-sc + labels: + {{- include "kubeblocks.labels" . | nindent 4 }} +parameters: + # parameters references: https://github.com/kubernetes-sigs/azuredisk-csi-driver/blob/master/docs/driver-parameters.md + fsType: {{ .Values.storageClass.provider.aks.fsType | default "xfs" }} + kind: {{ .Values.storageClass.provider.aks.volumeType }} + skuName: Standard_LRS +provisioner: kubernetes.io/azure-disk +reclaimPolicy: Delete +volumeBindingMode: WaitForFirstConsumer +--- +{{- end }} \ No newline at end of file diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index a9902fe04..5de869848 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -41,7 +41,7 @@ fullnameOverride: "" updateStrategy: rollingUpdate: maxSurge: 1 - maxUnavailable: 1 + maxUnavailable: 40% type: RollingUpdate ## Change `hostNetwork` to `true` when you want the KubeBlocks's pod to share its host's network namespace. @@ -60,6 +60,19 @@ hostNetwork: false ## dnsPolicy: ClusterFirst +## Configure podDisruptionBudget spec settings +## +## @param podDisruptionBudget.minAvailable +## @param podDisruptionBudget.maxUnavailable +podDisruptionBudget: + # Configures the minimum available pods for KubeBlocks disruptions. + # Cannot be used if `maxUnavailable` is set. + minAvailable: 1 + # Configures the maximum unavailable pods for KubeBlocks disruptions. + # Cannot be used if `minAvailable` is set. + maxUnavailable: + + ## Logger settings ## ## @param loggerSettings.developmentMode @@ -153,15 +166,16 @@ serviceMonitor: # Only used if `service.type` is `NodePort`. nodePort: -## @param topologySpreadConstraints +## KubeBlocks pods deployment topologySpreadConstraints settings ## +## @param topologySpreadConstraints topologySpreadConstraints: [] ## Resource settings ## -## @param topologySpreadConstraints.limits -## @param topologySpreadConstraints.requests +## @param resources.limits +## @param resources.requests resources: {} # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little @@ -222,13 +236,25 @@ affinity: values: - "true" -## PDB settings +## @param data plane settings ## -## @param podDisruptionBudget.minAvailable -## @param podDisruptionBudget.maxUnavailable -podDisruptionBudget: - minAvailable: 1 - maxUnavailable: +dataPlane: + tolerations: + - key: kb-data + operator: Equal + value: "true" + effect: NoSchedule + + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kb-data + operator: In + values: + - "true" ## AdmissionWebhooks settings ## @@ -242,20 +268,21 @@ admissionWebhooks: ## Data protection settings ## +## @param dataProtection.enableVolumeSnapshot - set this to true if cluster does have snapshot.storage.k8s.io API installed +## @param dataProtection.backupPVCName - set the default pvc to store the file for backup +## @param dataProtection.backupPVCInitCapacity - set the default pvc initCapacity if the pvc need to be created by backup controller +## @param dataProtection.backupPVCStorageClassName - set the default pvc storageClassName if the pvc need to be created by backup controller +## @param dataProtection.backupPVCCreatePolicy - set the default create policy of the pvc, optional values: IfNotPresent, Never +## @param dataProtection.backupPVConfigMapName - set the default configmap name which contains key "persistentVolume" and value of the persistentVolume struct. +## @param dataProtection.backupPVConfigMapNamespace - set the default configmap namespace of pv template. dataProtection: - ## @param dataProtection.enableVolumeSnapshot - set this to true if cluster does have snapshot.storage.k8s.io API installed - ## enableVolumeSnapshot: false - ## @param dataProtection.backupSchedule -- set backup policy schedule time - ## backupSchedule is in Cron format, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron. - ## Example: 0 2 * * * -- backup job will be scheduled to start at 2:00 each day. - ## - backupSchedule: "" - ## @param dataProtection.backupTTL -- set backup time to live. - ## backupTTL is a time.Duration-parseable string describing how long. - ## Example: 168h0m0s -- the backup will expire in 7 days. - ## - backupTTL: "" + backupPVCName: "" + backupPVCInitCapacity: "" + backupPVCStorageClassName: "" + backupPVCCreatePolicy: "" + backupPVConfigMapName: "" + backupPVConfigMapNamespace: "" ## Addon controller settings, this will require cluster-admin clusterrole. addonController: @@ -322,7 +349,7 @@ prometheus: ## If true, alertmanager will create/use a Persistent Volume Claim ## If false, use emptyDir ## - enabled: true + enabled: false ## alertmanager data Persistent Volume size ## @@ -496,6 +523,17 @@ prometheus: ## evaluation_interval: 15s + ## Additional Prometheus server container flags + ## + extraFlags: + - web.enable-lifecycle + - web.enable-remote-write-receiver + + ## Additional Prometheus server container arguments + ## + extraArgs: + log.level: info + ## https://prometheus.io/docs/prometheus/latest/configuration/configuration/#remote_write ## remoteWrite: [] @@ -529,7 +567,7 @@ prometheus: ## If true, Prometheus server will create/use a Persistent Volume Claim ## If false, use emptyDir ## - enabled: true + enabled: false ## Prometheus server data Persistent Volume size ## @@ -1085,6 +1123,91 @@ prometheus: summary: 'Redis replication broken' description: 'Redis instance lost a slave. (instance: {{ $labels.pod }})' + mongodb_replicaset_alert_rules.yaml: |- + groups: + - name: MongodbExporter + rules: + - alert: MongodbDown + expr: 'max_over_time(mongodb_up[1m]) == 0' + for: 0m + labels: + severity: critical + annotations: + summary: 'MongoDB is Down' + description: 'MongoDB instance is down\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbRestarted + expr: 'mongodb_instance_uptime_seconds < 60' + for: 0m + labels: + severity: info + annotations: + summary: 'Mongodb has just been restarted (< 60s)' + description: 'Mongodb has just been restarted {{ $value | printf "%.1f" }} seconds ago\n LABELS = {{ $labels }}' + + - alert: MongodbReplicaMemberUnhealthy + expr: 'max_over_time(mongodb_rs_members_health[1m]) == 0' + for: 0m + labels: + severity: critical + annotations: + summary: 'Mongodb replica member is unhealthy' + description: 'MongoDB replica member is not healthy\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbReplicationLag + expr: '(mongodb_rs_members_optimeDate{member_state="PRIMARY"} - on (pod) group_right mongodb_rs_members_optimeDate{member_state="SECONDARY"}) / 1000 > 10' + for: 0m + labels: + severity: critical + annotations: + summary: 'MongoDB replication lag (> 10s)' + description: 'Mongodb replication lag is more than 10s\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbReplicationHeadroom + expr: 'sum(avg(mongodb_mongod_replset_oplog_head_timestamp - mongodb_mongod_replset_oplog_tail_timestamp)) - sum(avg(mongodb_rs_members_optimeDate{member_state="PRIMARY"} - on (pod) group_right mongodb_rs_members_optimeDate{member_state="SECONDARY"})) <= 0' + for: 0m + labels: + severity: critical + annotations: + summary: 'MongoDB replication headroom (< 0)' + description: 'MongoDB replication headroom is <= 0\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbNumberCursorsOpen + expr: 'mongodb_ss_metrics_cursor_open{csr_type="total"} > 10 * 1000' + for: 2m + labels: + severity: warning + annotations: + summary: 'MongoDB opened cursors num (> 10k)' + description: 'Too many cursors opened by MongoDB for clients (> 10k)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbCursorsTimeouts + expr: 'increase(mongodb_ss_metrics_cursor_timedOut[1m]) > 100' + for: 2m + labels: + severity: warning + annotations: + summary: 'MongoDB cursors timeouts (>100/minute)' + description: 'Too many cursors are timing out (> 100/minute)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbTooManyConnections + expr: 'avg by(pod) (rate(mongodb_ss_connections{conn_type="current"}[1m])) / avg by(pod) (sum (mongodb_ss_connections) by(pod)) * 100 > 80' + for: 2m + labels: + severity: warning + annotations: + summary: 'MongoDB too many connections (> 80%)' + description: 'Too many connections (> 80%)\n VALUE = {{ $value }}\n LABELS = {{ $labels }}' + + - alert: MongodbVirtualMemoryUsage + expr: '(sum(mongodb_ss_mem_virtual) BY (pod) / sum(mongodb_ss_mem_resident) BY (pod)) > 100' + for: 2m + labels: + severity: warning + annotations: + summary: MongoDB virtual memory usage high + description: "High memory usage: the quotient of (mem_virtual / mem_resident) is more than 100\n VALUE = {{ $value }}\n LABELS = {{ $labels }}" + kafka_alert_rules.yaml: |- group: - name: KafkaExporter @@ -1124,6 +1247,7 @@ prometheus: - /etc/config/postgresql_alert_rules.yml - /etc/config/redis_alert_rules.yml - /etc/config/kafka_alert_rules.yml + - /etc/config/mongodb_replicaset_alert_rules.yaml scrape_configs: - job_name: prometheus @@ -1376,6 +1500,41 @@ prometheus: ## enabled: false +## loki settings for kubeblocks +loki: + enabled: false + singleBinary: + replicas: 1 + tolerations: + - key: kb-controller + operator: Equal + value: "true" + effect: NoSchedule + monitoring: + lokiCanary: + enabled: false + selfMonitoring: + enabled: false + grafanaAgent: + installOperator: false + dashboards: + enabled: false + rules: + enabled: false + serviceMonitor: + enabled: false + test: + enabled: false + loki: + auth_enabled: false + commonConfig: + replication_factor: 1 + storage: + type: filesystem + podSecurityContext: + runAsNonRoot: false + runAsUser: 0 + grafana: ## If false, grafana sub-chart will not be installed ## @@ -1530,7 +1689,7 @@ grafana: snapshot-controller: ## @param snapshot-controller.enabled -- Enable snapshot-controller chart. ## - enabled: false + enabled: true ## @param snapshot-controller.replicaCount -- Number of replicas to deploy. ## replicaCount: 1 @@ -1547,6 +1706,13 @@ snapshot-controller: value: "true" effect: NoSchedule + volumeSnapshotClasses: + - name: default-vsc + annotations: + snapshot.storage.kubernetes.io/is-default-class: "true" + driver: hostpath.csi.k8s.io + deletionPolicy: Delete + affinity: nodeAffinity: preferredDuringSchedulingIgnoredDuringExecution: @@ -1561,30 +1727,27 @@ snapshot-controller: kubeblocks-csi-driver: enabled: false + +cloudProvider: + ## cloudProvider secret settings + ## @param cloudProvider.accessKey -- S3 Access Key. + ## @param cloudProvider.secretKey -- S3 Secret Key. + ## @param cloudProvider.region -- S3 region. + ## @param cloudProvider.cloud -- cloud name: [aws,aliyun]. + ## @param cloudProvider.bucket -- S3 Bucket. + accessKey: "" + secretKey: "" + region: "" + name: "" + bucket: "" + ## csi-s3 settings ## ref: https://artifacthub.io/packages/helm/cloudve/csi-s3#configuration ## csi-s3: - ## @param csi-s3.enabled -- Enable csi-s3 chart. + ## @param backupRepo.enabled -- Enable csi-s3 chart. ## enabled: false - ## csi-s3 secret settings - ## @param csi-s3.secret.accessKey -- S3 Access Key. - ## @param csi-s3.secret.secretKey -- S3 Secret Key. - ## @param csi-s3.secret.endpoint -- S3 Endpoint. - ## - secret: - accessKey: "" - secretKey: "" - ## csi-s3 storageClass setting - ## @param csi-s3.storageClass.create -- Specifies whether the storage class should be created. - ## @param csi-s3.storageClass.singleBucket -- Use a single bucket for all dynamically provisioned persistent volumes. - ## @param csi-s3.storageClass.mountOptions -- mount options. - ## - storageClass: - create: true - singleBucket: "" - mountOptions: "--memory-limit 1000 --dir-mode 0777 --file-mode 0666 --region cn-northwest-1" alertmanager-webhook-adaptor: ## Linkage with prometheus.enabled @@ -1650,7 +1813,7 @@ csi-hostpath-driver: create: true default: true -aws-loadbalancer-controller: +aws-load-balancer-controller: clusterName: "" enabled: false replicaCount: 1 @@ -1662,3 +1825,49 @@ aws-loadbalancer-controller: serviceAccount: create: true name: kubeblocks-service-account-aws-load-balancer-controller + affinity: + nodeAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + preference: + matchExpressions: + - key: kb-controller + operator: In + values: + - "true" + +## k8s cluster feature gates, ref: https://kubernetes.io/docs/reference/command-line-tools-reference/feature-gates/ +enabledAlphaFeatureGates: + ## @param enabledAlphaFeatureGates.recoverVolumeExpansionFailure -- Specifies whether feature gates RecoverVolumeExpansionFailure is enabled in k8s cluster. + ## + recoverVolumeExpansionFailure: false + + +agamotto: + enabled: false + image: + registry: registry.cn-hangzhou.aliyuncs.com + repository: apecloud/agamotto + pullPolicy: IfNotPresent + tag: 0.1.0-beta.2 + +## @section KubeBlocks default storageClass Parameters for cloud provider. +storageClass: + mountOptions: + - noatime + - nobarrier + provider: + eks: + volumeType: gp3 + fsType: xfs + gke: + volumeType: pd-balanced + fsType: xfs + aliyun: + volumeType: cloud_essd + fsType: xfs + aks: + volumeType: managed + fsType: xfs + tke: + volumeType: CLOUD_SSD diff --git a/deploy/kafka-cluster/Chart.yaml b/deploy/kafka-cluster/Chart.yaml index c854cb9b5..c6d706415 100644 --- a/deploy/kafka-cluster/Chart.yaml +++ b/deploy/kafka-cluster/Chart.yaml @@ -4,7 +4,7 @@ description: A Kafka server cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: 3.4.0 diff --git a/deploy/kafka-cluster/templates/_helpers.tpl b/deploy/kafka-cluster/templates/_helpers.tpl index 91a40da16..798cb279e 100644 --- a/deploy/kafka-cluster/templates/_helpers.tpl +++ b/deploy/kafka-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "kafka-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "kafka-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "kafka-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "kafka-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/kafka-cluster/templates/cluster.yaml b/deploy/kafka-cluster/templates/cluster.yaml index 788f7f938..42b1ae40a 100644 --- a/deploy/kafka-cluster/templates/cluster.yaml +++ b/deploy/kafka-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "kafka-cluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: kafka # ref clusterdefinition.name diff --git a/deploy/kafka-cluster/templates/role.yaml b/deploy/kafka-cluster/templates/role.yaml new file mode 100644 index 000000000..751e6011d --- /dev/null +++ b/deploy/kafka-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "kafka-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/kafka-cluster/templates/rolebinding.yaml b/deploy/kafka-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..dddde6ac6 --- /dev/null +++ b/deploy/kafka-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "kafka-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "kafka-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/kafka-cluster/templates/serviceaccount.yaml b/deploy/kafka-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..b24c1376d --- /dev/null +++ b/deploy/kafka-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "kafka-cluster.serviceAccountName" . }} + labels: + {{ include "kafka-cluster.labels" . | nindent 4 }} diff --git a/deploy/kafka-cluster/templates/tests/test-connection.yaml b/deploy/kafka-cluster/templates/tests/test-connection.yaml index 571418646..b65b2ab60 100644 --- a/deploy/kafka-cluster/templates/tests/test-connection.yaml +++ b/deploy/kafka-cluster/templates/tests/test-connection.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "kafka-cluster.fullname" . }}-test-connection" + name: "{{ include "clustername" . }}-test-connection" labels: {{- include "kafka-cluster.labels" . | nindent 4 }} annotations: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "kafka-cluster.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "clustername" . }}:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/deploy/kafka-cluster/values.yaml b/deploy/kafka-cluster/values.yaml index 17c1cc836..9106e496e 100644 --- a/deploy/kafka-cluster/values.yaml +++ b/deploy/kafka-cluster/values.yaml @@ -186,3 +186,10 @@ topologyKeys: ## @param affinity is affinity setting for Kafka cluster pods assignment ## affinity: {} + +nameOverride: "" +fullnameOverride: "" + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/kafka/Chart.yaml b/deploy/kafka/Chart.yaml index d8b5b22dc..a31a3078f 100644 --- a/deploy/kafka/Chart.yaml +++ b/deploy/kafka/Chart.yaml @@ -11,7 +11,7 @@ annotations: type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: 3.4.0 diff --git a/deploy/kafka/scripts/libkafka.sh b/deploy/kafka/scripts/libkafka.sh index 025dc88c9..cf8182aa2 100644 --- a/deploy/kafka/scripts/libkafka.sh +++ b/deploy/kafka/scripts/libkafka.sh @@ -616,7 +616,7 @@ kafka_configure_internal_communications() { if [[ -n "$KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL" ]]; then kafka_server_conf_set sasl.mechanism.inter.broker.protocol "$KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL" else - error "When using SASL for inter broker comunication the mechanism should be provided at KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL" + error "When using SASL for inter broker communication the mechanism should be provided at KAFKA_CFG_SASL_MECHANISM_INTER_BROKER_PROTOCOL" exit 1 fi fi diff --git a/deploy/kafka/templates/clusterdefinition.yaml b/deploy/kafka/templates/clusterdefinition.yaml index 61f4f5642..012685d79 100644 --- a/deploy/kafka/templates/clusterdefinition.yaml +++ b/deploy/kafka/templates/clusterdefinition.yaml @@ -130,16 +130,19 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: superusers + optional: false - name: KAFKA_KRAFT_CLUSTER_ID valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: kraftClusterID + optional: false # - name: KAFKA_CERTIFICATE_PASSWORD # valueFrom: # secretKeyRef: # name: $(CONN_CREDENTIAL_SECRET_NAME) # key: sslCertPassword + # optional: false ports: - name: kafka-client @@ -306,16 +309,19 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: superusers + optional: false - name: KAFKA_KRAFT_CLUSTER_ID valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: kraftClusterID + optional: false # - name: KAFKA_CERTIFICATE_PASSWORD # valueFrom: # secretKeyRef: # name: $(CONN_CREDENTIAL_SECRET_NAME) # key: sslCertPassword + # optional: false ports: - name: kafka-ctrlr @@ -472,16 +478,19 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: superusers + optional: false - name: KAFKA_KRAFT_CLUSTER_ID valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: kraftClusterID + optional: false # - name: KAFKA_CERTIFICATE_PASSWORD # valueFrom: # secretKeyRef: # name: $(CONN_CREDENTIAL_SECRET_NAME) # key: sslCertPassword + # optional: false ports: - name: kafka-client diff --git a/deploy/milvus-cluster/Chart.yaml b/deploy/milvus-cluster/Chart.yaml new file mode 100644 index 000000000..ce529730b --- /dev/null +++ b/deploy/milvus-cluster/Chart.yaml @@ -0,0 +1,9 @@ +apiVersion: v2 +name: milvus-cluster +description: A Milvus cluster Helm chart for KubeBlocks. + +type: application + +version: 0.5.1-beta.0 + +appVersion: "2.2.4" diff --git a/deploy/milvus-cluster/templates/NOTES.txt b/deploy/milvus-cluster/templates/NOTES.txt new file mode 100644 index 000000000..c3b3453e3 --- /dev/null +++ b/deploy/milvus-cluster/templates/NOTES.txt @@ -0,0 +1,2 @@ +1. Get the application URL by running these commands: + diff --git a/deploy/milvus-cluster/templates/_helpers.tpl b/deploy/milvus-cluster/templates/_helpers.tpl new file mode 100644 index 000000000..4446ad7a4 --- /dev/null +++ b/deploy/milvus-cluster/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "milvus.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "milvus.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "milvus.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "milvus.labels" -}} +helm.sh/chart: {{ include "milvus.chart" . }} +{{ include "milvus.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "milvus.selectorLabels" -}} +app.kubernetes.io/name: {{ include "milvus.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "clustername" -}} +{{ include "milvus.fullname" .}} +{{- end}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "milvus.serviceAccountName" -}} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} +{{- end }} diff --git a/deploy/milvus-cluster/templates/cluster.yaml b/deploy/milvus-cluster/templates/cluster.yaml new file mode 100644 index 000000000..68946a95a --- /dev/null +++ b/deploy/milvus-cluster/templates/cluster.yaml @@ -0,0 +1,101 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: {{ include "clustername" . }} + labels: {{ include "milvus.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: milvus # ref clusterdefinition.name + clusterVersionRef: milvus-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} # ref clusterversion.name + terminationPolicy: {{ .Values.terminationPolicy }} + affinity: + {{- with .Values.topologyKeys }} + topologyKeys: {{ . | toYaml | nindent 6 }} + {{- end }} + {{- with $.Values.tolerations }} + tolerations: {{ . | toYaml | nindent 4 }} + {{- end }} + componentSpecs: + - name: milvus # user-defined + componentDefRef: milvus # ref clusterdefinition components.name + monitor: {{ .Values.monitor.enabled | default false }} + replicas: {{ .Values.replicaCount | default 1 }} + {{- with .Values.resources }} + resources: + {{- with .limits }} + limits: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- with .requests }} + requests: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: {{ .Values.persistence.data.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.data.size }} + {{- end }} + - name: etcd # user-defined + componentDefRef: etcd # ref clusterdefinition components.name + monitor: {{ .Values.monitor.enabled | default false }} + replicas: {{ .Values.replicaCount | default 1 }} + {{- with .Values.resources }} + resources: + {{- with .limits }} + limits: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- with .requests }} + requests: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: {{ .Values.persistence.data.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.data.size }} + {{- end }} + - name: minio # user-defined + componentDefRef: minio # ref clusterdefinition components.name + monitor: {{ .Values.monitor.enabled | default false }} + replicas: {{ .Values.replicaCount | default 1 }} + {{- with .Values.resources }} + resources: + {{- with .limits }} + limits: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- with .requests }} + requests: + cpu: {{ .cpu | quote }} + memory: {{ .memory | quote }} + {{- end }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: {{ .Values.persistence.data.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.data.size }} + {{- end }} \ No newline at end of file diff --git a/deploy/milvus-cluster/values.yaml b/deploy/milvus-cluster/values.yaml new file mode 100644 index 000000000..e4aa18b5e --- /dev/null +++ b/deploy/milvus-cluster/values.yaml @@ -0,0 +1,35 @@ +# Default values for wesqlcluster. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +nameOverride: "" +fullnameOverride: "" + +replicaCount: 1 +terminationPolicy: Delete + +clusterVersionOverride: "" + +monitor: + enabled: false + +resources: { } + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + + # limits: + # cpu: 500m + # memory: 2Gi + # requests: + # cpu: 100m + # memory: 1Gi +persistence: + enabled: true + data: + storageClassName: + size: 10Gi + +serviceAccount: + name: "" \ No newline at end of file diff --git a/deploy/milvus/Chart.yaml b/deploy/milvus/Chart.yaml index 40702d561..a97fbcb67 100644 --- a/deploy/milvus/Chart.yaml +++ b/deploy/milvus/Chart.yaml @@ -1,11 +1,11 @@ apiVersion: v2 -name: milvus-standalone +name: milvus description: . type: application # This is the chart version -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 # This is the version number of milvus appVersion: "2.2.4" diff --git a/deploy/milvus/templates/backuppolicytemplate.yaml b/deploy/milvus/templates/backuppolicytemplate.yaml index 38c1d98c3..c1be0359c 100644 --- a/deploy/milvus/templates/backuppolicytemplate.yaml +++ b/deploy/milvus/templates/backuppolicytemplate.yaml @@ -1,15 +1,22 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-milvus + name: milvus-backup-policy-template labels: - clusterdefinition.kubeblocks.io/name: milvus-standalone + clusterdefinition.kubeblocks.io/name: milvus {{- include "milvus.labels" . | nindent 4 }} spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s - - credentialKeyword: - userKeyword: username - passwordKeyword: password + clusterDefinitionRef: milvus + backupPolicies: + - componentDefRef: milvus + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * 0" + snapshot: + target: + connectionCredentialKey: + passwordKey: password + usernameKey: username \ No newline at end of file diff --git a/deploy/milvus/templates/clusterdefinition.yaml b/deploy/milvus/templates/clusterdefinition.yaml index 0bc3e9753..a17ce24f7 100644 --- a/deploy/milvus/templates/clusterdefinition.yaml +++ b/deploy/milvus/templates/clusterdefinition.yaml @@ -2,7 +2,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: ClusterDefinition metadata: - name: milvus-standalone + name: milvus labels: {{- include "milvus.labels" . | nindent 4 }} spec: @@ -26,8 +26,8 @@ spec: scrapePort: 9187 logConfigs: configSpecs: - - name: milvus-standalone-config-template - templateRef: milvus-standalone-config-template + - name: milvus-config-template + templateRef: milvus-config-template volumeName: milvus-config namespace: {{.Release.Namespace}} service: @@ -106,11 +106,13 @@ spec: secretKeyRef: key: accesskey name: $(CONN_CREDENTIAL_SECRET_NAME) + optional: false - name: MINIO_SECRET_KEY valueFrom: secretKeyRef: key: secretkey name: $(CONN_CREDENTIAL_SECRET_NAME) + optional: false initContainers: - name: milvus-init command: @@ -291,9 +293,11 @@ spec: secretKeyRef: key: accesskey name: $(CONN_CREDENTIAL_SECRET_NAME) + optional: false - name: MINIO_SECRET_KEY valueFrom: secretKeyRef: key: secretkey name: $(CONN_CREDENTIAL_SECRET_NAME) + optional: false diff --git a/deploy/milvus/templates/clusterversion.yaml b/deploy/milvus/templates/clusterversion.yaml index f13a210cd..b387ecb96 100644 --- a/deploy/milvus/templates/clusterversion.yaml +++ b/deploy/milvus/templates/clusterversion.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "milvus.labels" . | nindent 4 }} spec: - clusterDefinitionRef: milvus-standalone + clusterDefinitionRef: milvus componentVersions: - componentDefRef: minio versionsContext: diff --git a/deploy/milvus/templates/configmap.yaml b/deploy/milvus/templates/configmap.yaml index 864337864..2cda5ac6a 100644 --- a/deploy/milvus/templates/configmap.yaml +++ b/deploy/milvus/templates/configmap.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: milvus-standalone-config-template + name: milvus-config-template namespace: {{ .Release.Namespace | quote }} labels: {{- include "milvus.labels" . | nindent 4 }} diff --git a/deploy/mongodb-cluster/Chart.yaml b/deploy/mongodb-cluster/Chart.yaml index 2a6b451d4..181820704 100644 --- a/deploy/mongodb-cluster/Chart.yaml +++ b/deploy/mongodb-cluster/Chart.yaml @@ -4,9 +4,9 @@ description: A MongoDB cluster Helm chart for KubeBlocks type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 -appVersion: "6.0.3" +appVersion: "5.0.14" home: https://www.mongodb.com icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png @@ -19,5 +19,5 @@ keywords: - replication maintainers: - - name: free6om + - name: xuriwuyun url: https://github.com/apecloud/kubeblocks/deploy diff --git a/deploy/mongodb-cluster/templates/NOTES.txt b/deploy/mongodb-cluster/templates/NOTES.txt index 5466e6632..55cb7d091 100644 --- a/deploy/mongodb-cluster/templates/NOTES.txt +++ b/deploy/mongodb-cluster/templates/NOTES.txt @@ -1,22 +1,18 @@ -1. Get the application URL by running these commands: -{{- if .Values.ingress.enabled }} -{{- range $host := .Values.ingress.hosts }} - {{- range .paths }} - http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} - {{- end }} -{{- end }} -{{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "mongodb-cluster.fullname" . }}) - export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") - echo http://$NODE_IP:$NODE_PORT -{{- else if contains "LoadBalancer" .Values.service.type }} - NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "mongodb-cluster.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "mongodb-cluster.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") - echo http://$SERVICE_IP:{{ .Values.service.port }} -{{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "mongodb-cluster.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") - export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") - echo "Visit http://127.0.0.1:8080 to use your application" - kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT -{{- end }} +** Please be patient while the chart is being deployed ** + +To get MongoDB connection address accessed from within your cluster: + + export MONGODB_ADDRESS=$(kubectl get secret --namespace {{ .Release.Namespace }} -l app.kubernetes.io/managed-by=kubeblocks,app.kubernetes.io/instance={{ .Release.Name }} -o jsonpath="{.items[0].data.headlessEndpoint}" | base64 -d) + +To get the root password run: + + export MONGODB_ROOT_PASSWORD=$(kubectl get secret --namespace {{ .Release.Namespace }} -l app.kubernetes.io/managed-by=kubeblocks,app.kubernetes.io/instance={{ .Release.Name}} -o jsonpath="{.items[0].data.password}" | base64 -d) + +To connect to your database, create a MongoDB client container: + + kubectl run --namespace {{ .Release.Namespace }} {{ .Release.Name }}-client --rm --tty -i --restart='Never' --env="MONGODB_ROOT_PASSWORD=$MONGODB_ROOT_PASSWORD" --env="MONGODB_ADDRESS=$MONGODB_ADDRESS" --image mongo:5.0.14 --command -- bash + +Then, run the following command: + + mongosh admin --host $MONGODB_ADDRESS --authenticationDatabase admin -u root -p $MONGODB_ROOT_PASSWORD + diff --git a/deploy/mongodb-cluster/templates/_helpers.tpl b/deploy/mongodb-cluster/templates/_helpers.tpl index 6132a557c..4b0e5b70a 100644 --- a/deploy/mongodb-cluster/templates/_helpers.tpl +++ b/deploy/mongodb-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "mongodb-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "mongodb-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "mongodb-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "mongodb-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/mongodb-cluster/templates/cluster.yaml b/deploy/mongodb-cluster/templates/cluster.yaml index 4032cfc9d..b6640a161 100644 --- a/deploy/mongodb-cluster/templates/cluster.yaml +++ b/deploy/mongodb-cluster/templates/cluster.yaml @@ -1,7 +1,8 @@ +{{- if eq .Values.architecture "sharding" }} apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }}-sharding labels: {{- include "mongodb-cluster.labels" . | nindent 4}} spec: @@ -21,7 +22,6 @@ spec: - name: shard-{{ $i }} componentDefRef: shard monitor: {{ $.Values.monitor.enabled }} - serviceType: {{ $.Values.service.type | default "ClusterIP" }} replicas: {{ .replicas | default "3" }} {{- with .tolerations }} tolerations: {{ .| toYaml | nindent 8 }} @@ -81,7 +81,6 @@ spec: - name: mongos-{{ $i }} componentDefRef: mongos monitor: {{ $.Values.monitor.enabled }} - serviceType: {{ $.Values.service.type | default "ClusterIP" }} replicas: {{ .replicas | default 1 }} {{- with .tolerations }} tolerations: {{ .| toYaml | nindent 8 }} @@ -97,4 +96,5 @@ spec: {{- end }} {{- $i := add1 $i }} {{- end }} - {{- end }} \ No newline at end of file + {{- end }} +{{- end }} diff --git a/deploy/mongodb-cluster/templates/replicaset.yaml b/deploy/mongodb-cluster/templates/replicaset.yaml new file mode 100644 index 000000000..197472d16 --- /dev/null +++ b/deploy/mongodb-cluster/templates/replicaset.yaml @@ -0,0 +1,48 @@ +{{- if eq .Values.architecture "replicaset" }} +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: {{ .Release.Name }} + labels: + {{- include "mongodb-cluster.labels" . | nindent 4}} +spec: + clusterDefinitionRef: mongodb + clusterVersionRef: mongodb-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} + terminationPolicy: {{ .Values.terminationPolicy }} + affinity: + {{- with $.Values.topologyKeys }} + topologyKeys: {{ . | toYaml | nindent 6 }} + {{- end }} + {{- with $.Values.tolerations }} + tolerations: {{ . | toYaml | nindent 4 }} + {{- end }} + componentSpecs: + - name: mongodb + componentDefRef: mongodb + monitor: {{ $.Values.monitor.enabled }} + replicas: {{ $.Values.mongodb.replicas }} + serviceAccountName: {{ include "mongodb-cluster.serviceAccountName" . }} + {{- with $.Values.mongodb.tolerations }} + tolerations: {{ .| toYaml | nindent 8 }} + {{- end }} + {{- with $.Values.mongodb.resources }} + resources: + limits: + cpu: {{ .limits.cpu | quote }} + memory: {{ .limits.memory | quote }} + requests: + cpu: {{ .requests.cpu | quote }} + memory: {{ .requests.memory | quote }} + {{- end }} + {{- if $.Values.mongodb.persistence.enabled }} + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: {{ $.Values.mongodb.persistence.data.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ $.Values.mongodb.persistence.data.size }} + {{- end }} +{{- end }} diff --git a/deploy/mongodb-cluster/templates/role.yaml b/deploy/mongodb-cluster/templates/role.yaml new file mode 100644 index 000000000..f4130b94e --- /dev/null +++ b/deploy/mongodb-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "mongodb-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/mongodb-cluster/templates/rolebinding.yaml b/deploy/mongodb-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..5086ffd11 --- /dev/null +++ b/deploy/mongodb-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "mongodb-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "mongodb-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/mongodb-cluster/templates/serviceaccount.yaml b/deploy/mongodb-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..b9dde5342 --- /dev/null +++ b/deploy/mongodb-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "mongodb-cluster.serviceAccountName" . }} + labels: + {{ include "mongodb-cluster.labels" . | nindent 4 }} diff --git a/deploy/mongodb-cluster/templates/tests/test-connection.yaml b/deploy/mongodb-cluster/templates/tests/test-connection.yaml deleted file mode 100644 index 1259080a5..000000000 --- a/deploy/mongodb-cluster/templates/tests/test-connection.yaml +++ /dev/null @@ -1,15 +0,0 @@ -apiVersion: v1 -kind: Pod -metadata: - name: "{{ include "mongodb-cluster.fullname" . }}-test-connection" - labels: - {{- include "mongodb-cluster.labels" . | nindent 4 }} - annotations: - "helm.sh/hook": test -spec: - containers: - - name: wget - image: busybox - command: ['wget'] - args: ['{{ include "mongodb-cluster.fullname" . }}:{{ .Values.service.port }}'] - restartPolicy: Never diff --git a/deploy/mongodb-cluster/values.yaml b/deploy/mongodb-cluster/values.yaml index 2e508ddaf..2912ec979 100644 --- a/deploy/mongodb-cluster/values.yaml +++ b/deploy/mongodb-cluster/values.yaml @@ -1,3 +1,5 @@ +## @param architecture define MongoDB cluster topology architecture ( `replicaset` or `sharding`) +architecture: replicaset ## @param terminationPolicy define Cluster termination policy. One of DoNotTerminate, Halt, Delete, WipeOut. ## @@ -85,6 +87,52 @@ shard: ## tolerations: [ ] +mongodb: + ## @param mongodb.replicas Number of MongoDB replicas per replicaset to deploy + ## + replicas: 3 + ## MongoDB workload pod resource requests and limits + ## ref: http://kubernetes.io/docs/user-guide/compute-resources/ + ## @param mongodb.resources.limits The resources limits for the init container + ## @param mongodb.resources.requests The requested resources for the init container + ## + resources: { } + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + ## Enable persistence using Persistent Volume Claims + ## ref: https://kubernetes.io/docs/user-guide/persistent-volumes/ + ## + persistence: + ## @param mongodb.persistence.enabled Enable persistence using Persistent Volume Claims + ## + enabled: true + ## `data` volume settings + ## + data: + ## @param mongodb.persistence.data.storageClassName Storage class of backing PVC + ## If defined, storageClassName: + ## If set to "-", storageClassName: "", which disables dynamic provisioning + ## If undefined (the default) or set to null, no storageClassName spec is + ## set, choosing the default provisioner. (gp2 on AWS, standard on + ## GKE, AWS & OpenStack) + ## + storageClassName: + ## @param mongodb.persistence.size Size of data volume + ## + size: 20Gi + ## @param mongodb.tolerations Tolerations for MongoDB pods assignment + ## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ + ## + tolerations: [ ] + configsvr: ## @param configsvr.replicas Number of MongoDB replicas per configsvr to deploy ## @@ -259,4 +307,11 @@ ingress: ## port: ## name: http ## - extraRules: [] \ No newline at end of file + extraRules: [] + +enabledLogs: + - running + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/mongodb/Chart.yaml b/deploy/mongodb/Chart.yaml index 818006870..de866f4fe 100644 --- a/deploy/mongodb/Chart.yaml +++ b/deploy/mongodb/Chart.yaml @@ -4,9 +4,9 @@ description: MongoDB is a document database designed for ease of application dev type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 -appVersion: "6.0.3" +appVersion: "5.0.14" home: https://www.mongodb.com icon: https://bitnami.com/assets/stacks/mongodb/img/mongodb-stack-220x234.png @@ -19,5 +19,5 @@ keywords: - replication maintainers: - - name: free6om + - name: xuriwuyun url: https://github.com/apecloud/kubeblocks/deploy diff --git a/deploy/mongodb/config/keyfile.tpl b/deploy/mongodb/config/keyfile.tpl new file mode 100644 index 000000000..a6010f73a --- /dev/null +++ b/deploy/mongodb/config/keyfile.tpl @@ -0,0 +1 @@ +{{ randAscii 64 | b64enc }} diff --git a/deploy/mongodb/config/mongodb5.0-config.tpl b/deploy/mongodb/config/mongodb5.0-config.tpl new file mode 100644 index 000000000..de5c5a187 --- /dev/null +++ b/deploy/mongodb/config/mongodb5.0-config.tpl @@ -0,0 +1,65 @@ +# mongod.conf +# for documentation of all options, see: +# http://docs.mongodb.org/manual/reference/configuration-options/ + +{{- $log_root := getVolumePathByName ( index $.podSpec.containers 0 ) "log" }} +{{- $mongodb_root := getVolumePathByName ( index $.podSpec.containers 0 ) "data" }} +{{- $mongodb_port_info := getPortByName ( index $.podSpec.containers 0 ) "mongodb" }} +{{- $phy_memory := getContainerMemory ( index $.podSpec.containers 0 ) }} + +# require port +{{- $mongodb_port := 27017 }} +{{- if $mongodb_port_info }} +{{- $mongodb_port = $mongodb_port_info.containerPort }} +{{- end }} + +# where and how to store data. +storage: + dbPath: {{ $mongodb_root }}/db + journal: + enabled: true + directoryPerDB: true + +# where to write logging data. +{{ block "logsBlock" . }} +systemLog: + destination: file + quiet: false + logAppend: true + logRotate: reopen + path: /data/mongodb/logs/mongodb.log + verbosity: 0 +{{ end }} + +# network interfaces +net: + port: {{ $mongodb_port }} + unixDomainSocket: + enabled: false + pathPrefix: {{ $mongodb_root }}/tmp + ipv6: false + bindIpAll: true + #bindIp: + +# replica set options +replication: + replSetName: replicaset + enableMajorityReadConcern: true + +# sharding options +#sharding: + #clusterRole: + +# process management options +processManagement: + fork: false + pidFilePath: {{ $mongodb_root }}/tmp/mongodb.pid + +# set parameter options +setParameter: + enableLocalhostAuthBypass: true + +# security options +security: + authorization: enabled + keyFile: /etc/mongodb/keyfile diff --git a/deploy/mongodb/scripts/replicaset-post-start.tpl b/deploy/mongodb/scripts/replicaset-post-start.tpl new file mode 100644 index 000000000..d4c79463a --- /dev/null +++ b/deploy/mongodb/scripts/replicaset-post-start.tpl @@ -0,0 +1,52 @@ +#!/bin/sh +# usage: replicaset-post-start.sh type_name is_configsvr +# type_name: component.type, in uppercase +# is_configsvr: true or false, default false +{{- $mongodb_root := getVolumePathByName ( index $.podSpec.containers 0 ) "data" }} +{{- $mongodb_port_info := getPortByName ( index $.podSpec.containers 0 ) "mongodb" }} + +# require port +{{- $mongodb_port := 27017 }} +{{- if $mongodb_port_info }} +{{- $mongodb_port = $mongodb_port_info.containerPort }} +{{- end }} + +PORT={{ $mongodb_port }} +MONGODB_ROOT={{ $mongodb_root }} +INDEX=$(echo $KB_POD_NAME | grep -o "\-[0-9]\+\$"); +INDEX=${INDEX#-}; +if [ $INDEX -ne 0 ]; then exit 0; fi + +until mongosh --quiet --port $PORT --eval "print('ready')"; do sleep 1; done + +RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); +RPL_SET_NAME=${RPL_SET_NAME%-}; + +TYPE_NAME=$1 +IS_CONFIGSVR=$2 +MEMBERS="" +i=0 +while [ $i -lt $(eval echo \$KB_"$TYPE_NAME"_N) ]; do + host=$(eval echo \$KB_"$TYPE_NAME"_"$i"_HOSTNAME) + host=$host"."$KB_NAMESPACE".svc.cluster.local" + until mongosh --quiet --port $PORT --host $host --eval "print('peer is ready')"; do sleep 1; done + if [ $i -eq 0 ]; then + MEMBERS="{_id: $i, host: \"$host:$PORT\", priority:2}" + else + MEMBERS="$MEMBERS,{_id: $i, host: \"$host:$PORT\"}" + fi + i=$(( i + 1)) +done +CONFIGSVR="" +if [ ""$IS_CONFIGSVR = "true" ]; then CONFIGSVR="configsvr: true,"; fi + +until is_inited=$(mongosh --quiet --port $PORT --eval "rs.status().ok" -u root --password $MONGODB_ROOT_PASSWORD || mongosh --quiet --port $PORT --eval "try { rs.status().ok } catch (e) { 0 }") ; do sleep 1; done +if [ $is_inited -ne 1 ]; then + sleep 10 + set -e + mongosh --quiet --port $PORT --eval "rs.initiate({_id: \"$RPL_SET_NAME\", $CONFIGSVR members: [$MEMBERS]})"; + set +e +fi; + +(until mongosh --quiet --port $PORT --eval "rs.isMaster().isWritablePrimary"|grep true; do sleep 1; done; +mongosh --quiet --port $PORT admin --eval "db.createUser({ user: '$MONGODB_ROOT_USER', pwd: '$MONGODB_ROOT_PASSWORD', roles: [{role: 'root', db: 'admin'}] })") /dev/null 2>&1 & diff --git a/deploy/mongodb/scripts/replicaset-restore.tpl b/deploy/mongodb/scripts/replicaset-restore.tpl new file mode 100644 index 000000000..eba4ca7b7 --- /dev/null +++ b/deploy/mongodb/scripts/replicaset-restore.tpl @@ -0,0 +1,28 @@ +#!/bin/sh + +set -e +PORT=27017 +MONGODB_ROOT=/data/mongodb +mkdir -p $MONGODB_ROOT/db +mkdir -p $MONGODB_ROOT/logs +mkdir -p $MONGODB_ROOT/tmp + +res=`ls -A ${DATA_DIR}` +if [ ! -z ${res} ]; then + echo "${DATA_DIR} is not empty! Please make sure that the directory is empty before restoring the backup." + exit 1 +fi +tar -xvf ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz -C ${DATA_DIR}/../ +mv ${DATA_DIR}/../${BACKUP_NAME}/* ${DATA_DIR} +RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); +RPL_SET_NAME=${RPL_SET_NAME%-}; +MODE=$1 +mongod $MODE --bind_ip_all --port $PORT --dbpath $MONGODB_ROOT/db --directoryperdb --logpath $MONGODB_ROOT/logs/mongodb.log --logappend --pidfilepath $MONGODB_ROOT/tmp/mongodb.pid& +until mongosh --quiet --port $PORT --host $host --eval "print('peer is ready')"; do sleep 1; done +PID=`cat $MONGODB_ROOT/tmp/mongodb.pid` + +mongosh --quiet --port $PORT local --eval "db.system.replset.deleteOne({})" +mongosh --quiet --port $PORT local --eval "db.system.replset.find()" +mongosh --quiet --port $PORT admin --eval 'db.dropUser("root", {w: "majority", wtimeout: 4000})' || true +kill $PID +wait $PID diff --git a/deploy/mongodb/scripts/replicaset-setup.tpl b/deploy/mongodb/scripts/replicaset-setup.tpl new file mode 100644 index 000000000..2bef290a3 --- /dev/null +++ b/deploy/mongodb/scripts/replicaset-setup.tpl @@ -0,0 +1,20 @@ +#!/bin/sh + +{{- $mongodb_root := getVolumePathByName ( index $.podSpec.containers 0 ) "data" }} +{{- $mongodb_port_info := getPortByName ( index $.podSpec.containers 0 ) "mongodb" }} + +# require port +{{- $mongodb_port := 27017 }} +{{- if $mongodb_port_info }} +{{- $mongodb_port = $mongodb_port_info.containerPort }} +{{- end }} + +PORT={{ $mongodb_port }} +MONGODB_ROOT={{ $mongodb_root }} +RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); +RPL_SET_NAME=${RPL_SET_NAME%-}; +mkdir -p $MONGODB_ROOT/db +mkdir -p $MONGODB_ROOT/logs +mkdir -p $MONGODB_ROOT/tmp +MODE=$1 +mongod $MODE --bind_ip_all --port $PORT --replSet $RPL_SET_NAME --config /etc/mongodb/mongodb.conf diff --git a/deploy/mongodb/templates/_helpers.tpl b/deploy/mongodb/templates/_helpers.tpl index 87b44f5ba..d2b8c7f68 100644 --- a/deploy/mongodb/templates/_helpers.tpl +++ b/deploy/mongodb/templates/_helpers.tpl @@ -49,3 +49,29 @@ Selector labels app.kubernetes.io/name: {{ include "mongodb.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} + + +{{/* +Return MongoDB service port +*/}} +{{- define "mongodb.service.port" -}} +{{- .Values.primary.service.ports.mongodb -}} +{{- end -}} + +{{/* +Return the name for a custom database to create +*/}} +{{- define "mongodb.database" -}} +{{- .Values.auth.database -}} +{{- end -}} + +{{/* +Get the password key. +*/}} +{{- define "mongodb.password" -}} +{{- if or (.Release.IsInstall) (not (lookup "apps.kubeblocks.io/v1alpha1" "ClusterDefinition" "" "mongodb")) -}} +{{ .Values.auth.password | default "$(RANDOM_PASSWD)"}} +{{- else -}} +{{ index (lookup "apps.kubeblocks.io/v1alpha1" "ClusterDefinition" "" "mongodb").spec.connectionCredential "password"}} +{{- end }} +{{- end }} diff --git a/deploy/mongodb/templates/backuppolicytemplate.yaml b/deploy/mongodb/templates/backuppolicytemplate.yaml new file mode 100644 index 000000000..1002039f5 --- /dev/null +++ b/deploy/mongodb/templates/backuppolicytemplate.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: BackupPolicyTemplate +metadata: + name: mongodb-backup-policy-template + labels: + clusterdefinition.kubeblocks.io/name: mongodb + {{- include "mongodb.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: mongodb + backupPolicies: + - componentDefRef: mongodb + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * *" + datafile: + enable: false + cronExpression: "0 18 * * *" + snapshot: + target: + role: primary + connectionCredentialKey: + passwordKey: password + usernameKey: username + datafile: + backupToolName: mongodb-physical-backup-tool + backupsHistoryLimit: 7 + target: + role: primary diff --git a/deploy/mongodb/templates/backuptool.yaml b/deploy/mongodb/templates/backuptool.yaml new file mode 100644 index 000000000..3c8b267ca --- /dev/null +++ b/deploy/mongodb/templates/backuptool.yaml @@ -0,0 +1,51 @@ +apiVersion: dataprotection.kubeblocks.io/v1alpha1 +kind: BackupTool +metadata: + name: mongodb-physical-backup-tool + labels: + clusterdefinition.kubeblocks.io/name: mongodb + {{- include "mongodb.labels" . | nindent 4 }} +spec: + image: mongo:5.0.14 + deployKind: job + env: + - name: DATA_DIR + value: /data/mongodb/db + physical: + restoreCommands: + - | + set -e + mkdir -p ${DATA_DIR} + res=`ls -A ${DATA_DIR}` + if [ ! -z "${res}" ]; then + echo "${DATA_DIR} is not empty! Please make sure that the directory is empty before restoring the backup." + exit 1 + fi + tar -xvf ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz -C ${DATA_DIR} + PORT=27017 + MONGODB_ROOT=/data/mongodb + RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); + RPL_SET_NAME=${RPL_SET_NAME%-}; + mkdir -p $MONGODB_ROOT/db + mkdir -p $MONGODB_ROOT/logs + mkdir -p $MONGODB_ROOT/tmp + MODE=$1 + mongod $MODE --bind_ip_all --port $PORT --dbpath $MONGODB_ROOT/db --directoryperdb --logpath $MONGODB_ROOT/logs/mongodb.log --logappend --pidfilepath $MONGODB_ROOT/tmp/mongodb.pid& + until mongosh --quiet --port $PORT --host $host --eval "print('peer is ready')"; do sleep 1; done + PID=`cat $MONGODB_ROOT/tmp/mongodb.pid` + + mongosh --quiet --port $PORT local --eval "db.system.replset.deleteOne({})" + mongosh --quiet --port $PORT local --eval "db.system.replset.find()" + mongosh --quiet --port $PORT admin --eval 'db.dropUser("root", {w: "majority", wtimeout: 4000})' || true + kill $PID + wait $PID + incrementalRestoreCommands: [] + logical: + restoreCommands: [] + incrementalRestoreCommands: [] + backupCommands: + - | + mkdir -p ${BACKUP_DIR} && cd ${DATA_DIR} + tar -czvf ${BACKUP_DIR}/${BACKUP_NAME}.tar.gz ./ + echo $? + incrementalBackupCommands: [] diff --git a/deploy/mongodb/templates/clusterdefinition.yaml b/deploy/mongodb/templates/clusterdefinition.yaml index a74a93036..dca12aad4 100644 --- a/deploy/mongodb/templates/clusterdefinition.yaml +++ b/deploy/mongodb/templates/clusterdefinition.yaml @@ -7,91 +7,47 @@ metadata: spec: type: mongodb connectionCredential: - username: admin - password: "" + username: root + password: {{ (include "mongodb.password" .) | quote }} + endpoint: "$(SVC_FQDN):$(SVC_PORT_mongodb)" + host: "$(SVC_FQDN)" + port: "$(SVC_PORT_mongodb)" + headlessEndpoint: "$(KB_CLUSTER_COMP_NAME)-0.$(HEADLESS_SVC_FQDN):$(SVC_PORT_mongodb)" + headlessHost: "$(KB_CLUSTER_COMP_NAME)-0.$(HEADLESS_SVC_FQDN)" + headlessPort: "$(SVC_PORT_mongodb)" componentDefs: - - name: mongos + - name: mongodb + characterType: mongodb scriptSpecs: - name: mongodb-scripts templateRef: mongodb-scripts volumeName: scripts namespace: {{ .Release.Namespace }} defaultMode: 493 - workloadType: Stateless - service: - ports: - - name: mongos - port: 27017 - targetPort: mongos - podSpec: - containers: - - name: mongos - ports: - - name: mongos - containerPort: 27017 - command: - - /scripts/mongos-setup.sh - volumeMounts: - - name: scripts - mountPath: /scripts/mongos-setup.sh - subPath: mongos-setup.sh - - name: configsvr - scriptSpecs: - - name: mongodb-scripts - templateRef: mongodb-scripts - volumeName: scripts + configSpecs: + - name: mongodb-config + templateRef: mongodb5.0-config-template namespace: {{ .Release.Namespace }} - defaultMode: 493 - characterType: mongodb - workloadType: Consensus - consensusSpec: - leader: - name: "primary" - accessMode: ReadWrite - followers: - - name: "secondary" - accessMode: Readonly - updateStrategy: Serial - probes: - roleChangedProbe: - periodSeconds: 2 - failureThreshold: 3 - service: - ports: - - name: configsvr - port: 27018 - targetPort: configsvr - podSpec: - containers: - - name: configsvr - ports: - - name: configsvr - containerPort: 27018 - command: - - /scripts/replicaset-setup.sh - - --configsvr - lifecycle: - postStart: - exec: - command: - - /scripts/replicaset-post-start.sh - - CONFIGSVR - - "true" - volumeMounts: - - name: scripts - mountPath: /scripts/replicaset-setup.sh - subPath: replicaset-setup.sh - - name: scripts - mountPath: /scripts/replicaset-post-start.sh - subPath: replicaset-post-start.sh - - name: shard - scriptSpecs: - - name: mongodb-scripts - templateRef: mongodb-scripts - volumeName: scripts + volumeName: mongodb-config + constraintRef: mongodb-config-constraints + keys: + - mongodb.conf + defaultMode: 256 + - name: mongodb-metrics-config + templateRef: mongodb-metrics-config namespace: {{ .Release.Namespace }} - defaultMode: 493 - characterType: mongodb + volumeName: mongodb-metrics-config + defaultMode: 0777 + monitor: + builtIn: false + exporterConfig: + scrapePath: /metrics + scrapePort: 9216 + logConfigs: + {{- range $name,$pattern := .Values.logConfigs }} + - name: {{ $name }} + filePathPattern: {{ $pattern }} + {{- end }} workloadType: Consensus consensusSpec: leader: @@ -100,43 +56,89 @@ spec: followers: - name: "secondary" accessMode: Readonly - updateStrategy: BestEffortParallel + updateStrategy: Serial probes: - roleChangedProbe: - periodSeconds: 2 - failureThreshold: 3 + roleProbeTimeoutAfterPodsReady: 300 + roleProbe: + failureThreshold: {{ .Values.roleProbe.failureThreshold }} + periodSeconds: {{ .Values.roleProbe.periodSeconds }} + timeoutSeconds: {{ .Values.roleProbe.timeoutSeconds }} service: ports: - - name: shard - port: 27018 - targetPort: shard + - name: mongodb + protocol: TCP + port: 27017 + volumeTypes: + - name: data + type: data podSpec: containers: - - name: shard + - name: mongodb ports: - - name: shard - containerPort: 27018 + - name: mongodb + protocol: TCP + containerPort: 27017 command: - /scripts/replicaset-setup.sh - - --shardsvr + env: + - name: MONGODB_ROOT_USER + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: username + optional: false + - name: MONGODB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: password + optional: false lifecycle: postStart: exec: command: - /scripts/replicaset-post-start.sh - - SHARD - - "false" + - MONGODB volumeMounts: + - mountPath: /data/mongodb + name: data + - mountPath: /etc/mongodb/mongodb.conf + name: mongodb-config + subPath: mongodb.conf + - mountPath: /etc/mongodb/keyfile + name: mongodb-config + subPath: keyfile - name: scripts mountPath: /scripts/replicaset-setup.sh subPath: replicaset-setup.sh - name: scripts mountPath: /scripts/replicaset-post-start.sh subPath: replicaset-post-start.sh - - name: agent + - name: metrics + image: {{ .Values.metrics.image.registry | default "docker.io" }}/{{ .Values.metrics.image.repository }}:{{ .Values.metrics.image.tag }} + imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} + securityContext: + runAsNonRoot: true + runAsUser: 1001 + env: + - name: MONGODB_ROOT_USER + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: username + optional: false + - name: MONGODB_ROOT_PASSWORD + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: password + optional: false command: - - /scripts/shard-agent.sh + - "/bin/agamotto" + - "--config=/opt/conf/metrics-config.yaml" + ports: + - name: http-metrics + containerPort: 9216 volumeMounts: - - name: scripts - mountPath: /scripts/shard-agent.sh - subPath: shard-agent.sh + - name: mongodb-metrics-config + mountPath: /opt/conf diff --git a/deploy/mongodb/templates/clusterversion.yaml b/deploy/mongodb/templates/clusterversion.yaml index f0f435277..fbdea5a50 100644 --- a/deploy/mongodb/templates/clusterversion.yaml +++ b/deploy/mongodb/templates/clusterversion.yaml @@ -7,23 +7,12 @@ metadata: spec: clusterDefinitionRef: mongodb componentVersions: - - componentDefRef: mongos + - componentDefRef: mongodb versionsContext: containers: - - name: mongos + - name: mongodb image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} - - componentDefRef: configsvr - versionsContext: - containers: - - name: configsvr - image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} - imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} - - componentDefRef: shard - versionsContext: - containers: - - name: shard - image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} - - name: agent - image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} - imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} \ No newline at end of file + systemAccountSpec: + cmdExecutorConfig: + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} \ No newline at end of file diff --git a/deploy/mongodb/templates/configconstraint.yaml b/deploy/mongodb/templates/configconstraint.yaml new file mode 100644 index 000000000..292151749 --- /dev/null +++ b/deploy/mongodb/templates/configconstraint.yaml @@ -0,0 +1,13 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ConfigConstraint +metadata: + name: mongodb-config-constraints + labels: + {{- include "mongodb.labels" . | nindent 4 }} +spec: + configurationSchema: + cue: "" + + # mysql configuration file format + formatterConfig: + format: yaml \ No newline at end of file diff --git a/deploy/mongodb/templates/configmap.yaml b/deploy/mongodb/templates/configmap.yaml deleted file mode 100644 index 67926afc5..000000000 --- a/deploy/mongodb/templates/configmap.yaml +++ /dev/null @@ -1,75 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: mongodb-scripts - labels: - {{- include "mongodb.labels" . | nindent 4 }} -data: - mongos-setup.sh: |- - #!/bin/sh - - PORT=27018 - CONFIG_SVR_NAME=$KB_CLUSTER_NAME"-configsvr" - DOMAIN=$CONFIG_SVR_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" - mongos --bind_ip_all --configdb $CONFIG_SVR_NAME/$CONFIG_SVR_NAME"-0."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-1."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-2."$DOMAIN:$PORT - replicaset-setup.sh: |- - #!/bin/sh - - RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); - RPL_SET_NAME=${RPL_SET_NAME%-}; - PORT=27018 - MODE=$1 - mongod $MODE --bind_ip_all --port $PORT --replSet $RPL_SET_NAME - replicaset-post-start.sh: |- - #!/bin/sh - # usage: replicaset-post-start.sh type_name is_configsvr - # type_name: component.type, in uppercase - # is_configsvr: true or false, default false - INDEX=$(echo $KB_POD_NAME | grep -o "\-[0-9]\+\$"); - INDEX=${INDEX#-}; - if [ $INDEX -ne 0 ]; then exit 0; fi - - PORT=27018 - until mongosh --port $PORT --eval "print('ready')"; do sleep 1; done - - RPL_SET_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); - RPL_SET_NAME=${RPL_SET_NAME%-}; - - TYPE_NAME=$1 - IS_CONFIGSVR=$2 - MEMBERS="" - i=0 - while [ $i -lt $(eval echo \$KB_"$TYPE_NAME"_N) ]; do - if [ $i -ne 0 ]; then MEMBERS="$MEMBERS,"; fi - host=$(eval echo \$KB_"$TYPE_NAME"_"$i"_HOSTNAME) - host=$host"."$KB_NAMESPACE".svc.cluster.local" - until mongosh --port $PORT --host $host --eval "print('peer is ready')"; do sleep 1; done - MEMBERS="$MEMBERS{_id: $i, host: \"$host:$PORT\"}" - i=$(( i + 1)) - done - CONFIGSVR="" - if [ $IS_CONFIGSVR = "true" ]; then CONFIGSVR="configsvr: true,"; fi - mongosh --port $PORT --eval "rs.initiate({_id: \"$RPL_SET_NAME\", $CONFIGSVR members: [$MEMBERS]})" - shard-agent.sh: |- - #!/bin/sh - - INDEX=$(echo $KB_POD_NAME | grep -o "\-[0-9]\+\$"); - INDEX=${INDEX#-}; - if [ $INDEX -ne 0 ]; then - trap : TERM INT; (while true; do sleep 1000; done) & wait - fi - - # wait main container ready - PORT=27018 - until mongosh --port $PORT --eval "rs.status().ok"; do sleep 1; done - # add shard to mongos - SHARD_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); - SHARD_NAME=${SHARD_NAME%-}; - DOMAIN=$SHARD_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" - MONGOS_HOST=$KB_CLUSTER_NAME"-mongos" - MONGOS_PORT=27017 - SHARD_CONFIG=$SHARD_NAME/$SHARD_NAME"-0."$DOMAIN:$PORT,$SHARD_NAME"-1."$DOMAIN:$PORT,$SHARD_NAME"-2."$DOMAIN:$PORT - until mongosh --host $MONGOS_HOST --port $MONGOS_PORT --eval "print('service is ready')"; do sleep 1; done - mongosh --host $MONGOS_HOST --port $MONGOS_PORT --eval "sh.addShard(\"$SHARD_CONFIG\")" - - trap : TERM INT; (while true; do sleep 1000; done) & wait \ No newline at end of file diff --git a/deploy/mongodb/templates/configtemplate.yaml b/deploy/mongodb/templates/configtemplate.yaml new file mode 100644 index 000000000..a15bda17b --- /dev/null +++ b/deploy/mongodb/templates/configtemplate.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mongodb5.0-config-template + labels: + {{- include "mongodb.labels" . | nindent 4 }} +data: + mongodb.conf: |- + {{- .Files.Get "config/mongodb5.0-config.tpl" | nindent 4 }} + keyfile: |- + {{- .Files.Get "config/keyfile.tpl" | nindent 4 }} diff --git a/deploy/mongodb/templates/metrics-configmap.yaml b/deploy/mongodb/templates/metrics-configmap.yaml new file mode 100644 index 000000000..1f305a643 --- /dev/null +++ b/deploy/mongodb/templates/metrics-configmap.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mongodb-metrics-config + labels: + {{- include "mongodb.labels" . | nindent 4 }} +data: + metrics-config.yaml: {{ toYaml .Values.metrics.config | quote }} diff --git a/deploy/mongodb/templates/scriptstemplate.yaml b/deploy/mongodb/templates/scriptstemplate.yaml new file mode 100644 index 000000000..638a41d57 --- /dev/null +++ b/deploy/mongodb/templates/scriptstemplate.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mongodb-scripts + labels: + {{- include "mongodb.labels" . | nindent 4 }} +data: + mongos-setup.sh: |- + #!/bin/sh + + PORT=27018 + CONFIG_SVR_NAME=$KB_CLUSTER_NAME"-configsvr" + DOMAIN=$CONFIG_SVR_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" + mongos --bind_ip_all --configdb $CONFIG_SVR_NAME/$CONFIG_SVR_NAME"-0."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-1."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-2."$DOMAIN:$PORT + replicaset-setup.sh: |- + {{- .Files.Get "scripts/replicaset-setup.tpl" | nindent 4 }} + replicaset-post-start.sh: |- + {{- .Files.Get "scripts/replicaset-post-start.tpl" | nindent 4 }} + shard-agent.sh: |- + #!/bin/sh + + INDEX=$(echo $KB_POD_NAME | grep -o "\-[0-9]\+\$"); + INDEX=${INDEX#-}; + if [ $INDEX -ne 0 ]; then + trap : TERM INT; (while true; do sleep 1000; done) & wait + fi + + # wait main container ready + PORT=27018 + until mongosh --quiet --port $PORT --eval "rs.status().ok"; do sleep 1; done + # add shard to mongos + SHARD_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); + SHARD_NAME=${SHARD_NAME%-}; + DOMAIN=$SHARD_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" + MONGOS_HOST=$KB_CLUSTER_NAME"-mongos" + MONGOS_PORT=27017 + SHARD_CONFIG=$SHARD_NAME/$SHARD_NAME"-0."$DOMAIN:$PORT,$SHARD_NAME"-1."$DOMAIN:$PORT,$SHARD_NAME"-2."$DOMAIN:$PORT + until mongosh --quiet --host $MONGOS_HOST --port $MONGOS_PORT --eval "print('service is ready')"; do sleep 1; done + mongosh --quiet --host $MONGOS_HOST --port $MONGOS_PORT --eval "sh.addShard(\"$SHARD_CONFIG\")" + + trap : TERM INT; (while true; do sleep 1000; done) & wait diff --git a/deploy/mongodb/templates/sharding-clusterdefinition.yaml b/deploy/mongodb/templates/sharding-clusterdefinition.yaml new file mode 100644 index 000000000..072c23f67 --- /dev/null +++ b/deploy/mongodb/templates/sharding-clusterdefinition.yaml @@ -0,0 +1,150 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterDefinition +metadata: + name: mongodb-sharding + labels: + {{- include "mongodb.labels" . | nindent 4 }} +spec: + type: mongodb + connectionCredential: + username: root + password: {{ (include "mongodb.password" .) | quote }} + endpoint: "$(SVC_FQDN):$(SVC_PORT_tcp-monogdb)" + host: "$(SVC_FQDN)" + port: "$(SVC_PORT_tcp-monogdb)" + headlessEndpoint: "$(KB_CLUSTER_COMP_NAME)-0.$(HEADLESS_SVC_FQDN):$(SVC_PORT_tcp-monogdb)" + headlessHost: "$(POD_NAME_PREFIX)-0.$(HEADLESS_SVC_FQDN)" + headlessPort: "$(SVC_PORT_tcp-monogdb)" + componentDefs: + - name: mongos + scriptSpecs: + - name: mongodb-scripts + templateRef: mongodb-sharding-scripts + volumeName: scripts + namespace: {{ .Release.Namespace }} + defaultMode: 493 + workloadType: Stateless + service: + ports: + - name: mongos + port: 27017 + targetPort: mongos + podSpec: + containers: + - name: mongos + ports: + - name: mongos + containerPort: 27017 + command: + - /scripts/mongos-setup.sh + volumeMounts: + - name: scripts + mountPath: /scripts/mongos-setup.sh + subPath: mongos-setup.sh + - name: configsvr + scriptSpecs: + - name: mongodb-scripts + templateRef: mongodb-sharding-scripts + volumeName: scripts + namespace: {{ .Release.Namespace }} + defaultMode: 493 + characterType: mongodb + workloadType: Consensus + consensusSpec: + leader: + name: "primary" + accessMode: ReadWrite + followers: + - name: "secondary" + accessMode: Readonly + updateStrategy: Serial + probes: + roleProbe: + failureThreshold: {{ .Values.roleProbe.failureThreshold }} + periodSeconds: {{ .Values.roleProbe.periodSeconds }} + timeoutSeconds: {{ .Values.roleProbe.timeoutSeconds }} + service: + ports: + - name: configsvr + port: 27018 + targetPort: configsvr + podSpec: + containers: + - name: configsvr + ports: + - name: configsvr + containerPort: 27018 + command: + - /scripts/replicaset-setup.sh + - --configsvr + lifecycle: + postStart: + exec: + command: + - /scripts/replicaset-post-start.sh + - CONFIGSVR + - "true" + volumeMounts: + - name: scripts + mountPath: /scripts/replicaset-setup.sh + subPath: replicaset-setup.sh + - name: scripts + mountPath: /scripts/replicaset-post-start.sh + subPath: replicaset-post-start.sh + - name: shard + scriptSpecs: + - name: mongodb-scripts + templateRef: mongodb-sharding-scripts + volumeName: scripts + namespace: {{ .Release.Namespace }} + defaultMode: 493 + characterType: mongodb + workloadType: Consensus + consensusSpec: + leader: + name: "primary" + accessMode: ReadWrite + followers: + - name: "secondary" + accessMode: Readonly + updateStrategy: BestEffortParallel + probes: + roleProbe: + failureThreshold: {{ .Values.roleProbe.failureThreshold }} + periodSeconds: {{ .Values.roleProbe.periodSeconds }} + timeoutSeconds: {{ .Values.roleProbe.timeoutSeconds }} + service: + ports: + - name: shard + port: 27018 + targetPort: shard + podSpec: + containers: + - name: shard + ports: + - name: shard + containerPort: 27018 + command: + - /scripts/replicaset-setup.sh + - --shardsvr + lifecycle: + postStart: + exec: + command: + - /scripts/replicaset-post-start.sh + - SHARD + - "false" + volumeMounts: + - name: scripts + mountPath: /scripts/replicaset-setup.sh + subPath: replicaset-setup.sh + - name: scripts + mountPath: /scripts/replicaset-post-start.sh + subPath: replicaset-post-start.sh + - name: agent + command: + - /scripts/shard-agent.sh + volumeMounts: + - name: scripts + mountPath: /scripts/shard-agent.sh + subPath: shard-agent.sh diff --git a/deploy/mongodb/templates/sharding-clusterversion.yaml b/deploy/mongodb/templates/sharding-clusterversion.yaml new file mode 100644 index 000000000..609e0a512 --- /dev/null +++ b/deploy/mongodb/templates/sharding-clusterversion.yaml @@ -0,0 +1,29 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterVersion +metadata: + name: mongodb-sharding-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} + labels: + {{- include "mongodb.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: mongodb-sharding + componentVersions: + - componentDefRef: mongos + versionsContext: + containers: + - name: mongos + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + - componentDefRef: configsvr + versionsContext: + containers: + - name: configsvr + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + - componentDefRef: shard + versionsContext: + containers: + - name: shard + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + - name: agent + image: {{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} diff --git a/deploy/mongodb/templates/sharding-scriptstemplate.yaml b/deploy/mongodb/templates/sharding-scriptstemplate.yaml new file mode 100644 index 000000000..d791696fd --- /dev/null +++ b/deploy/mongodb/templates/sharding-scriptstemplate.yaml @@ -0,0 +1,41 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: mongodb-sharding-scripts + labels: + {{- include "mongodb.labels" . | nindent 4 }} +data: + mongos-setup.sh: |- + #!/bin/sh + + PORT=27018 + CONFIG_SVR_NAME=$KB_CLUSTER_NAME"-configsvr" + DOMAIN=$CONFIG_SVR_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" + mongos --bind_ip_all --configdb $CONFIG_SVR_NAME/$CONFIG_SVR_NAME"-0."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-1."$DOMAIN:$PORT,$CONFIG_SVR_NAME"-2."$DOMAIN:$PORT + replicaset-setup.sh: |- + {{- .Files.Get "scripts/replicaset-setup.tpl" | nindent 4 }} + replicaset-post-start.sh: |- + {{- .Files.Get "scripts/replicaset-post-start.tpl" | nindent 4 }} + shard-agent.sh: |- + #!/bin/sh + + INDEX=$(echo $KB_POD_NAME | grep -o "\-[0-9]\+\$"); + INDEX=${INDEX#-}; + if [ $INDEX -ne 0 ]; then + trap : TERM INT; (while true; do sleep 1000; done) & wait + fi + + # wait main container ready + PORT=27018 + until mongosh --quiet --port $PORT --eval "rs.status().ok"; do sleep 1; done + # add shard to mongos + SHARD_NAME=$(echo $KB_POD_NAME | grep -o ".*-"); + SHARD_NAME=${SHARD_NAME%-}; + DOMAIN=$SHARD_NAME"-headless."$KB_NAMESPACE".svc.cluster.local" + MONGOS_HOST=$KB_CLUSTER_NAME"-mongos" + MONGOS_PORT=27017 + SHARD_CONFIG=$SHARD_NAME/$SHARD_NAME"-0."$DOMAIN:$PORT,$SHARD_NAME"-1."$DOMAIN:$PORT,$SHARD_NAME"-2."$DOMAIN:$PORT + until mongosh --quiet --host $MONGOS_HOST --port $MONGOS_PORT --eval "print('service is ready')"; do sleep 1; done + mongosh --quiet --host $MONGOS_HOST --port $MONGOS_PORT --eval "sh.addShard(\"$SHARD_CONFIG\")" + + trap : TERM INT; (while true; do sleep 1000; done) & wait diff --git a/deploy/mongodb/values.yaml b/deploy/mongodb/values.yaml index df6f33cca..6024a400b 100644 --- a/deploy/mongodb/values.yaml +++ b/deploy/mongodb/values.yaml @@ -12,8 +12,80 @@ clusterVersionOverride: "" nameOverride: "" fullnameOverride: "" -replicaset: - roleChangedProbe: - failureThreshold: 2 - periodSeconds: 1 - timeoutSeconds: 1 +roleProbe: + failureThreshold: 3 + periodSeconds: 2 + timeoutSeconds: 1 + +## Authentication parameters +## +auth: + ## @param auth.password Password for the "mongodb" admin user, leave empty + ## for random generated password. + ## + password: + ## @param auth.database Name for a custom database to create + ## + database: "admin" + +logConfigs: + running: /data/mongodb/logs/mongodb.log* + +metrics: + image: + registry: registry.cn-hangzhou.aliyuncs.com + repository: apecloud/agamotto + tag: 0.0.4 + pullPolicy: IfNotPresent + config: + extensions: + health_check: + endpoint: 0.0.0.0:13133 + path: /health/status + check_collector_pipeline: + enabled: true + interval: 2m + exporter_failure_threshold: 5 + + receivers: + apecloudmongodb: + uri: mongodb://${env:MONGODB_ROOT_USER}:${env:MONGODB_ROOT_PASSWORD}@127.0.0.1:27017/admin?ssl=false&authSource=admin + # uri: mongodb://127.0.0.1:27017 + collect-all: true + collection_interval: 15s + direct-connect: true + global-conn-pool: false + log-level: info + compatible-mode: true + + processors: + batch: + timeout: 5s + memory_limiter: + limit_mib: 1024 + spike_limit_mib: 256 + check_interval: 10s + + exporters: + prometheus: + endpoint: 0.0.0.0:9216 + const_labels: [ ] + send_timestamps: false + metric_expiration: 30s + enable_open_metrics: false + resource_to_telemetry_conversion: + enabled: true + + service: + telemetry: + logs: + level: info + metrics: + address: 0.0.0.0:8888 + pipelines: + metrics: + receivers: [apecloudmongodb] + processors: [memory_limiter] + exporters: [prometheus] + + extensions: [health_check] diff --git a/deploy/nyancat/Chart.yaml b/deploy/nyancat/Chart.yaml index 68a4c6259..0f308f828 100644 --- a/deploy/nyancat/Chart.yaml +++ b/deploy/nyancat/Chart.yaml @@ -4,8 +4,8 @@ description: A demo application for showing database cluster availability. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 -appVersion: 0.5.0-alpha.3 +appVersion: 0.5.1-beta.0 kubeVersion: '>=1.22.0-0' diff --git a/deploy/nyancat/templates/clusterrole.yaml b/deploy/nyancat/templates/clusterrole.yaml index f1eb5de40..78c8f0495 100644 --- a/deploy/nyancat/templates/clusterrole.yaml +++ b/deploy/nyancat/templates/clusterrole.yaml @@ -2,6 +2,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: name: {{ include "nyancat.fullname" . }} + labels: + {{- include "nyancat.labels" . | nindent 4 }} rules: - apiGroups: [""] resources: ["services", "pods", "secrets"] diff --git a/deploy/nyancat/templates/clusterrolebinding.yaml b/deploy/nyancat/templates/clusterrolebinding.yaml index 02e74c5a0..bf5b49d8f 100644 --- a/deploy/nyancat/templates/clusterrolebinding.yaml +++ b/deploy/nyancat/templates/clusterrolebinding.yaml @@ -2,6 +2,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: {{ include "nyancat.fullname" . }} + labels: + {{- include "nyancat.labels" . | nindent 4 }} subjects: - kind: ServiceAccount name: {{ include "nyancat.serviceAccountName" . }} diff --git a/deploy/opensearch-cluster/.helmignore b/deploy/opensearch-cluster/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deploy/opensearch-cluster/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/opensearch-cluster/Chart.yaml b/deploy/opensearch-cluster/Chart.yaml new file mode 100644 index 000000000..42ebeb36e --- /dev/null +++ b/deploy/opensearch-cluster/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: opensearch-cluster +description: A Helm chart for OpenSearch Cluster + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "2.7.0" diff --git a/deploy/opensearch-cluster/templates/NOTES.txt b/deploy/opensearch-cluster/templates/NOTES.txt new file mode 100644 index 000000000..253569192 --- /dev/null +++ b/deploy/opensearch-cluster/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "opensearch-cluster.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "opensearch-cluster.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "opensearch-cluster.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "opensearch-cluster.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/opensearch-cluster/templates/_helpers.tpl b/deploy/opensearch-cluster/templates/_helpers.tpl new file mode 100644 index 000000000..f27b863ef --- /dev/null +++ b/deploy/opensearch-cluster/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "opensearch-cluster.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "opensearch-cluster.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "opensearch-cluster.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "opensearch-cluster.labels" -}} +helm.sh/chart: {{ include "opensearch-cluster.chart" . }} +{{ include "opensearch-cluster.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "opensearch-cluster.selectorLabels" -}} +app.kubernetes.io/name: {{ include "opensearch-cluster.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{- define "clustername" -}} +{{ include "opensearch-cluster.fullname" .}} +{{- end}} + +{{/* +Create the name of the service account to use +*/}} +{{- define "opensearch-cluster.serviceAccountName" -}} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} +{{- end }} diff --git a/deploy/opensearch-cluster/templates/cluster.yaml b/deploy/opensearch-cluster/templates/cluster.yaml new file mode 100644 index 000000000..3d5c56b1c --- /dev/null +++ b/deploy/opensearch-cluster/templates/cluster.yaml @@ -0,0 +1,53 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: {{ include "clustername" . }} + labels: {{ include "opensearch-cluster.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: opensearch # ref clusterdefinition.name + clusterVersionRef: opensearch-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} # ref clusterversion.name + terminationPolicy: {{ .Values.terminationPolicy }} + affinity: + {{- with .Values.topologyKeys }} + topologyKeys: {{ . | toYaml | nindent 6 }} + {{- end }} + {{- with $.Values.tolerations }} + tolerations: {{ . | toYaml | nindent 4 }} + {{- end }} + componentSpecs: + - name: opensearch # user-defined + componentDefRef: opensearch # ref clusterdefinition componentDefs.name + monitor: {{ .Values.monitor.enabled | default false }} + replicas: {{ .Values.replicaCount | default 3 }} + {{- with .Values.resources }} + resources: + limits: + cpu: {{ .limits.cpu | quote }} + memory: {{ .limits.memory | quote }} + requests: + cpu: {{ .requests.cpu | quote }} + memory: {{ .requests.memory | quote }} + {{- end }} + {{- if .Values.persistence.enabled }} + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: {{ .Values.persistence.data.storageClassName }} + accessModes: + - ReadWriteOnce + resources: + requests: + storage: {{ .Values.persistence.data.size }} + {{- end }} + - name: dashboard # user-defined + componentDefRef: dashboard # ref clusterdefinition componentDefs.name + replicas: {{ .Values.dashboard.replicaCount | default 1 }} + {{- with .Values.dashboard.resources }} + resources: + limits: + cpu: {{ .limits.cpu | quote }} + memory: {{ .limits.memory | quote }} + requests: + cpu: {{ .requests.cpu | quote }} + memory: {{ .requests.memory | quote }} + {{- end }} diff --git a/deploy/opensearch-cluster/values.yaml b/deploy/opensearch-cluster/values.yaml new file mode 100644 index 000000000..d98c87ceb --- /dev/null +++ b/deploy/opensearch-cluster/values.yaml @@ -0,0 +1,67 @@ +# Default values for opensearch-cluster. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +terminationPolicy: Delete + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +ingress: + enabled: false + +service: + type: ClusterIP + +nodeSelector: {} + +tolerations: [] + +affinity: {} + +topologyKeys: +- kubernetes.io/hostname + +monitor: + enabled: false + +persistence: + enabled: true + data: + storageClassName: + size: 1Gi + +dashboard: + resources: {} + replicaCount: 1 + +serviceAccount: + name: \ No newline at end of file diff --git a/deploy/opensearch/.helmignore b/deploy/opensearch/.helmignore new file mode 100644 index 000000000..0e8a0eb36 --- /dev/null +++ b/deploy/opensearch/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/deploy/opensearch/Chart.yaml b/deploy/opensearch/Chart.yaml new file mode 100644 index 000000000..ce3b85797 --- /dev/null +++ b/deploy/opensearch/Chart.yaml @@ -0,0 +1,24 @@ +apiVersion: v2 +name: opensearch +description: A Helm chart for OpenSearch + +# A chart can be either an 'application' or a 'library' chart. +# +# Application charts are a collection of templates that can be packaged into versioned archives +# to be deployed. +# +# Library charts provide useful utilities or functions for the chart developer. They're included as +# a dependency of application charts to inject those utilities and functions into the rendering +# pipeline. Library charts do not define any templates and therefore cannot be deployed. +type: application + +# This is the chart version. This version number should be incremented each time you make changes +# to the chart and its templates, including the app version. +# Versions are expected to follow Semantic Versioning (https://semver.org/) +version: 0.1.0 + +# This is the version number of the application being deployed. This version number should be +# incremented each time you make changes to the application. Versions are not expected to +# follow Semantic Versioning. They should reflect the version the application is using. +# It is recommended to use it with quotes. +appVersion: "2.7.0" diff --git a/deploy/opensearch/TODO b/deploy/opensearch/TODO new file mode 100644 index 000000000..b6c208dc5 --- /dev/null +++ b/deploy/opensearch/TODO @@ -0,0 +1,6 @@ +* support plugin +* support account +* enhance security +* splitting different roles +* remove OPENSEARCH_JAVA_OPTS +* optimize NOTES.txt \ No newline at end of file diff --git a/deploy/opensearch/configs/opensearch.yaml.tpl b/deploy/opensearch/configs/opensearch.yaml.tpl new file mode 100644 index 000000000..5aab99d8c --- /dev/null +++ b/deploy/opensearch/configs/opensearch.yaml.tpl @@ -0,0 +1,52 @@ +{{- $clusterName := $.cluster.metadata.name }} + +cluster.name: {{$clusterName}} + +# Bind to all interfaces because we don't know what IP address Docker will assign to us. +network.host: 0.0.0.0 + +# Setting network.host to a non-loopback address enables the annoying bootstrap checks. "Single-node" mode disables them again. +# Implicitly done if ".singleNode" is set to "true". +# discovery.type: single-node + +# Start OpenSearch Security Demo Configuration +# WARNING: revise all the lines below before you go into production +plugins: + security: + ssl: + transport: + pemcert_filepath: esnode.pem + pemkey_filepath: esnode-key.pem + pemtrustedcas_filepath: root-ca.pem + enforce_hostname_verification: false + http: + enabled: true + pemcert_filepath: esnode.pem + pemkey_filepath: esnode-key.pem + pemtrustedcas_filepath: root-ca.pem + allow_unsafe_democertificates: true + allow_default_init_securityindex: true + authcz: + admin_dn: + - CN=kirk,OU=client,O=client,L=test,C=de + audit.type: internal_opensearch + enable_snapshot_restore_privilege: true + check_snapshot_restore_write_privileges: true + restapi: + roles_enabled: ["all_access", "security_rest_api_access"] + system_indices: + enabled: true + indices: + [ + ".opendistro-alerting-config", + ".opendistro-alerting-alert*", + ".opendistro-anomaly-results*", + ".opendistro-anomaly-detector*", + ".opendistro-anomaly-checkpoints", + ".opendistro-anomaly-detection-state", + ".opendistro-reports-*", + ".opendistro-notifications-*", + ".opendistro-notebooks", + ".opendistro-asynchronous-search-response*", + ] +######## End OpenSearch Security Demo Configuration ######## diff --git a/deploy/opensearch/templates/NOTES.txt b/deploy/opensearch/templates/NOTES.txt new file mode 100644 index 000000000..1f5c1cf2f --- /dev/null +++ b/deploy/opensearch/templates/NOTES.txt @@ -0,0 +1,22 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ .path }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "opensearch.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "opensearch.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "opensearch.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "opensearch.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT +{{- end }} diff --git a/deploy/opensearch/templates/_helpers.tpl b/deploy/opensearch/templates/_helpers.tpl new file mode 100644 index 000000000..ffd081f0d --- /dev/null +++ b/deploy/opensearch/templates/_helpers.tpl @@ -0,0 +1,62 @@ +{{/* +Expand the name of the chart. +*/}} +{{- define "opensearch.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define "opensearch.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define "opensearch.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define "opensearch.labels" -}} +helm.sh/chart: {{ include "opensearch.chart" . }} +{{ include "opensearch.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define "opensearch.selectorLabels" -}} +app.kubernetes.io/name: {{ include "opensearch.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define "opensearch.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include "opensearch.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/deploy/opensearch/templates/clusterdefinition.yaml b/deploy/opensearch/templates/clusterdefinition.yaml new file mode 100644 index 000000000..e5da99dfd --- /dev/null +++ b/deploy/opensearch/templates/clusterdefinition.yaml @@ -0,0 +1,205 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterDefinition +metadata: + name: opensearch + labels: + {{- include "opensearch.labels" . | nindent 4 }} +spec: + type: opensearch + connectionCredential: + username: root + password: "$(RANDOM_PASSWD)" + endpoint: "https://$(SVC_FQDN):$(SVC_PORT_http)" + host: "$(SVC_FQDN)" + port: "$(SVC_PORT_http)" + componentDefs: + - name: opensearch + characterType: opensearch + monitor: + builtIn: false + exporterConfig: + scrapePath: /metrics + scrapePort: 9600 + configSpecs: + - name: opensearch-config-template + templateRef: opensearch-config-template + volumeName: opensearch-config + namespace: {{.Release.Namespace}} + workloadType: Stateful + service: + ports: + - name: http + port: 9200 + targetPort: http + - name: transport + port: 9300 + targetPort: transport + volumeTypes: + - name: data + type: data + podSpec: + initContainers: + - name: fsgroup-volume + imagePullPolicy: IfNotPresent + command: ['sh', '-c'] + args: + - 'chown -R 1000:1000 /usr/share/opensearch/data' + securityContext: + runAsUser: 0 + volumeMounts: + - name: data + mountPath: /usr/share/opensearch/data + - name: sysctl + imagePullPolicy: IfNotPresent + command: + - sh + - -c + - | + set -xe + DESIRED="262144" + CURRENT=$(sysctl -n vm.max_map_count) + if [ "$DESIRED" -gt "$CURRENT" ]; then + sysctl -w vm.max_map_count=$DESIRED + fi + securityContext: + runAsUser: 0 + privileged: true + containers: + - name: opensearch + imagePullPolicy: IfNotPresent + readinessProbe: + tcpSocket: + port: 9200 + periodSeconds: 5 + timeoutSeconds: 3 + failureThreshold: 3 + startupProbe: + tcpSocket: + port: 9200 + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 3 + failureThreshold: 30 + ports: + - name: http + containerPort: 9200 + - name: transport + containerPort: 9300 + - name: metrics + containerPort: 9600 + env: + - name: node.name + valueFrom: + fieldRef: + fieldPath: metadata.name + - name: cluster.initial_master_nodes + value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-0" + - name: discovery.seed_hosts + value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-headless" + - name: cluster.name + value: "$(KB_CLUSTER_NAME)" + - name: network.host + value: "0.0.0.0" + - name: OPENSEARCH_JAVA_OPTS + value: "-Xmx512M -Xms512M" + - name: node.roles + value: "master,ingest,data,remote_cluster_client" + volumeMounts: + - mountPath: /usr/share/opensearch/data + name: data + - mountPath: /usr/share/opensearch/config/opensearch.yaml + subPath: opensearch.yaml + name: opensearch-config + - name: opensearch-master-graceful-termination-handler + imagePullPolicy: IfNotPresent + command: + - "sh" + - -c + - | + #!/usr/bin/env bash + set -eo pipefail + + http () { + local path="${1}" + if [ -n "${USERNAME}" ] && [ -n "${PASSWORD}" ]; then + BASIC_AUTH="-u ${USERNAME}:${PASSWORD}" + else + BASIC_AUTH='' + fi + curl -XGET -s -k --fail ${BASIC_AUTH} https://$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-headless:9200:${path} + } + + cleanup () { + while true ; do + local master="$(http "/_cat/master?h=node" || echo "")" + if [[ $master == "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)"* && $master != "${NODE_NAME}" ]]; then + echo "This node is not master." + break + fi + echo "This node is still master, waiting gracefully for it to step down" + sleep 1 + done + + exit 0 + } + + trap cleanup SIGTERM + + sleep infinity & + wait $! + - name: dashboard + characterType: opensearch-dashboard + workloadType: Stateless + service: + ports: + - name: http + port: 5601 + targetPort: http + podSpec: + containers: + - name: dashboard + imagePullPolicy: "{{ .Values.image.pullPolicy }}" + command: + - sh + - -c + - | + #!/usr/bin/bash + set -e + bash opensearch-dashboards-docker-entrypoint.sh opensearch-dashboards + env: + - name: OPENSEARCH_HOSTS + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: endpoint + optional: false + - name: SERVER_HOST + value: "0.0.0.0" + startupProbe: + tcpSocket: + port: 5601 + periodSeconds: 10 + timeoutSeconds: 5 + failureThreshold: 20 + successThreshold: 1 + initialDelaySeconds: 10 + livenessProbe: + tcpSocket: + port: 5601 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 10 + successThreshold: 1 + initialDelaySeconds: 10 + readinessProbe: + tcpSocket: + port: 5601 + periodSeconds: 20 + timeoutSeconds: 5 + failureThreshold: 10 + successThreshold: 1 + initialDelaySeconds: 10 + ports: + - containerPort: 5601 + name: http + protocol: TCP \ No newline at end of file diff --git a/deploy/opensearch/templates/clusterversion.yaml b/deploy/opensearch/templates/clusterversion.yaml new file mode 100644 index 000000000..b2bc2c18c --- /dev/null +++ b/deploy/opensearch/templates/clusterversion.yaml @@ -0,0 +1,31 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterVersion +metadata: + name: opensearch-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} + labels: + {{- include "opensearch.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: opensearch + componentVersions: + - componentDefRef: opensearch + versionsContext: + initContainers: + - name: fsgroup-volume + image: {{ .Values.image.registry | default "docker.io" }}/busybox:latest + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + - name: sysctl + image: {{ .Values.image.registry | default "docker.io" }}/busybox:latest + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + containers: + - name: opensearch + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + - name: opensearch-master-graceful-termination-handler + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ default .Chart.AppVersion .Values.image.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + - componentDefRef: dashboard + versionsContext: + containers: + - name: dashboard + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.dashboard.repository }}:{{ default .Chart.AppVersion .Values.image.dashboard.tag }} + imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} diff --git a/deploy/opensearch/templates/configconstraint.yaml b/deploy/opensearch/templates/configconstraint.yaml new file mode 100644 index 000000000..1e90fa824 --- /dev/null +++ b/deploy/opensearch/templates/configconstraint.yaml @@ -0,0 +1,9 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ConfigConstraint +metadata: + name: opensearch-config-constraint + labels: + {{- include "opensearch.labels" . | nindent 4 }} +spec: + formatterConfig: + format: yaml \ No newline at end of file diff --git a/deploy/opensearch/templates/configmap.yaml b/deploy/opensearch/templates/configmap.yaml new file mode 100644 index 000000000..d4d752aac --- /dev/null +++ b/deploy/opensearch/templates/configmap.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: opensearch-config-template + namespace: {{ .Release.Namespace | quote }} + labels: + {{- include "opensearch.labels" . | nindent 4 }} +data: + opensearch.yaml: |- + {{- .Files.Get "configs/opensearch.yaml.tpl" | nindent 4 }} \ No newline at end of file diff --git a/deploy/opensearch/values.yaml b/deploy/opensearch/values.yaml new file mode 100644 index 000000000..6e31c2dcb --- /dev/null +++ b/deploy/opensearch/values.yaml @@ -0,0 +1,60 @@ +# Default values for opensearch. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + registry: registry.cn-hangzhou.aliyuncs.com + repository: opensearchproject/opensearch + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart appVersion. + tag: "" + dashboard: + repository: opensearchproject/opensearch-dashboards + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" +clusterVersionOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: {} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/deploy/postgresql-cluster/Chart.yaml b/deploy/postgresql-cluster/Chart.yaml index 56c51fcc3..44df8e813 100644 --- a/deploy/postgresql-cluster/Chart.yaml +++ b/deploy/postgresql-cluster/Chart.yaml @@ -4,6 +4,14 @@ description: A PostgreSQL (with Patroni HA) cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 -appVersion: "15.2.0" +# appVersion specifies the version of the PostgreSQL (with Patroni HA) database to be created, +# and this value should be consistent with an existing clusterVersion. +# All supported clusterVersion versions can be viewed through `kubectl get clusterVersion`. +# The current default value is the highest version of the PostgreSQL (with Patroni HA) supported in KubeBlocks. +appVersion: "14.7.2" + +annotations: + kubeblocks.io/clusterVersions: "14.7.2,12.14.0" + kubeblocks.io/multiCV: "true" \ No newline at end of file diff --git a/deploy/postgresql-cluster/templates/NOTES.txt b/deploy/postgresql-cluster/templates/NOTES.txt index c3b3453e3..a970ee332 100644 --- a/deploy/postgresql-cluster/templates/NOTES.txt +++ b/deploy/postgresql-cluster/templates/NOTES.txt @@ -1,2 +1,3 @@ -1. Get the application URL by running these commands: +1. By default, the helm chart will create a PostgreSQL (with Patroni HA) cluster with the same version as the Chart appVersion. +2. If you need to create a different version, you need to specify the version through clusterVersionOverride value. diff --git a/deploy/postgresql-cluster/templates/_helpers.tpl b/deploy/postgresql-cluster/templates/_helpers.tpl index 05a757a7e..8a67c03b3 100644 --- a/deploy/postgresql-cluster/templates/_helpers.tpl +++ b/deploy/postgresql-cluster/templates/_helpers.tpl @@ -50,13 +50,26 @@ app.kubernetes.io/name: {{ include "postgresqlcluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "postgresqlcluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "postgresqlcluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "postgresqlcluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} + +{{/* +Create the name of the storageClass to use +lookup function refer: https://helm.sh/docs/chart_template_guide/functions_and_pipelines/#using-the-lookup-function +*/}} +{{- define "postgresqlcluster.storageClassName" -}} +{{- $sc := (lookup "v1" "StorageClass" "" "kb-default-sc") }} +{{- if $sc }} + {{- printf "kb-default-sc" -}} +{{- else }} + {{- printf "%s" $.Values.persistence.data.storageClassName -}} +{{- end -}} +{{- end -}} \ No newline at end of file diff --git a/deploy/postgresql-cluster/templates/cluster.yaml b/deploy/postgresql-cluster/templates/cluster.yaml index 8740f5728..22a320e3e 100644 --- a/deploy/postgresql-cluster/templates/cluster.yaml +++ b/deploy/postgresql-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "postgresqlcluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: postgresql # ref clusterdefinition.name @@ -19,6 +19,7 @@ spec: componentDefRef: postgresql # ref clusterdefinition components.name monitor: {{ .Values.monitor.enabled | default false }} replicas: {{ .Values.replicaCount | default 2 }} + serviceAccountName: {{ include "postgresqlcluster.serviceAccountName" . }} primaryIndex: {{ .Values.primaryIndex | default 0 }} switchPolicy: type: {{ .Values.switchPolicy.type}} @@ -40,7 +41,7 @@ spec: volumeClaimTemplates: - name: data # ref clusterdefinition components.containers.volumeMounts.name spec: - storageClassName: {{ .Values.persistence.data.storageClassName }} + storageClassName: {{ include "postgresqlcluster.storageClassName" . }} accessModes: - ReadWriteOnce resources: diff --git a/deploy/postgresql-cluster/templates/role.yaml b/deploy/postgresql-cluster/templates/role.yaml new file mode 100644 index 000000000..3b49d6cd2 --- /dev/null +++ b/deploy/postgresql-cluster/templates/role.yaml @@ -0,0 +1,53 @@ +{{- if .Values.serviceAccount.enabled }} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "postgresqlcluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - get + - list + - patch + - update + - watch + # delete is required only for 'patronictl remove' + - delete + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - patch + - update + - create + - list + - watch + # delete is required only for 'patronictl remove' + - delete + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create +{{- end }} diff --git a/deploy/postgresql-cluster/templates/rolebinding.yaml b/deploy/postgresql-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..a10936359 --- /dev/null +++ b/deploy/postgresql-cluster/templates/rolebinding.yaml @@ -0,0 +1,16 @@ +{{- if .Values.serviceAccount.enabled }} +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "postgresqlcluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "postgresqlcluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} +{{- end }} diff --git a/deploy/postgresql-cluster/templates/serviceaccount.yaml b/deploy/postgresql-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..b3482824c --- /dev/null +++ b/deploy/postgresql-cluster/templates/serviceaccount.yaml @@ -0,0 +1,8 @@ +{{- if .Values.serviceAccount.enabled }} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "postgresqlcluster.serviceAccountName" . }} + labels: + {{ include "postgresqlcluster.labels" . | nindent 4 }} +{{- end }} diff --git a/deploy/postgresql-cluster/templates/validate.yaml b/deploy/postgresql-cluster/templates/validate.yaml new file mode 100644 index 000000000..e5a06b71a --- /dev/null +++ b/deploy/postgresql-cluster/templates/validate.yaml @@ -0,0 +1,3 @@ +{{- if and ( not .Values.serviceAccount.enabled ) ( not .Values.serviceAccount.name ) }} + {{ fail "serviceAccount.enabled is false, the serviceAccount.name is required." }} +{{- end }} diff --git a/deploy/postgresql-cluster/values.yaml b/deploy/postgresql-cluster/values.yaml index a318813a9..f78d99d57 100644 --- a/deploy/postgresql-cluster/values.yaml +++ b/deploy/postgresql-cluster/values.yaml @@ -1,7 +1,10 @@ -# Default values for wesqlcluster. +# Default values for PostgreSQL (with Patroni HA). # This is a YAML-formatted file. # Declare variables to be passed into your templates. +nameOverride: "" +fullnameOverride: "" + replicaCount: 2 terminationPolicy: Delete @@ -15,6 +18,14 @@ switchPolicy: monitor: enabled: false +# PostgreSQL (with Patroni HA) needs the corresponding RBAC permission to create a cluster(refer to role.yaml, rolebinding.yaml, serviceaccount.yaml) +# If you need to automatically create RBAC, please ensure serviceAccount.enabled=true. +# Otherwise, the user needs to create the corresponding serviceAccount, role and roleBinding permissions manually to use PostgreSQL (with Patroni HA) normally. +serviceAccount: + enabled: true + # if enabled is false, the name is required + name: "" + resources: { } # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little @@ -26,11 +37,21 @@ resources: { } # memory: 2Gi # requests: # cpu: 100m - # memory: 1Gi + # memory: 1Gi + persistence: enabled: true data: - storageClassName: - size: 1Gi + storageClassName: "" + size: 4Gi + enabledLogs: - running + +topologyKeys: + - kubernetes.io/hostname + +## @param tolerations +## ref: https://kubernetes.io/docs/concepts/configuration/taint-and-toleration/ +## +tolerations: [ ] \ No newline at end of file diff --git a/deploy/postgresql/Chart.yaml b/deploy/postgresql/Chart.yaml index a2c45dc38..ba29d6daa 100644 --- a/deploy/postgresql/Chart.yaml +++ b/deploy/postgresql/Chart.yaml @@ -4,9 +4,16 @@ description: A PostgreSQL (with Patroni HA) cluster definition Helm chart for Ku type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 -appVersion: "15.2.0" +# The helm chart contains multiple kernel versions of PostgreSQL (with Patroni HA), +# and each PostgreSQL (with Patroni HA) version corresponds to a clusterVersion object. +# appVersion should be consistent with the highest PostgreSQL (with Patroni HA) kernel version in clusterVersion. +appVersion: "14.7.2" + +annotations: + kubeblocks.io/clusterVersions: "14.7.2,12.14.1" + kubeblocks.io/multiCV: "true" home: https://kubeblocks.io/ icon: https://github.com/apecloud/kubeblocks/raw/main/img/logo.png diff --git a/deploy/postgresql/config/pg12-config-constraint.cue b/deploy/postgresql/config/pg12-config-constraint.cue new file mode 100644 index 000000000..5d6dcc0c2 --- /dev/null +++ b/deploy/postgresql/config/pg12-config-constraint.cue @@ -0,0 +1,1112 @@ +//Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +//This file is part of KubeBlocks project +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . + +// PostgreSQL parameters: https://postgresqlco.nf/doc/en/param/ +#PGParameter: { + // Allows tablespaces directly inside pg_tblspc, for testing, pg version: 15 + allow_in_place_tablespaces?: bool + // Allows modification of the structure of system tables as well as certain other risky actions on system tables. This is otherwise not allowed even for superusers. Ill-advised use of this setting can cause irretrievable data loss or seriously corrupt the database system. + allow_system_table_mods?: bool + // Sets the application name to be reported in statistics and logs. + application_name?: string + // Sets the shell command that will be called to archive a WAL file. + archive_command?: string + // The library to use for archiving completed WAL file segments. If set to an empty string (the default), archiving via shell is enabled, and archive_command is used. Otherwise, the specified shared library is used for archiving. The WAL archiver process is restarted by the postmaster when this parameter changes. For more information, see backup-archiving-wal and archive-modules. + archive_library?: string + // When archive_mode is enabled, completed WAL segments are sent to archive storage by setting archive_command or guc-archive-library. In addition to off, to disable, there are two modes: on, and always. During normal operation, there is no difference between the two modes, but when set to always the WAL archiver is enabled also during archive recovery or standby mode. In always mode, all files restored from the archive or streamed with streaming replication will be archived (again). See continuous-archiving-in-standby for details. + archive_mode: string & "always" | "on" | "off" + // (s) Forces a switch to the next xlog file if a new file has not been started within N seconds. + archive_timeout: int & >=0 & <=2147483647 | *300 @timeDurationResource(1s) + // Enable input of NULL elements in arrays. + array_nulls?: bool + // (s) Sets the maximum allowed time to complete client authentication. + authentication_timeout?: int & >=1 & <=600 @timeDurationResource(1s) + // Use EXPLAIN ANALYZE for plan logging. + "auto_explain.log_analyze"?: bool + // Log buffers usage. + "auto_explain.log_buffers"?: bool & false | true + // EXPLAIN format to be used for plan logging. + "auto_explain.log_format"?: string & "text" | "xml" | "json" | "yaml" + + // (ms) Sets the minimum execution time above which plans will be logged. + "auto_explain.log_min_duration"?: int & >=-1 & <=2147483647 @timeDurationResource() + + // Log nested statements. + "auto_explain.log_nested_statements"?: bool & false | true + + // Collect timing data, not just row counts. + "auto_explain.log_timing"?: bool & false | true + + // Include trigger statistics in plans. + "auto_explain.log_triggers"?: bool & false | true + + // Use EXPLAIN VERBOSE for plan logging. + "auto_explain.log_verbose"?: bool & false | true + + // Fraction of queries to process. + "auto_explain.sample_rate"?: float & >=0 & <=1 + + // Starts the autovacuum subprocess. + autovacuum?: bool + + // Number of tuple inserts, updates or deletes prior to analyze as a fraction of reltuples. + autovacuum_analyze_scale_factor: float & >=0 & <=100 | *0.05 + + // Minimum number of tuple inserts, updates or deletes prior to analyze. + autovacuum_analyze_threshold?: int & >=0 & <=2147483647 + + // Age at which to autovacuum a table to prevent transaction ID wraparound. + autovacuum_freeze_max_age?: int & >=100000 & <=2000000000 + + // Sets the maximum number of simultaneously running autovacuum worker processes. + autovacuum_max_workers?: int & >=1 & <=8388607 + + // Multixact age at which to autovacuum a table to prevent multixact wraparound. + autovacuum_multixact_freeze_max_age?: int & >=10000000 & <=2000000000 + + // (s) Time to sleep between autovacuum runs. + autovacuum_naptime: int & >=1 & <=2147483 | *15 @timeDurationResource(1s) + + // (ms) Vacuum cost delay in milliseconds, for autovacuum. + autovacuum_vacuum_cost_delay?: int & >=-1 & <=100 @timeDurationResource() + + // Vacuum cost amount available before napping, for autovacuum. + autovacuum_vacuum_cost_limit?: int & >=-1 & <=10000 + + // Number of tuple inserts prior to vacuum as a fraction of reltuples. + autovacuum_vacuum_insert_scale_factor?: float & >=0 & <=100 + + // Minimum number of tuple inserts prior to vacuum, or -1 to disable insert vacuums. + autovacuum_vacuum_insert_threshold?: int & >=-1 & <=2147483647 + + // Number of tuple updates or deletes prior to vacuum as a fraction of reltuples. + autovacuum_vacuum_scale_factor: float & >=0 & <=100 | *0.1 + + // Minimum number of tuple updates or deletes prior to vacuum. + autovacuum_vacuum_threshold?: int & >=0 & <=2147483647 + + // (kB) Sets the maximum memory to be used by each autovacuum worker process. + autovacuum_work_mem?: int & >=-1 & <=2147483647 @storeResource(1KB) + + // (8Kb) Number of pages after which previously performed writes are flushed to disk. + backend_flush_after?: int & >=0 & <=256 @storeResource(8KB) + + // Sets whether "\" is allowed in string literals. + backslash_quote?: string & "safe_encoding" | "on" | "off" + + // Log backtrace for errors in these functions. + backtrace_functions?: string + + // (ms) Background writer sleep time between rounds. + bgwriter_delay?: int & >=10 & <=10000 @timeDurationResource() + + // (8Kb) Number of pages after which previously performed writes are flushed to disk. + bgwriter_flush_after?: int & >=0 & <=256 @storeResource(8KB) + + // Background writer maximum number of LRU pages to flush per round. + bgwriter_lru_maxpages?: int & >=0 & <=1000 + + // Multiple of the average buffer usage to free per round. + bgwriter_lru_multiplier?: float & >=0 & <=10 + + // Sets the output format for bytea. + bytea_output?: string & "escape" | "hex" + + // Check function bodies during CREATE FUNCTION. + check_function_bodies?: bool & false | true + + // Time spent flushing dirty buffers during checkpoint, as fraction of checkpoint interval. + checkpoint_completion_target: float & >=0 & <=1 | *0.9 + + // (8kB) Number of pages after which previously performed writes are flushed to disk. + checkpoint_flush_after?: int & >=0 & <=256 @storeResource(8KB) + + // (s) Sets the maximum time between automatic WAL checkpoints. + checkpoint_timeout?: int & >=30 & <=3600 @timeDurationResource(1s) + + // (s) Enables warnings if checkpoint segments are filled more frequently than this. + checkpoint_warning?: int & >=0 & <=2147483647 @timeDurationResource(1s) + + // time between checks for client disconnection while running queries + client_connection_check_interval?: int & >=0 & <=2147483647 @timeDurationResource() + + // Sets the clients character set encoding. + client_encoding?: string + + // Sets the message levels that are sent to the client. + client_min_messages?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "log" | "notice" | "warning" | "error" + + // Sets the delay in microseconds between transaction commit and flushing WAL to disk. + commit_delay?: int & >=0 & <=100000 + + // Sets the minimum concurrent open transactions before performing commit_delay. + commit_siblings?: int & >=0 & <=1000 + + // Enables in-core computation of a query identifier + compute_query_id?: string & "on" | "auto" + + // Sets the servers main configuration file. + config_file?: string + + // Enables the planner to use constraints to optimize queries. + constraint_exclusion?: string & "partition" | "on" | "off" + + // Sets the planners estimate of the cost of processing each index entry during an index scan. + cpu_index_tuple_cost?: float & >=0 & <=1.79769e+308 + + // Sets the planners estimate of the cost of processing each operator or function call. + cpu_operator_cost?: float & >=0 & <=1.79769e+308 + + // Sets the planners estimate of the cost of processing each tuple (row). + cpu_tuple_cost?: float & >=0 & <=1.79769e+308 + + // Sets the database to store pg_cron metadata tables + "cron.database_name"?: string + + // Log all jobs runs into the job_run_details table + "cron.log_run"?: string & "on" | "off" + + // Log all cron statements prior to execution. + "cron.log_statement"?: string & "on" | "off" + + // Maximum number of jobs that can run concurrently. + "cron.max_running_jobs": int & >=0 & <=100 | *5 + + // Enables background workers for pg_cron + "cron.use_background_workers"?: string + + // Sets the planners estimate of the fraction of a cursors rows that will be retrieved. + cursor_tuple_fraction?: float & >=0 & <=1 + + // Sets the servers data directory. + data_directory?: string + + // Sets the display format for date and time values. + datestyle?: string + + // Enables per-database user names. + db_user_namespace?: bool & false | true + + // (ms) Sets the time to wait on a lock before checking for deadlock. + deadlock_timeout?: int & >=1 & <=2147483647 @timeDurationResource() + + // Indents parse and plan tree displays. + debug_pretty_print?: bool & false | true + + // Logs each querys parse tree. + debug_print_parse?: bool & false | true + + // Logs each querys execution plan. + debug_print_plan?: bool & false | true + + // Logs each querys rewritten parse tree. + debug_print_rewritten?: bool & false | true + + // Sets the default statistics target. + default_statistics_target?: int & >=1 & <=10000 + + // Sets the default tablespace to create tables and indexes in. + default_tablespace?: string + + // Sets the default TOAST compression method for columns of newly-created tables + default_toast_compression?: string & "pglz" | "lz4" + + // Sets the default deferrable status of new transactions. + default_transaction_deferrable?: bool & false | true + + // Sets the transaction isolation level of each new transaction. + default_transaction_isolation?: string & "serializable" | "repeatable read" | "read committed" | "read uncommitted" + + // Sets the default read-only status of new transactions. + default_transaction_read_only?: bool & false | true + + // (8kB) Sets the planners assumption about the size of the disk cache. + effective_cache_size?: int & >=1 & <=2147483647 @storeResource(8KB) + + // Number of simultaneous requests that can be handled efficiently by the disk subsystem. + effective_io_concurrency?: int & >=0 & <=1000 + + // Enables or disables the query planner's use of async-aware append plan types + enable_async_append?: bool & false | true + + // Enables the planners use of bitmap-scan plans. + enable_bitmapscan?: bool & false | true + + // Enables the planner's use of gather merge plans. + enable_gathermerge?: bool & false | true + + // Enables the planners use of hashed aggregation plans. + enable_hashagg?: bool & false | true + + // Enables the planners use of hash join plans. + enable_hashjoin?: bool & false | true + + // Enables the planner's use of incremental sort steps. + enable_incremental_sort?: bool & false | true + + // Enables the planner's use of index-only-scan plans. + enable_indexonlyscan?: bool & false | true + + // Enables the planners use of index-scan plans. + enable_indexscan?: bool & false | true + + // Enables the planners use of materialization. + enable_material?: bool & false | true + + // Enables the planner's use of memoization + enable_memoize?: bool & false | true + + // Enables the planners use of merge join plans. + enable_mergejoin?: bool & false | true + + // Enables the planners use of nested-loop join plans. + enable_nestloop?: bool & false | true + + // Enables the planner's use of parallel append plans. + enable_parallel_append?: bool & false | true + + // Enables the planner's user of parallel hash plans. + enable_parallel_hash?: bool & false | true + + // Enable plan-time and run-time partition pruning. + enable_partition_pruning?: bool & false | true + + // Enables partitionwise aggregation and grouping. + enable_partitionwise_aggregate?: bool & false | true + + // Enables partitionwise join. + enable_partitionwise_join?: bool & false | true + + // Enables the planners use of sequential-scan plans. + enable_seqscan?: bool & false | true + + // Enables the planners use of explicit sort steps. + enable_sort?: bool & false | true + + // Enables the planners use of TID scan plans. + enable_tidscan?: bool & false | true + + // Warn about backslash escapes in ordinary string literals. + escape_string_warning?: bool & false | true + + // Terminate session on any error. + exit_on_error?: bool & false | true + + // Sets the number of digits displayed for floating-point values. + extra_float_digits?: int & >=-15 & <=3 + + // Forces use of parallel query facilities. + force_parallel_mode?: bool & false | true + + // Sets the FROM-list size beyond which subqueries are not collapsed. + from_collapse_limit?: int & >=1 & <=2147483647 + + // Forces synchronization of updates to disk. + fsync: bool & false | true | *true + + // Writes full pages to WAL when first modified after a checkpoint. + full_page_writes: bool & false | true | *true + + // Enables genetic query optimization. + geqo?: bool & false | true + + // GEQO: effort is used to set the default for other GEQO parameters. + geqo_effort?: int & >=1 & <=10 + + // GEQO: number of iterations of the algorithm. + geqo_generations?: int & >=0 & <=2147483647 + + // GEQO: number of individuals in the population. + geqo_pool_size?: int & >=0 & <=2147483647 @storeResource() + + // GEQO: seed for random path selection. + geqo_seed?: float & >=0 & <=1 + + // GEQO: selective pressure within the population. + geqo_selection_bias?: float & >=1.5 & <=2 + + // Sets the threshold of FROM items beyond which GEQO is used. + geqo_threshold?: int & >=2 & <=2147483647 + + // Sets the maximum allowed result for exact search by GIN. + gin_fuzzy_search_limit?: int & >=0 & <=2147483647 + + // (kB) Sets the maximum size of the pending list for GIN index. + gin_pending_list_limit?: int & >=64 & <=2147483647 @storeResource(1KB) + + // Multiple of work_mem to use for hash tables. + hash_mem_multiplier?: float & >=1 & <=1000 + + // Sets the servers hba configuration file. + hba_file?: string + + // Force group aggregation for hll + "hll.force_groupagg"?: bool & false | true + + // Allows feedback from a hot standby to the primary that will avoid query conflicts. + hot_standby_feedback?: bool & false | true + + // Use of huge pages on Linux. + huge_pages?: string & "on" | "off" | "try" + + // The size of huge page that should be requested. Controls the size of huge pages, when they are enabled with huge_pages. The default is zero (0). When set to 0, the default huge page size on the system will be used. This parameter can only be set at server start. + huge_page_size?: int & >=0 & <=2147483647 @storeResource(1KB) + + // Sets the servers ident configuration file. + ident_file?: string + + // (ms) Sets the maximum allowed duration of any idling transaction. + idle_in_transaction_session_timeout: int & >=0 & <=2147483647 | *86400000 @timeDurationResource() + + // Terminate any session that has been idle (that is, waiting for a client query), but not within an open transaction, for longer than the specified amount of time + idle_session_timeout?: int & >=0 & <=2147483647 @timeDurationResource() + + // Continues recovery after an invalid pages failure. + ignore_invalid_pages: bool & false | true | *false + + // Sets the display format for interval values. + intervalstyle?: string & "postgres" | "postgres_verbose" | "sql_standard" | "iso_8601" + + // Allow JIT compilation. + jit: bool + + // Perform JIT compilation if query is more expensive. + jit_above_cost?: float & >=-1 & <=1.79769e+308 + + // Perform JIT inlining if query is more expensive. + jit_inline_above_cost?: float & >=-1 & <=1.79769e+308 + + // Optimize JITed functions if query is more expensive. + jit_optimize_above_cost?: float & >=-1 & <=1.79769e+308 + + // Sets the FROM-list size beyond which JOIN constructs are not flattened. + join_collapse_limit?: int & >=1 & <=2147483647 + + // Sets the language in which messages are displayed. + lc_messages?: string + + // Sets the locale for formatting monetary amounts. + lc_monetary?: string + + // Sets the locale for formatting numbers. + lc_numeric?: string + + // Sets the locale for formatting date and time values. + lc_time?: string + + // Sets the host name or IP address(es) to listen to. + listen_addresses?: string + + // Enables backward compatibility mode for privilege checks on large objects. + lo_compat_privileges: bool & false | true | *false + + // (ms) Sets the minimum execution time above which autovacuum actions will be logged. + log_autovacuum_min_duration: int & >=-1 & <=2147483647 | *10000 @timeDurationResource() + + // Logs each checkpoint. + log_checkpoints: bool & false | true | *true + + // Logs each successful connection. + log_connections?: bool & false | true + + // Sets the destination for server log output. + log_destination?: string & "stderr" | "csvlog" + + // Sets the destination directory for log files. + log_directory?: string + + // Logs end of a session, including duration. + log_disconnections?: bool & false | true + + // Logs the duration of each completed SQL statement. + log_duration?: bool & false | true + + // Sets the verbosity of logged messages. + log_error_verbosity?: string & "terse" | "default" | "verbose" + + // Writes executor performance statistics to the server log. + log_executor_stats?: bool & false | true + + // Sets the file permissions for log files. + log_file_mode?: string + + // Sets the file name pattern for log files. + log_filename?: string + + // Start a subprocess to capture stderr output and/or csvlogs into log files. + logging_collector: bool & false | true | *true + + // Logs the host name in the connection logs. + log_hostname?: bool & false | true + + // (kB) Sets the maximum memory to be used for logical decoding. + logical_decoding_work_mem?: int & >=64 & <=2147483647 @storeResource(1KB) + + // Controls information prefixed to each log line. + log_line_prefix?: string + + // Logs long lock waits. + log_lock_waits?: bool & false | true + + // (ms) Sets the minimum execution time above which a sample of statements will be logged. Sampling is determined by log_statement_sample_rate. + log_min_duration_sample?: int & >=-1 & <=2147483647 @timeDurationResource() + + // (ms) Sets the minimum execution time above which statements will be logged. + log_min_duration_statement?: int & >=-1 & <=2147483647 @timeDurationResource() + + // Causes all statements generating error at or above this level to be logged. + log_min_error_statement?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "info" | "notice" | "warning" | "error" | "log" | "fatal" | "panic" + + // Sets the message levels that are logged. + log_min_messages?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "info" | "notice" | "warning" | "error" | "log" | "fatal" + + // When logging statements, limit logged parameter values to first N bytes. + log_parameter_max_length?: int & >=-1 & <=1073741823 + + // When reporting an error, limit logged parameter values to first N bytes. + log_parameter_max_length_on_error?: int & >=-1 & <=1073741823 + + // Writes parser performance statistics to the server log. + log_parser_stats?: bool & false | true + + // Writes planner performance statistics to the server log. + log_planner_stats?: bool & false | true + + // Controls whether a log message is produced when the startup process waits longer than deadlock_timeout for recovery conflicts + log_recovery_conflict_waits?: bool & false | true + + // Logs each replication command. + log_replication_commands?: bool & false | true + + // (min) Automatic log file rotation will occur after N minutes. + log_rotation_age: int & >=1 & <=1440 | *60 @timeDurationResource(1min) + + // (kB) Automatic log file rotation will occur after N kilobytes. + log_rotation_size?: int & >=0 & <=2097151 @storeResource(1KB) + + // Time between progress updates for long-running startup operations. Sets the amount of time after which the startup process will log a message about a long-running operation that is still in progress, as well as the interval between further progress messages for that operation. The default is 10 seconds. A setting of 0 disables the feature. If this value is specified without units, it is taken as milliseconds. This setting is applied separately to each operation. This parameter can only be set in the postgresql.conf file or on the server command line. + log_startup_progress_interval: int & >=0 & <=2147483647 @timeDurationResource() + + // Sets the type of statements logged. + log_statement?: string & "none" | "ddl" | "mod" | "all" + + // Fraction of statements exceeding log_min_duration_sample to be logged. + log_statement_sample_rate?: float & >=0 & <=1 + + // Writes cumulative performance statistics to the server log. + log_statement_stats?: bool + + // (kB) Log the use of temporary files larger than this number of kilobytes. + log_temp_files?: int & >=-1 & <=2147483647 @storeResource(1KB) + + // Sets the time zone to use in log messages. + log_timezone?: string + + // Set the fraction of transactions to log for new transactions. + log_transaction_sample_rate?: float & >=0 & <=1 + + // Truncate existing log files of same name during log rotation. + log_truncate_on_rotation: bool & false | true | *false + + // A variant of effective_io_concurrency that is used for maintenance work. + maintenance_io_concurrency?: int & >=0 & <=1000 + + // (kB) Sets the maximum memory to be used for maintenance operations. + maintenance_work_mem?: int & >=1024 & <=2147483647 @storeResource(1KB) + + // Sets the maximum number of concurrent connections. + max_connections?: int & >=6 & <=8388607 + + // Sets the maximum number of simultaneously open files for each server process. + max_files_per_process?: int & >=64 & <=2147483647 + + // Sets the maximum number of locks per transaction. + max_locks_per_transaction: int & >=10 & <=2147483647 | *64 + + // Maximum number of logical replication worker processes. + max_logical_replication_workers?: int & >=0 & <=262143 + + // Sets the maximum number of parallel processes per maintenance operation. + max_parallel_maintenance_workers?: int & >=0 & <=1024 + + // Sets the maximum number of parallel workers than can be active at one time. + max_parallel_workers?: int & >=0 & <=1024 + + // Sets the maximum number of parallel processes per executor node. + max_parallel_workers_per_gather?: int & >=0 & <=1024 + + // Sets the maximum number of predicate-locked tuples per page. + max_pred_locks_per_page?: int & >=0 & <=2147483647 + + // Sets the maximum number of predicate-locked pages and tuples per relation. + max_pred_locks_per_relation?: int & >=-2147483648 & <=2147483647 + + // Sets the maximum number of predicate locks per transaction. + max_pred_locks_per_transaction?: int & >=10 & <=2147483647 + + // Sets the maximum number of simultaneously prepared transactions. + max_prepared_transactions: int & >=0 & <=8388607 | *0 + + // Sets the maximum number of replication slots that the server can support. + max_replication_slots: int & >=5 & <=8388607 | *20 + + // (kB) Sets the maximum stack depth, in kilobytes. + max_stack_depth: int & >=100 & <=2147483647 | *6144 @storeResource(1KB) + + // (ms) Sets the maximum delay before canceling queries when a hot standby server is processing archived WAL data. + max_standby_archive_delay?: int & >=-1 & <=2147483647 @timeDurationResource() + + // (ms) Sets the maximum delay before canceling queries when a hot standby server is processing streamed WAL data. + max_standby_streaming_delay?: int & >=-1 & <=2147483647 @timeDurationResource() + + // Maximum number of synchronization workers per subscription + max_sync_workers_per_subscription?: int & >=0 & <=262143 + + // Sets the maximum number of simultaneously running WAL sender processes. + max_wal_senders: int & >=5 & <=8388607 | *20 + + // (MB) Sets the WAL size that triggers a checkpoint. + max_wal_size: int & >=2 & <=2147483647 | *2048 @storeResource(1MB) + + // Sets the maximum number of concurrent worker processes. + max_worker_processes?: int & >=0 & <=262143 + + // Specifies the amount of memory that should be allocated at server startup for use by parallel queries + min_dynamic_shared_memory?: int & >=0 & <=715827882 @storeResource(1MB) + + // (8kB) Sets the minimum amount of index data for a parallel scan. + min_parallel_index_scan_size?: int & >=0 & <=715827882 @storeResource(8KB) + + // Sets the minimum size of relations to be considered for parallel scan. Sets the minimum size of relations to be considered for parallel scan. + min_parallel_relation_size?: int & >=0 & <=715827882 @storeResource(8KB) + + // (8kB) Sets the minimum amount of table data for a parallel scan. + min_parallel_table_scan_size?: int & >=0 & <=715827882 @storeResource(8KB) + + // (MB) Sets the minimum size to shrink the WAL to. + min_wal_size: int & >=2 & <=2147483647 | *192 @storeResource(1MB) + + // (min) Time before a snapshot is too old to read pages changed after the snapshot was taken. + old_snapshot_threshold?: int & >=-1 & <=86400 @timeDurationResource(1min) + + // Emulate oracle's date output behaviour. + "orafce.nls_date_format"?: string + + // Specify timezone used for sysdate function. + "orafce.timezone"?: string + + // Controls whether Gather and Gather Merge also run subplans. + parallel_leader_participation?: bool & false | true + + // Sets the planner's estimate of the cost of starting up worker processes for parallel query. + parallel_setup_cost?: float & >=0 & <=1.79769e+308 + + // Sets the planner's estimate of the cost of passing each tuple (row) from worker to master backend. + parallel_tuple_cost?: float & >=0 & <=1.79769e+308 + + // Encrypt passwords. + password_encryption?: string & "md5" | "scram-sha-256" + + // Specifies which classes of statements will be logged by session audit logging. + "pgaudit.log"?: string & "ddl" | "function" | "misc" | "read" | "role" | "write" | "none" | "all" | "-ddl" | "-function" | "-misc" | "-read" | "-role" | "-write" + + // Specifies that session logging should be enabled in the case where all relations in a statement are in pg_catalog. + "pgaudit.log_catalog"?: bool & false | true + + // Specifies the log level that will be used for log entries. + "pgaudit.log_level"?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "info" | "notice" | "warning" | "log" + + // Specifies that audit logging should include the parameters that were passed with the statement. + "pgaudit.log_parameter"?: bool & false | true + + // Specifies whether session audit logging should create a separate log entry for each relation (TABLE, VIEW, etc.) referenced in a SELECT or DML statement. + "pgaudit.log_relation"?: bool & false | true + + // Specifies that audit logging should include the rows retrieved or affected by a statement. + "pgaudit.log_rows": bool & false | true | *false + + // Specifies whether logging will include the statement text and parameters (if enabled). + "pgaudit.log_statement": bool & false | true | *true + + // Specifies whether logging will include the statement text and parameters with the first log entry for a statement/substatement combination or with every entry. + "pgaudit.log_statement_once"?: bool & false | true + + // Specifies the master role to use for object audit logging. + "pgaudit.role"?: string + + // It specifies whether to perform Recheck which is an internal process of full text search. + "pg_bigm.enable_recheck"?: string & "on" | "off" + + // It specifies the maximum number of 2-grams of the search keyword to be used for full text search. + "pg_bigm.gin_key_limit": int & >=0 & <=2147483647 | *0 + + // It specifies the minimum threshold used by the similarity search. + "pg_bigm.similarity_limit": float & >=0 & <=1 | *0.3 + + // Logs results of hint parsing. + "pg_hint_plan.debug_print"?: string & "off" | "on" | "detailed" | "verbose" + + // Force planner to use plans specified in the hint comment preceding to the query. + "pg_hint_plan.enable_hint"?: bool & false | true + + // Force planner to not get hint by using table lookups. + "pg_hint_plan.enable_hint_table"?: bool & false | true + + // Message level of debug messages. + "pg_hint_plan.message_level"?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "log" | "info" | "notice" | "warning" | "error" + + // Message level of parse errors. + "pg_hint_plan.parse_messages"?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "log" | "info" | "notice" | "warning" | "error" + + // Batch inserts if possible + "pglogical.batch_inserts"?: bool & false | true + + // Sets log level used for logging resolved conflicts. + "pglogical.conflict_log_level"?: string & "debug5" | "debug4" | "debug3" | "debug2" | "debug1" | "info" | "notice" | "warning" | "error" | "log" | "fatal" | "panic" + + // Sets method used for conflict resolution for resolvable conflicts. + "pglogical.conflict_resolution"?: string & "error" | "apply_remote" | "keep_local" | "last_update_wins" | "first_update_wins" + + // connection options to add to all peer node connections + "pglogical.extra_connection_options"?: string + + // pglogical specific synchronous commit value + "pglogical.synchronous_commit"?: bool & false | true + + // Use SPI instead of low-level API for applying changes + "pglogical.use_spi"?: bool & false | true + + // Starts the autoprewarm worker. + "pg_prewarm.autoprewarm"?: bool & false | true + + // Sets the interval between dumps of shared buffers + "pg_prewarm.autoprewarm_interval"?: int & >=0 & <=2147483 + + // Sets if the result value is normalized or not. + "pg_similarity.block_is_normalized"?: bool & false | true + + // Sets the threshold used by the Block similarity function. + "pg_similarity.block_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Block similarity function. + "pg_similarity.block_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.cosine_is_normalized"?: bool & false | true + + // Sets the threshold used by the Cosine similarity function. + "pg_similarity.cosine_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Cosine similarity function. + "pg_similarity.cosine_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.dice_is_normalized"?: bool & false | true + + // Sets the threshold used by the Dice similarity measure. + "pg_similarity.dice_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Dice similarity measure. + "pg_similarity.dice_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.euclidean_is_normalized"?: bool & false | true + + // Sets the threshold used by the Euclidean similarity measure. + "pg_similarity.euclidean_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Euclidean similarity measure. + "pg_similarity.euclidean_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.hamming_is_normalized"?: bool & false | true + + // Sets the threshold used by the Block similarity metric. + "pg_similarity.hamming_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.jaccard_is_normalized"?: bool & false | true + + // Sets the threshold used by the Jaccard similarity measure. + "pg_similarity.jaccard_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Jaccard similarity measure. + "pg_similarity.jaccard_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.jaro_is_normalized"?: bool & false | true + + // Sets the threshold used by the Jaro similarity measure. + "pg_similarity.jaro_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.jarowinkler_is_normalized"?: bool & false | true + + // Sets the threshold used by the Jarowinkler similarity measure. + "pg_similarity.jarowinkler_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.levenshtein_is_normalized"?: bool & false | true + + // Sets the threshold used by the Levenshtein similarity measure. + "pg_similarity.levenshtein_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.matching_is_normalized"?: bool & false | true + + // Sets the threshold used by the Matching Coefficient similarity measure. + "pg_similarity.matching_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Matching Coefficient similarity measure. + "pg_similarity.matching_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.mongeelkan_is_normalized"?: bool & false | true + + // Sets the threshold used by the Monge-Elkan similarity measure. + "pg_similarity.mongeelkan_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Monge-Elkan similarity measure. + "pg_similarity.mongeelkan_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets the gap penalty used by the Needleman-Wunsch similarity measure. + "pg_similarity.nw_gap_penalty"?: float & >=-9.22337e+18 & <=9.22337e+18 + + // Sets if the result value is normalized or not. + "pg_similarity.nw_is_normalized"?: bool & false | true + + // Sets the threshold used by the Needleman-Wunsch similarity measure. + "pg_similarity.nw_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.overlap_is_normalized"?: bool & false | true + + // Sets the threshold used by the Overlap Coefficient similarity measure. + "pg_similarity.overlap_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Overlap Coefficientsimilarity measure. + "pg_similarity.overlap_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.qgram_is_normalized"?: bool & false | true + + // Sets the threshold used by the Q-Gram similarity measure. + "pg_similarity.qgram_threshold"?: float & >=0 & <=1 + + // Sets the tokenizer for Q-Gram measure. + "pg_similarity.qgram_tokenizer"?: string & "alnum" | "gram" | "word" | "camelcase" + + // Sets if the result value is normalized or not. + "pg_similarity.swg_is_normalized"?: bool & false | true + + // Sets the threshold used by the Smith-Waterman-Gotoh similarity measure. + "pg_similarity.swg_threshold"?: float & >=0 & <=1 + + // Sets if the result value is normalized or not. + "pg_similarity.sw_is_normalized"?: bool & false | true + + // Sets the threshold used by the Smith-Waterman similarity measure. + "pg_similarity.sw_threshold"?: float & >=0 & <=1 + + // Sets the maximum number of statements tracked by pg_stat_statements. + "pg_stat_statements.max"?: int & >=100 & <=2147483647 + + // Save pg_stat_statements statistics across server shutdowns. + "pg_stat_statements.save"?: bool & false | true + + // Selects which statements are tracked by pg_stat_statements. + "pg_stat_statements.track"?: string & "none" | "top" | "all" + + // Selects whether planning duration is tracked by pg_stat_statements. + "pg_stat_statements.track_planning"?: bool & false | true + + // Selects whether utility commands are tracked by pg_stat_statements. + "pg_stat_statements.track_utility"?: bool & false | true + + // Sets the behavior for interacting with passcheck feature. + "pgtle.enable_password_check"?: string & "on" | "off" | "require" + + // Number of workers to use for a physical transport. + "pg_transport.num_workers"?: int & >=1 & <=32 + + // Specifies whether to report timing information during transport. + "pg_transport.timing"?: bool & false | true + + // (kB) Amount of memory each worker can allocate for a physical transport. + "pg_transport.work_mem"?: int & >=65536 & <=2147483647 @storeResource(1KB) + + // Controls the planner selection of custom or generic plan. + plan_cache_mode?: string & "auto" | "force_generic_plan" | "force_custom_plan" + + // Sets the TCP port the server listens on. + port?: int & >=1 & <=65535 + + // Sets the amount of time to wait after authentication on connection startup. The amount of time to delay when a new server process is started, after it conducts the authentication procedure. This is intended to give developers an opportunity to attach to the server process with a debugger. If this value is specified without units, it is taken as seconds. A value of zero (the default) disables the delay. This parameter cannot be changed after session start. + post_auth_delay?: int & >=0 & <=2147 @timeDurationResource(1s) + + // Sets the amount of time to wait before authentication on connection startup. The amount of time to delay just after a new server process is forked, before it conducts the authentication procedure. This is intended to give developers an opportunity to attach to the server process with a debugger to trace down misbehavior in authentication. If this value is specified without units, it is taken as seconds. A value of zero (the default) disables the delay. This parameter can only be set in the postgresql.conf file or on the server command line. + pre_auth_delay?: int & >=0 & <=60 @timeDurationResource(1s) + + // Enable for disable GDAL drivers used with PostGIS in Postgres 9.3.5 and above. + "postgis.gdal_enabled_drivers"?: string & "ENABLE_ALL" | "DISABLE_ALL" + + // When generating SQL fragments, quote all identifiers. + quote_all_identifiers?: bool & false | true + + // Sets the planners estimate of the cost of a nonsequentially fetched disk page. + random_page_cost?: float & >=0 & <=1.79769e+308 + + // Lower threshold of Dice similarity. Molecules with similarity lower than threshold are not similar by # operation. + "rdkit.dice_threshold"?: float & >=0 & <=1 + + // Should stereochemistry be taken into account in substructure matching. If false, no stereochemistry information is used in substructure matches. + "rdkit.do_chiral_sss"?: bool & false | true + + // Should enhanced stereochemistry be taken into account in substructure matching. + "rdkit.do_enhanced_stereo_sss"?: bool & false | true + + // Lower threshold of Tanimoto similarity. Molecules with similarity lower than threshold are not similar by % operation. + "rdkit.tanimoto_threshold"?: float & >=0 & <=1 + + // When set to fsync, PostgreSQL will recursively open and synchronize all files in the data directory before crash recovery begins + recovery_init_sync_method?: string & "fsync" | "syncfs" + + // When set to on, which is the default, PostgreSQL will automatically remove temporary files after a backend crash + remove_temp_files_after_crash: float & >=0 & <=1 | *0 + + // Reinitialize server after backend crash. + restart_after_crash?: bool & false | true + + // Enable row security. + row_security?: bool & false | true + + // Sets the schema search order for names that are not schema-qualified. + search_path?: string + + // Sets the planners estimate of the cost of a sequentially fetched disk page. + seq_page_cost?: float & >=0 & <=1.79769e+308 + + // Lists shared libraries to preload into each backend. + session_preload_libraries?: string & "auto_explain" | "orafce" | "pg_bigm" | "pg_hint_plan" | "pg_prewarm" | "pg_similarity" | "pg_stat_statements" | "pg_transport" | "plprofiler" + + // Sets the sessions behavior for triggers and rewrite rules. + session_replication_role?: string & "origin" | "replica" | "local" + + // (8kB) Sets the number of shared memory buffers used by the server. + shared_buffers?: int & >=16 & <=1073741823 @storeResource(8KB) + + // Lists shared libraries to preload into server. + // TODO: support enum list, e.g. shared_preload_libraries = 'pg_stat_statements, auto_explain' + // shared_preload_libraries?: string & "auto_explain" | "orafce" | "pgaudit" | "pglogical" | "pg_bigm" | "pg_cron" | "pg_hint_plan" | "pg_prewarm" | "pg_similarity" | "pg_stat_statements" | "pg_tle" | "pg_transport" | "plprofiler" + + // Enables SSL connections. + ssl: bool & false | true | *true + + // Location of the SSL server authority file. + ssl_ca_file?: string + + // Location of the SSL server certificate file. + ssl_cert_file?: string + + // Sets the list of allowed SSL ciphers. + ssl_ciphers?: string + + // Location of the SSL server private key file + ssl_key_file?: string + + // Sets the maximum SSL/TLS protocol version to use. + ssl_max_protocol_version?: string & "TLSv1" | "TLSv1.1" | "TLSv1.2" + + // Sets the minimum SSL/TLS protocol version to use. + ssl_min_protocol_version?: string & "TLSv1" | "TLSv1.1" | "TLSv1.2" + + // Causes ... strings to treat backslashes literally. + standard_conforming_strings?: bool & false | true + + // (ms) Sets the maximum allowed duration of any statement. + statement_timeout?: int & >=0 & <=2147483647 @timeDurationResource() + + // Writes temporary statistics files to the specified directory. + stats_temp_directory?: string + + // Sets the number of connection slots reserved for superusers. + superuser_reserved_connections: int & >=0 & <=8388607 | *3 + + // Enable synchronized sequential scans. + synchronize_seqscans?: bool & false | true + + // Sets the current transactions synchronization level. + synchronous_commit?: string & "local" | "on" | "off" + + // Maximum number of TCP keepalive retransmits. + tcp_keepalives_count?: int & >=0 & <=2147483647 + + // (s) Time between issuing TCP keepalives. + tcp_keepalives_idle?: int & >=0 & <=2147483647 @timeDurationResource(1s) + + // (s) Time between TCP keepalive retransmits. + tcp_keepalives_interval?: int & >=0 & <=2147483647 @timeDurationResource(1s) + + // TCP user timeout. Specifies the amount of time that transmitted data may remain unacknowledged before the TCP connection is forcibly closed. If this value is specified without units, it is taken as milliseconds. A value of 0 (the default) selects the operating system's default. This parameter is supported only on systems that support TCP_USER_TIMEOUT; on other systems, it must be zero. In sessions connected via a Unix-domain socket, this parameter is ignored and always reads as zero. + tcp_user_timeout?: int & >=0 & <=2147483647 @timeDurationResource() + + // (8kB) Sets the maximum number of temporary buffers used by each session. + temp_buffers?: int & >=100 & <=1073741823 @storeResource(8KB) + + // (kB) Limits the total size of all temporary files used by each process. + temp_file_limit?: int & >=-1 & <=2147483647 @storeResource(1KB) + + // Sets the tablespace(s) to use for temporary tables and sort files. + temp_tablespaces?: string + + // Sets the time zone for displaying and interpreting time stamps. + timezone?: string + + // Collects information about executing commands. + track_activities?: bool & false | true + + // Sets the size reserved for pg_stat_activity.current_query, in bytes. + track_activity_query_size: int & >=100 & <=1048576 | *4096 @storeResource() + + // Collects transaction commit time. + track_commit_timestamp?: bool & false | true + + // Collects statistics on database activity. + track_counts?: bool & false | true + + // Collects function-level statistics on database activity. + track_functions?: string & "none" | "pl" | "all" + + // Collects timing statistics on database IO activity. + track_io_timing: bool & false | true | *true + + // Enables timing of WAL I/O calls. + track_wal_io_timing?: bool & false | true + + // Treats expr=NULL as expr IS NULL. + transform_null_equals?: bool & false | true + + // Sets the directory where the Unix-domain socket will be created. + unix_socket_directories?: string + + // Sets the owning group of the Unix-domain socket. + unix_socket_group?: string + + // Sets the access permissions of the Unix-domain socket. + unix_socket_permissions?: int & >=0 & <=511 + + // Updates the process title to show the active SQL command. + update_process_title: bool & false | true | *true + + // (ms) Vacuum cost delay in milliseconds. + vacuum_cost_delay?: int & >=0 & <=100 @timeDurationResource() + + // Vacuum cost amount available before napping. + vacuum_cost_limit?: int & >=1 & <=10000 + + // Vacuum cost for a page dirtied by vacuum. + vacuum_cost_page_dirty?: int & >=0 & <=10000 + + // Vacuum cost for a page found in the buffer cache. + vacuum_cost_page_hit?: int & >=0 & <=10000 + + // Vacuum cost for a page not found in the buffer cache. + vacuum_cost_page_miss: int & >=0 & <=10000 | *5 + + // Number of transactions by which VACUUM and HOT cleanup should be deferred, if any. + vacuum_defer_cleanup_age?: int & >=0 & <=1000000 + + // Specifies the maximum age (in transactions) that a table's pg_class.relfrozenxid field can attain before VACUUM takes extraordinary measures to avoid system-wide transaction ID wraparound failure + vacuum_failsafe_age: int & >=0 & <=1200000000 | *1200000000 + + // Minimum age at which VACUUM should freeze a table row. + vacuum_freeze_min_age?: int & >=0 & <=1000000000 + + // Age at which VACUUM should scan whole table to freeze tuples. + vacuum_freeze_table_age?: int & >=0 & <=2000000000 + + // Specifies the maximum age (in transactions) that a table's pg_class.relminmxid field can attain before VACUUM takes extraordinary measures to avoid system-wide multixact ID wraparound failure + vacuum_multixact_failsafe_age: int & >=0 & <=1200000000 | *1200000000 + + // Minimum age at which VACUUM should freeze a MultiXactId in a table row. + vacuum_multixact_freeze_min_age?: int & >=0 & <=1000000000 + + // Multixact age at which VACUUM should scan whole table to freeze tuples. + vacuum_multixact_freeze_table_age?: int & >=0 & <=2000000000 + + // (8kB) Sets the number of disk-page buffers in shared memory for WAL. + wal_buffers?: int & >=-1 & <=262143 @storeResource(8KB) + + // Compresses full-page writes written in WAL file. + wal_compression: bool & false | true | *true + + // Sets the WAL resource managers for which WAL consistency checks are done. + wal_consistency_checking?: string + + // Buffer size for reading ahead in the WAL during recovery. + wal_decode_buffer_size: int & >=65536 & <=1073741823 | *524288 @storeResource() + + // (MB) Sets the size of WAL files held for standby servers. + wal_keep_size: int & >=0 & <=2147483647 | *2048 @storeResource(1MB) + + // Sets whether a WAL receiver should create a temporary replication slot if no permanent slot is configured. + wal_receiver_create_temp_slot: bool & false | true | *false + + // (s) Sets the maximum interval between WAL receiver status reports to the primary. + wal_receiver_status_interval?: int & >=0 & <=2147483 @timeDurationResource(1s) + + // (ms) Sets the maximum wait time to receive data from the primary. + wal_receiver_timeout: int & >=0 & <=3600000 | *30000 @timeDurationResource() + + // Recycles WAL files by renaming them. If set to on (the default), this option causes WAL files to be recycled by renaming them, avoiding the need to create new ones. On COW file systems, it may be faster to create new ones, so the option is given to disable this behavior. + wal_recycle?: bool + + // Sets the time to wait before retrying to retrieve WAL after a failed attempt. Specifies how long the standby server should wait when WAL data is not available from any sources (streaming replication, local pg_wal or WAL archive) before trying again to retrieve WAL data. If this value is specified without units, it is taken as milliseconds. The default value is 5 seconds. This parameter can only be set in the postgresql.conf file or on the server command line. + wal_retrieve_retry_interval: int & >=1 & <=2147483647 | *5000 @timeDurationResource() + + // (ms) Sets the maximum time to wait for WAL replication. + wal_sender_timeout: int & >=0 & <=3600000 | *30000 @timeDurationResource() + + // (kB) Size of new file to fsync instead of writing WAL. + wal_skip_threshold?: int & >=0 & <=2147483647 @storeResource(1KB) + + // Selects the method used for forcing WAL updates to disk. + wal_sync_method?: string & "fsync" | "fdatasync" | "open_sync" | "open_datasync" + + // (ms) WAL writer sleep time between WAL flushes. + wal_writer_delay?: int & >=1 & <=10000 @timeDurationResource() + + // (8Kb) Amount of WAL written out by WAL writer triggering a flush. + wal_writer_flush_after?: int & >=0 & <=2147483647 @storeResource(8KB) + + // If set to on (the default), this option causes new WAL files to be filled with zeroes. On some file systems, this ensures that space is allocated before we need to write WAL records. However, Copy-On-Write (COW) file systems may not benefit from this technique, so the option is given to skip the unnecessary work. If set to off, only the final byte is written when the file is created so that it has the expected size. + wal_init_zero?: string & "on" | "off" + + // (kB) Sets the maximum memory to be used for query workspaces. + work_mem?: int & >=64 & <=2147483647 @storeResource(1KB) + + // Sets how binary values are to be encoded in XML. + xmlbinary?: string & "base64" | "hex" + + // Sets whether XML data in implicit parsing and serialization operations is to be considered as documents or content fragments. + xmloption?: string & "content" | "document" + + ... +} + +configuration: #PGParameter & { +} diff --git a/deploy/postgresql/config/pg12-config-effect-scope.yaml b/deploy/postgresql/config/pg12-config-effect-scope.yaml new file mode 100644 index 000000000..f01623b21 --- /dev/null +++ b/deploy/postgresql/config/pg12-config-effect-scope.yaml @@ -0,0 +1,60 @@ +# Patroni bootstrap parameters +staticParameters: + - shared_buffers + - logging_collector + - log_destination + - log_directory + - log_filename + - log_file_mode + - log_rotation_age + - log_truncate_on_rotation + - ssl + - ssl_ca_file + - ssl_crl_file + - ssl_cert_file + - ssl_key_file + - shared_preload_libraries + - bg_mon.listen_address + - bg_mon.history_buckets + - pg_stat_statements.track_utility + - extwlist.extensions + - extwlist.custom_path + +immutableParameters: + - archive_timeout + - backtrace_functions + - config_file + - cron.use_background_workers + - data_directory + - db_user_namespace + - exit_on_error + - fsync + - full_page_writes + - hba_file + - ident_file + - ignore_invalid_pages + - listen_addresses + - lo_compat_privileges + - log_directory + - log_file_mode + - logging_collector + - log_line_prefix + - log_timezone + - log_truncate_on_rotation + - port + - rds.max_tcp_buffers + - recovery_init_sync_method + - restart_after_crash + - ssl + - ssl_ca_file + - ssl_cert_file + - ssl_ciphers + - ssl_key_file + - stats_temp_directory + - superuser_reserved_connections + - unix_socket_directories + - unix_socket_group + - unix_socket_permissions + - update_process_title + - wal_receiver_create_temp_slot + - wal_sync_method diff --git a/deploy/postgresql/config/pg12-config.tpl b/deploy/postgresql/config/pg12-config.tpl new file mode 100644 index 000000000..b957c7455 --- /dev/null +++ b/deploy/postgresql/config/pg12-config.tpl @@ -0,0 +1,321 @@ +# - Connection Settings - + +{{- $buffer_unit := "B" }} +{{- $shared_buffers := 1073741824 }} +{{- $max_connections := 10000 }} +{{- $autovacuum_max_workers := 3 }} +{{- $phy_memory := getContainerMemory ( index $.podSpec.containers 0 ) }} +{{- $phy_cpu := getContainerCPU ( index $.podSpec.containers 0 ) }} +{{- if gt $phy_memory 0 }} +{{- $shared_buffers = div $phy_memory 4 }} +{{- $max_connections = min ( div $phy_memory 9531392 ) 5000 }} +{{- $autovacuum_max_workers = min ( max ( div $phy_memory 17179869184 ) 3 ) 10 }} +{{- end }} + +{{- if ge $shared_buffers 1024 }} +{{- $shared_buffers = div $shared_buffers 1024 }} +{{- $buffer_unit = "kB" }} +{{- end }} + +{{- if ge $shared_buffers 1024 }} +{{- $shared_buffers = div $shared_buffers 1024 }} +{{- $buffer_unit = "MB" }} +{{- end }} + +{{- if ge $shared_buffers 1024 }} +{{- $shared_buffers = div $shared_buffers 1024 }} +{{ $buffer_unit = "GB" }} +{{- end }} + +listen_addresses = '*' +port = '5432' +archive_command = '/bin/true' +archive_mode = 'on' +auto_explain.log_analyze = 'False' +auto_explain.log_buffers = 'False' +auto_explain.log_format = 'text' +auto_explain.log_min_duration = '-1' +auto_explain.log_nested_statements = 'False' +auto_explain.log_timing = 'True' +auto_explain.log_triggers = 'False' +auto_explain.log_verbose = 'False' +auto_explain.sample_rate = '1' +autovacuum_analyze_scale_factor = '0.1' +autovacuum_analyze_threshold = '50' +autovacuum_freeze_max_age = '200000000' +autovacuum_max_workers = '{{ $autovacuum_max_workers }}' +autovacuum_multixact_freeze_max_age = '400000000' +autovacuum_naptime = '15s' +autovacuum_vacuum_cost_delay = '2' +autovacuum_vacuum_cost_limit = '200' +autovacuum_vacuum_scale_factor = '0.05' +autovacuum_vacuum_threshold = '50' +{{- if gt $phy_memory 0 }} +autovacuum_work_mem = '{{ printf "%dkB" ( max ( div $phy_memory 65536 ) 131072 ) }}' +{{- end }} +backend_flush_after = '0' +backslash_quote = 'safe_encoding' +bgwriter_delay = '200ms' +bgwriter_flush_after = '64' +bgwriter_lru_maxpages = '1000' +bgwriter_lru_multiplier = '10.0' +bytea_output = 'hex' +check_function_bodies = 'True' +checkpoint_completion_target = '0.9' +checkpoint_flush_after = '32' +checkpoint_timeout = '15min' +checkpoint_warning = '30s' +client_min_messages = 'notice' +# commit_delay = '20' +commit_siblings = '5' +constraint_exclusion = 'partition' + +#extension: pg_cron +cron.database_name = 'postgres' +cron.log_statement = 'on' +cron.max_running_jobs = '32' + +cursor_tuple_fraction = '0.1' +datestyle = 'ISO,YMD' +deadlock_timeout = '1000ms' +debug_pretty_print = 'True' +debug_print_parse = 'False' +debug_print_plan = 'False' +debug_print_rewritten = 'False' +default_statistics_target = '100' +default_transaction_deferrable = 'False' +default_transaction_isolation = 'read committed' +# unit 8KB +{{- if gt $phy_memory 0 }} +effective_cache_size = '{{ printf "%dMB" ( div ( div $phy_memory 16384 ) 128 ) }}' +{{- end }} +effective_io_concurrency = '1' +enable_bitmapscan = 'True' +enable_gathermerge = 'True' +enable_hashagg = 'True' +enable_hashjoin = 'True' +enable_indexonlyscan = 'True' +enable_indexscan = 'True' +enable_material = 'True' +enable_mergejoin = 'True' +enable_nestloop = 'True' +enable_parallel_append = 'True' +enable_parallel_hash = 'True' +enable_partition_pruning = 'True' +# patroni off +enable_partitionwise_aggregate = 'True' +# patroni off +enable_partitionwise_join = 'True' +enable_seqscan = 'True' +enable_sort = 'True' +enable_tidscan = 'True' +escape_string_warning = 'True' +extra_float_digits = '1' +force_parallel_mode = '0' +from_collapse_limit = '8' +#fsync=off # patroni for Extreme Performance +#full_page_writes=off # patroni for Extreme Performance +geqo = 'True' +geqo_effort = '5' +geqo_generations = '0' +geqo_pool_size = '0' +geqo_seed = '0' +geqo_selection_bias = '2' +geqo_threshold = '12' +gin_fuzzy_search_limit = '0' +gin_pending_list_limit = '4096kB' +# patroni on +hot_standby_feedback = 'False' +# rds huge_pages=on, patroni try +huge_pages = 'try' +#patroni 10min +idle_in_transaction_session_timeout = '3600000ms' +index_adviser.enable_log = 'on' +index_adviser.max_aggregation_column_count = '10' +index_adviser.max_candidate_index_count = '500' +intervalstyle = 'postgres' +join_collapse_limit = '8' +lc_monetary = 'C' +lc_numeric = 'C' +lc_time = 'C' +lock_timeout = '0' +# patroni 1s +log_autovacuum_min_duration = '10000' +log_checkpoints = 'True' +log_connections = 'False' +log_disconnections = 'False' +log_duration = 'False' +log_executor_stats = 'False' + +{{- block "logsBlock" . }} +{{- if hasKey $.component "enabledLogs" }} +{{- if mustHas "running" $.component.enabledLogs }} +logging_collector = 'True' +log_destination = 'csvlog' +log_directory = 'log' +log_filename = 'postgresql-%Y-%m-%d.log' +{{ end -}} +{{ end -}} +{{ end }} + +# log_lock_waits = 'True' +log_min_duration_statement = '1000' +log_parser_stats = 'False' +log_planner_stats = 'False' +log_replication_commands = 'False' +log_statement = 'ddl' +log_statement_stats = 'False' +log_temp_files = '128kB' +log_transaction_sample_rate = '0' +#maintenance_work_mem = '3952MB' +max_connections = '{{ $max_connections }}' +max_files_per_process = '1000' +max_logical_replication_workers = '32' +max_locks_per_transaction = '64' +max_parallel_maintenance_workers = '{{ max ( div $phy_cpu 2 ) 2 }}' +max_parallel_workers = '{{ max ( div ( mul $phy_cpu 3 ) 4 ) 8 }}' +max_parallel_workers_per_gather = '{{ max ( div $phy_cpu 2 ) 2 }}' +max_pred_locks_per_page = '2' +max_pred_locks_per_relation = '-2' +max_pred_locks_per_transaction = '64' +max_prepared_transactions = '100' +max_replication_slots = '16' +max_stack_depth = '2MB' + +max_standby_archive_delay = '300000ms' +max_standby_streaming_delay = '300000ms' +max_sync_workers_per_subscription = '2' +max_wal_senders = '64' +# {LEAST(GREATEST(DBInstanceClassMemory/2097152, 2048), 16384)} +max_wal_size = '{{ printf "%dMB" ( min ( max ( div $phy_memory 2097152 ) 4096 ) 32768 ) }}' +max_worker_processes = '{{ max $phy_cpu 8 }}' +# min_parallel_index_scan_size unit is 8KB, 64 = 512KB +min_parallel_index_scan_size = '512kB' +# min_parallel_table_scan_size unit is 8KB, 1024 = 8MB +min_parallel_table_scan_size = '8MB' +{{- if gt $phy_memory 0 }} +min_wal_size = '{{ printf "%dMB" ( min ( max ( div $phy_memory 8388608 ) 2048 ) 8192 ) }}' +{{- end }} + +old_snapshot_threshold = '-1' +operator_precedence_warning = 'off' +parallel_leader_participation = 'True' + +password_encryption = 'md5' +pg_stat_statements.max = '5000' +pg_stat_statements.save = 'False' + +# patroni all +pg_stat_statements.track = 'top' +# pg_stat_statements.track_planning = 'False' +pg_stat_statements.track_utility = 'False' + +#extension: pgaudit +pgaudit.log_catalog = 'True' +pgaudit.log_level = 'log' +pgaudit.log_parameter = 'False' +pgaudit.log_relation = 'False' +pgaudit.log_statement_once = 'False' +# TODO +# pgaudit.role = '' + +#extension: pglogical +pglogical.batch_inserts = 'True' +pglogical.conflict_log_level = 'log' +pglogical.conflict_resolution = 'apply_remote' +# TODO +# pglogical.extra_connection_options = '' +pglogical.synchronous_commit = 'False' +pglogical.use_spi = 'False' +plan_cache_mode = 'auto' +quote_all_identifiers = 'False' + +random_page_cost = '1.1' +row_security = 'True' +session_replication_role = 'origin' + +#extension: sql_firewall +sql_firewall.firewall = 'disable' + +#auto generated +shared_buffers = '{{ printf "%d%s" $shared_buffers $buffer_unit }}' +# shared_preload_libraries = 'pg_stat_statements,auto_explain,bg_mon,pgextwlist,pg_auth_mon,set_user,pg_cron,pg_stat_kcache' + +{{- if $.component.tls }} +{{- $ca_file := getCAFile }} +{{- $cert_file := getCertFile }} +{{- $key_file := getKeyFile }} +# tls +ssl = 'True' +ssl_ca_file = '{{ $ca_file }}' +ssl_cert_file = '{{ $cert_file }}' +ssl_key_file = '{{ $key_file }}' +{{- end }} + +# ssl_max_protocol_version='' +ssl_min_protocol_version = 'TLSv1' +standard_conforming_strings = 'True' +statement_timeout = '0' +#patroni 10 +superuser_reserved_connections = '20' +synchronize_seqscans = 'True' + +# rds off ,patroni off for Extreme Performance +synchronous_commit = 'off' +# synchronous_standby_names='' +tcp_keepalives_count = '10' +tcp_keepalives_idle = '45s' +tcp_keepalives_interval = '10s' +temp_buffers = '8MB' + +# {DBInstanceClassMemory/1024} +{{- if gt $phy_memory 0 }} +temp_file_limit = '{{ printf "%dkB" ( div $phy_memory 1024 ) }}' +{{- end }} + +#extension: timescaledb +#timescaledb.max_background_workers = '6' +#timescaledb.telemetry_level = 'off' +# TODO timezone +# timezone=Asia/Shanghai +track_activity_query_size = '4096' +track_commit_timestamp = 'False' +track_functions = 'pl' +track_io_timing = 'True' +transform_null_equals = 'False' + +vacuum_cleanup_index_scale_factor = '0.1' +# patroni 20ms +vacuum_cost_delay = '0' +# patroni 2000 +vacuum_cost_limit = '10000' +vacuum_cost_page_dirty = '20' +vacuum_cost_page_hit = '1' +vacuum_cost_page_miss = '2' +# patroni 50000 +vacuum_defer_cleanup_age = '0' +vacuum_freeze_min_age = '50000000' +vacuum_freeze_table_age = '200000000' +vacuum_multixact_freeze_min_age = '5000000' +vacuum_multixact_freeze_table_age = '200000000' +# wal_buffers ={LEAST(GREATEST(DBInstanceClassMemory/2097152, 2048), 16384)} # patroni 16M +# unit 8KB +wal_buffers = '{{ printf "%dMB" ( div ( min ( max ( div $phy_memory 2097152 ) 2048) 16384 ) 128 ) }}' +wal_compression = 'True' +wal_keep_segments = '4' +# patroni minimal for Extreme Performance +wal_level = 'replica' +# patroni on , off for Extreme Performance +wal_log_hints = 'False' +wal_receiver_status_interval = '1s' +wal_receiver_timeout = '60000' +wal_sender_timeout = '60000' +# patroni 20ms +wal_writer_delay = '200ms' +# rds unit 8KB, so 1M, patroni 1M +wal_writer_flush_after = '1MB' +# {GREATEST(DBInstanceClassMemory/4194304, 4096)} +work_mem = '{{ printf "%dkB" ( max ( div $phy_memory 4194304 ) 4096 ) }}' +xmlbinary = 'base64' +xmloption = 'content' +wal_init_zero = off \ No newline at end of file diff --git a/deploy/postgresql/config/pg14-config-constraint.cue b/deploy/postgresql/config/pg14-config-constraint.cue index 31f7f924e..7b0a4d8c4 100644 --- a/deploy/postgresql/config/pg14-config-constraint.cue +++ b/deploy/postgresql/config/pg14-config-constraint.cue @@ -1,31 +1,42 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // PostgreSQL parameters: https://postgresqlco.nf/doc/en/param/ #PGParameter: { + // Allows tablespaces directly inside pg_tblspc, for testing, pg version: 15 + allow_in_place_tablespaces?: bool + // Allows modification of the structure of system tables as well as certain other risky actions on system tables. This is otherwise not allowed even for superusers. Ill-advised use of this setting can cause irretrievable data loss or seriously corrupt the database system. + allow_system_table_mods?: bool // Sets the application name to be reported in statistics and logs. application_name?: string // Sets the shell command that will be called to archive a WAL file. archive_command?: string + // The library to use for archiving completed WAL file segments. If set to an empty string (the default), archiving via shell is enabled, and archive_command is used. Otherwise, the specified shared library is used for archiving. The WAL archiver process is restarted by the postmaster when this parameter changes. For more information, see backup-archiving-wal and archive-modules. + archive_library?: string + // When archive_mode is enabled, completed WAL segments are sent to archive storage by setting archive_command or guc-archive-library. In addition to off, to disable, there are two modes: on, and always. During normal operation, there is no difference between the two modes, but when set to always the WAL archiver is enabled also during archive recovery or standby mode. In always mode, all files restored from the archive or streamed with streaming replication will be archived (again). See continuous-archiving-in-standby for details. + archive_mode: string & "always" | "on" | "off" // (s) Forces a switch to the next xlog file if a new file has not been started within N seconds. archive_timeout: int & >=0 & <=2147483647 | *300 @timeDurationResource(1s) // Enable input of NULL elements in arrays. - array_nulls?: bool & false | true + array_nulls?: bool // (s) Sets the maximum allowed time to complete client authentication. - authentication_timeout?: int & >=1 & <=600 @timeDurationResource() + authentication_timeout?: int & >=1 & <=600 @timeDurationResource(1s) // Use EXPLAIN ANALYZE for plan logging. - "auto_explain.log_analyze"?: bool & false | true + "auto_explain.log_analyze"?: bool // Log buffers usage. "auto_explain.log_buffers"?: bool & false | true // EXPLAIN format to be used for plan logging. @@ -50,7 +61,7 @@ "auto_explain.sample_rate"?: float & >=0 & <=1 // Starts the autovacuum subprocess. - autovacuum?: bool & false | true + autovacuum?: bool // Number of tuple inserts, updates or deletes prior to analyze as a fraction of reltuples. autovacuum_analyze_scale_factor: float & >=0 & <=100 | *0.05 @@ -59,7 +70,7 @@ autovacuum_analyze_threshold?: int & >=0 & <=2147483647 // Age at which to autovacuum a table to prevent transaction ID wraparound. - autovacuum_freeze_max_age?: int & >=100000000 & <=750000000 + autovacuum_freeze_max_age?: int & >=100000 & <=2000000000 // Sets the maximum number of simultaneously running autovacuum worker processes. autovacuum_max_workers?: int & >=1 & <=8388607 @@ -71,7 +82,7 @@ autovacuum_naptime: int & >=1 & <=2147483 | *15 @timeDurationResource(1s) // (ms) Vacuum cost delay in milliseconds, for autovacuum. - autovacuum_vacuum_cost_delay?: int & >=-1 & <=100 + autovacuum_vacuum_cost_delay?: int & >=-1 & <=100 @timeDurationResource() // Vacuum cost amount available before napping, for autovacuum. autovacuum_vacuum_cost_limit?: int & >=-1 & <=10000 @@ -92,9 +103,9 @@ autovacuum_work_mem?: int & >=-1 & <=2147483647 @storeResource(1KB) // (8Kb) Number of pages after which previously performed writes are flushed to disk. - backend_flush_after?: int & >=0 & <=256 + backend_flush_after?: int & >=0 & <=256 @storeResource(8KB) - // Sets whether \ is allowed in string literals. + // Sets whether "\" is allowed in string literals. backslash_quote?: string & "safe_encoding" | "on" | "off" // Log backtrace for errors in these functions. @@ -104,7 +115,7 @@ bgwriter_delay?: int & >=10 & <=10000 @timeDurationResource() // (8Kb) Number of pages after which previously performed writes are flushed to disk. - bgwriter_flush_after?: int & >=0 & <=256 + bgwriter_flush_after?: int & >=0 & <=256 @storeResource(8KB) // Background writer maximum number of LRU pages to flush per round. bgwriter_lru_maxpages?: int & >=0 & <=1000 @@ -352,6 +363,9 @@ // Use of huge pages on Linux. huge_pages?: string & "on" | "off" | "try" + // The size of huge page that should be requested. Controls the size of huge pages, when they are enabled with huge_pages. The default is zero (0). When set to 0, the default huge page size on the system will be used. This parameter can only be set at server start. + huge_page_size?: int & >=0 & <=2147483647 @storeResource(1KB) + // Sets the servers ident configuration file. ident_file?: string @@ -368,7 +382,7 @@ intervalstyle?: string & "postgres" | "postgres_verbose" | "sql_standard" | "iso_8601" // Allow JIT compilation. - jit: bool & false | true | *false + jit: bool // Perform JIT compilation if query is more expensive. jit_above_cost?: float & >=-1 & <=1.79769e+308 @@ -484,6 +498,9 @@ // (kB) Automatic log file rotation will occur after N kilobytes. log_rotation_size?: int & >=0 & <=2097151 @storeResource(1KB) + // Time between progress updates for long-running startup operations. Sets the amount of time after which the startup process will log a message about a long-running operation that is still in progress, as well as the interval between further progress messages for that operation. The default is 10 seconds. A setting of 0 disables the feature. If this value is specified without units, it is taken as milliseconds. This setting is applied separately to each operation. This parameter can only be set in the postgresql.conf file or on the server command line. + log_startup_progress_interval: int & >=0 & <=2147483647 @timeDurationResource() + // Sets the type of statements logged. log_statement?: string & "none" | "ddl" | "mod" | "all" @@ -491,7 +508,7 @@ log_statement_sample_rate?: float & >=0 & <=1 // Writes cumulative performance statistics to the server log. - log_statement_stats?: bool & false | true + log_statement_stats?: bool // (kB) Log the use of temporary files larger than this number of kilobytes. log_temp_files?: int & >=-1 & <=2147483647 @storeResource(1KB) @@ -577,6 +594,9 @@ // (8kB) Sets the minimum amount of index data for a parallel scan. min_parallel_index_scan_size?: int & >=0 & <=715827882 @storeResource(8KB) + // Sets the minimum size of relations to be considered for parallel scan. Sets the minimum size of relations to be considered for parallel scan. + min_parallel_relation_size?: int & >=0 & <=715827882 @storeResource(8KB) + // (8kB) Sets the minimum amount of table data for a parallel scan. min_parallel_table_scan_size?: int & >=0 & <=715827882 @storeResource(8KB) @@ -584,7 +604,7 @@ min_wal_size: int & >=128 & <=201326592 | *192 @storeResource(1MB) // (min) Time before a snapshot is too old to read pages changed after the snapshot was taken. - old_snapshot_threshold?: int & >=-1 & <=86400 + old_snapshot_threshold?: int & >=-1 & <=86400 @timeDurationResource(1min) // Emulate oracle's date output behaviour. "orafce.nls_date_format"?: string @@ -838,6 +858,12 @@ // Sets the TCP port the server listens on. port?: int & >=1 & <=65535 + // Sets the amount of time to wait after authentication on connection startup. The amount of time to delay when a new server process is started, after it conducts the authentication procedure. This is intended to give developers an opportunity to attach to the server process with a debugger. If this value is specified without units, it is taken as seconds. A value of zero (the default) disables the delay. This parameter cannot be changed after session start. + post_auth_delay?: int & >=0 & <=2147 @timeDurationResource(1s) + + // Sets the amount of time to wait before authentication on connection startup. The amount of time to delay just after a new server process is forked, before it conducts the authentication procedure. This is intended to give developers an opportunity to attach to the server process with a debugger to trace down misbehavior in authentication. If this value is specified without units, it is taken as seconds. A value of zero (the default) disables the delay. This parameter can only be set in the postgresql.conf file or on the server command line. + pre_auth_delay?: int & >=0 & <=60 @timeDurationResource(1s) + // Enable for disable GDAL drivers used with PostGIS in Postgres 9.3.5 and above. "postgis.gdal_enabled_drivers"?: string & "ENABLE_ALL" | "DISABLE_ALL" @@ -887,7 +913,7 @@ shared_buffers?: int & >=16 & <=1073741823 @storeResource(8KB) // Lists shared libraries to preload into server. - // TODO support enum list, e.g. shared_preload_libraries = 'pg_stat_statements, auto_explain' + // TODO: support enum list, e.g. shared_preload_libraries = 'pg_stat_statements, auto_explain' // shared_preload_libraries?: string & "auto_explain" | "orafce" | "pgaudit" | "pglogical" | "pg_bigm" | "pg_cron" | "pg_hint_plan" | "pg_prewarm" | "pg_similarity" | "pg_stat_statements" | "pg_tle" | "pg_transport" | "plprofiler" // Enables SSL connections. @@ -938,11 +964,14 @@ // (s) Time between TCP keepalive retransmits. tcp_keepalives_interval?: int & >=0 & <=2147483647 @timeDurationResource(1s) + // TCP user timeout. Specifies the amount of time that transmitted data may remain unacknowledged before the TCP connection is forcibly closed. If this value is specified without units, it is taken as milliseconds. A value of 0 (the default) selects the operating system's default. This parameter is supported only on systems that support TCP_USER_TIMEOUT; on other systems, it must be zero. In sessions connected via a Unix-domain socket, this parameter is ignored and always reads as zero. + tcp_user_timeout?: int & >=0 & <=2147483647 @timeDurationResource() + // (8kB) Sets the maximum number of temporary buffers used by each session. temp_buffers?: int & >=100 & <=1073741823 @storeResource(8KB) // (kB) Limits the total size of all temporary files used by each process. - temp_file_limit?: int & >=-1 & <=2147483647 @storeResource(8KB) + temp_file_limit?: int & >=-1 & <=2147483647 @storeResource(1KB) // Sets the tablespace(s) to use for temporary tables and sort files. temp_tablespaces?: string @@ -954,7 +983,7 @@ track_activities?: bool & false | true // Sets the size reserved for pg_stat_activity.current_query, in bytes. - track_activity_query_size: int & >=100 & <=1048576 | *4096 + track_activity_query_size: int & >=100 & <=1048576 | *4096 @storeResource() // Collects transaction commit time. track_commit_timestamp?: bool & false | true @@ -1028,6 +1057,12 @@ // Compresses full-page writes written in WAL file. wal_compression: bool & false | true | *true + // Sets the WAL resource managers for which WAL consistency checks are done. + wal_consistency_checking?: string + + // Buffer size for reading ahead in the WAL during recovery. + wal_decode_buffer_size: int & >=65536 & <=1073741823 | *524288 @storeResource() + // (MB) Sets the size of WAL files held for standby servers. wal_keep_size: int & >=0 & <=2147483647 | *2048 @storeResource(1MB) @@ -1040,6 +1075,12 @@ // (ms) Sets the maximum wait time to receive data from the primary. wal_receiver_timeout: int & >=0 & <=3600000 | *30000 @timeDurationResource() + // Recycles WAL files by renaming them. If set to on (the default), this option causes WAL files to be recycled by renaming them, avoiding the need to create new ones. On COW file systems, it may be faster to create new ones, so the option is given to disable this behavior. + wal_recycle?: bool + + // Sets the time to wait before retrying to retrieve WAL after a failed attempt. Specifies how long the standby server should wait when WAL data is not available from any sources (streaming replication, local pg_wal or WAL archive) before trying again to retrieve WAL data. If this value is specified without units, it is taken as milliseconds. The default value is 5 seconds. This parameter can only be set in the postgresql.conf file or on the server command line. + wal_retrieve_retry_interval: int & >=1 & <=2147483647 | *5000 @timeDurationResource() + // (ms) Sets the maximum time to wait for WAL replication. wal_sender_timeout: int & >=0 & <=3600000 | *30000 @timeDurationResource() @@ -1058,6 +1099,9 @@ // (kB) Sets the maximum memory to be used for query workspaces. work_mem?: int & >=64 & <=2147483647 @storeResource(1KB) + // If set to on (the default), this option causes new WAL files to be filled with zeroes. On some file systems, this ensures that space is allocated before we need to write WAL records. However, Copy-On-Write (COW) file systems may not benefit from this technique, so the option is given to skip the unnecessary work. If set to off, only the final byte is written when the file is created so that it has the expected size. + wal_init_zero?: string & "on" | "off" + // Sets how binary values are to be encoded in XML. xmlbinary?: string & "base64" | "hex" diff --git a/deploy/postgresql/config/pg14-config-effect-scope.yaml b/deploy/postgresql/config/pg14-config-effect-scope.yaml index 6e4d69046..f01623b21 100644 --- a/deploy/postgresql/config/pg14-config-effect-scope.yaml +++ b/deploy/postgresql/config/pg14-config-effect-scope.yaml @@ -1,6 +1,5 @@ # Patroni bootstrap parameters staticParameters: - - archive_command - shared_buffers - logging_collector - log_destination @@ -19,4 +18,43 @@ staticParameters: - bg_mon.history_buckets - pg_stat_statements.track_utility - extwlist.extensions - - extwlist.custom_path \ No newline at end of file + - extwlist.custom_path + +immutableParameters: + - archive_timeout + - backtrace_functions + - config_file + - cron.use_background_workers + - data_directory + - db_user_namespace + - exit_on_error + - fsync + - full_page_writes + - hba_file + - ident_file + - ignore_invalid_pages + - listen_addresses + - lo_compat_privileges + - log_directory + - log_file_mode + - logging_collector + - log_line_prefix + - log_timezone + - log_truncate_on_rotation + - port + - rds.max_tcp_buffers + - recovery_init_sync_method + - restart_after_crash + - ssl + - ssl_ca_file + - ssl_cert_file + - ssl_ciphers + - ssl_key_file + - stats_temp_directory + - superuser_reserved_connections + - unix_socket_directories + - unix_socket_group + - unix_socket_permissions + - update_process_title + - wal_receiver_create_temp_slot + - wal_sync_method diff --git a/deploy/postgresql/config/pg14-config.tpl b/deploy/postgresql/config/pg14-config.tpl index c38b79027..baf45a024 100644 --- a/deploy/postgresql/config/pg14-config.tpl +++ b/deploy/postgresql/config/pg14-config.tpl @@ -7,27 +7,27 @@ {{- if gt $phy_memory 0 }} {{- $shared_buffers = div $phy_memory 4 }} {{- $max_connections = min ( div $phy_memory 9531392 ) 5000 }} -{{- end -}} +{{- end }} {{- if ge $shared_buffers 1024 }} {{- $shared_buffers = div $shared_buffers 1024 }} -{{- $buffer_unit = "KB" }} -{{- end -}} +{{- $buffer_unit = "kB" }} +{{- end }} {{- if ge $shared_buffers 1024 }} {{- $shared_buffers = div $shared_buffers 1024 }} {{- $buffer_unit = "MB" }} -{{- end -}} +{{- end }} {{- if ge $shared_buffers 1024 }} {{- $shared_buffers = div $shared_buffers 1024 }} {{ $buffer_unit = "GB" }} -{{- end -}} +{{- end }} listen_addresses = '*' port = '5432' -#archive_command = 'wal_dir=/pg/arcwal; [[ $(date +%H%M) == 1200 ]] && rm -rf ${wal_dir}/$(date -d"yesterday" +%Y%m%d); /bin/mkdir -p ${wal_dir}/$(date +%Y%m%d) && /usr/bin/lz4 -q -z %p > ${wal_dir}/$(date +%Y%m%d)/%f.lz4' -#archive_mode = 'True' +archive_command = '/bin/true' +archive_mode = 'on' auto_explain.log_analyze = 'True' auto_explain.log_min_duration = '1s' auto_explain.log_nested_statements = 'True' @@ -57,14 +57,22 @@ idle_in_transaction_session_timeout = '1h' listen_addresses = '0.0.0.0' log_autovacuum_min_duration = '1s' log_checkpoints = 'True' + +{{- block "logsBlock" . }} +{{- if hasKey $.component "enabledLogs" }} +{{- if mustHas "running" $.component.enabledLogs }} +logging_collector = 'True' log_destination = 'csvlog' log_directory = 'log' log_filename = 'postgresql-%Y-%m-%d.log' +{{ end -}} +{{ end -}} +{{ end }} + log_lock_waits = 'True' log_min_duration_statement = '100' log_replication_commands = 'True' log_statement = 'ddl' -logging_collector = 'True' #maintenance_work_mem = '3952MB' max_connections = '{{ $max_connections }}' max_locks_per_transaction = '128' @@ -78,18 +86,22 @@ max_standby_archive_delay = '10min' max_standby_streaming_delay = '3min' max_sync_workers_per_subscription = '6' max_wal_senders = '24' -max_wal_size = '100GB' max_worker_processes = '8' -min_wal_size = '20GB' +# max_wal_size = '100GB' +max_wal_size = '{{ printf "%dMB" ( min ( max ( div $phy_memory 2097152 ) 4096 ) 32768 ) }}' +{{- if gt $phy_memory 0 }} +# min_wal_size = '20GB' +min_wal_size = '{{ printf "%dMB" ( min ( max ( div $phy_memory 8388608 ) 2048 ) 8192 ) }}' +{{- end }} password_encryption = 'md5' pg_stat_statements.max = '5000' -pg_stat_statements.track = 'all' +pg_stat_statements.track = 'top' pg_stat_statements.track_planning = 'False' pg_stat_statements.track_utility = 'False' random_page_cost = '1.1' #auto generated shared_buffers = '{{ printf "%d%s" $shared_buffers $buffer_unit }}' -shared_preload_libraries = 'pg_stat_statements, auto_explain' +# shared_preload_libraries = 'pg_stat_statements,auto_explain,bg_mon,pgextwlist,pg_auth_mon,set_user,pg_cron,pg_stat_kcache' superuser_reserved_connections = '10' temp_file_limit = '100GB' #timescaledb.max_background_workers = '6' @@ -102,7 +114,7 @@ vacuum_cost_delay = '2ms' vacuum_cost_limit = '10000' vacuum_defer_cleanup_age = '50000' wal_buffers = '16MB' -wal_keep_size = '20GB' +wal_keep_size = '4GB' wal_level = 'replica' wal_log_hints = 'on' wal_receiver_status_interval = '1s' @@ -110,13 +122,14 @@ wal_receiver_timeout = '60s' wal_writer_delay = '20ms' wal_writer_flush_after = '1MB' work_mem = '32MB' +wal_init_zero = off {{- if $.component.tls }} {{- $ca_file := getCAFile }} {{- $cert_file := getCertFile }} {{- $key_file := getKeyFile }} # tls -ssl=ON +ssl= 'True' ssl_ca_file={{ $ca_file }} ssl_cert_file={{ $cert_file }} ssl_key_file={{ $key_file }} diff --git a/deploy/postgresql/config/pgbouncer-ini.tpl b/deploy/postgresql/config/pgbouncer-ini.tpl new file mode 100644 index 000000000..222f4d7a4 --- /dev/null +++ b/deploy/postgresql/config/pgbouncer-ini.tpl @@ -0,0 +1,21 @@ +[pgbouncer] +listen_addr = 0.0.0.0 +listen_port = 6432 +unix_socket_dir = /tmp/ +unix_socket_mode = 0777 +auth_file = /opt/bitnami/pgbouncer/conf/userlist.txt +pidfile =/opt/bitnami/pgbouncer/tmp/pgbouncer.pid +logfile =/opt/bitnami/pgbouncer/logs/pgbouncer.log +auth_type = md5 +pool_mode = session +ignore_startup_parameters = extra_float_digits +{{- $max_client_conn := 10000 }} +{{- $phy_memory := getContainerMemory ( index $.podSpec.containers 0 ) }} +{{- if gt $phy_memory 0 }} +{{- $max_client_conn = min ( div $phy_memory 9531392 ) 5000 }} +{{- end }} +max_client_conn = {{ $max_client_conn }} +admin_users = postgres + +;;; [database] +;;; config default database in pgbouncer_setup.sh \ No newline at end of file diff --git a/deploy/postgresql/scripts/patroni-reload.tpl b/deploy/postgresql/scripts/patroni-reload.tpl index 6669eea7b..0e0380f69 100644 --- a/deploy/postgresql/scripts/patroni-reload.tpl +++ b/deploy/postgresql/scripts/patroni-reload.tpl @@ -1,12 +1,14 @@ {{- $bootstrap := $.Files.Get "bootstrap.yaml" | fromYamlArray }} {{- $command := "reload" }} -{{- range $pk, $_ := $.arg0 }} +{{- $trimParams := dict }} +{{- range $pk, $val := $.arg0 }} + {{- /* trim single quotes for value in the pg config file */}} + {{- set $trimParams $pk ( $val | trimAll "'" ) }} {{- if has $pk $bootstrap }} {{- $command = "restart" }} - {{ break }} {{- end }} {{- end }} -{{ $params := dict "parameters" $.arg0 }} +{{ $params := dict "parameters" $trimParams }} {{- $err := execSql ( dict "postgresql" $params | toJson ) "config" }} {{- if $err }} {{- failed $err }} diff --git a/deploy/postgresql/scripts/restart-parameter.yaml b/deploy/postgresql/scripts/restart-parameter.yaml index eabd4c5ae..f73fb4e94 100644 --- a/deploy/postgresql/scripts/restart-parameter.yaml +++ b/deploy/postgresql/scripts/restart-parameter.yaml @@ -3,18 +3,31 @@ - autovacuum_freeze_max_age - autovacuum_max_workers - autovacuum_multixact_freeze_max_age +- bg_mon.history_buckets +- bonjour +- bonjour_name +- cluster_name - config_file - cron.database_name +- cron.enable_superuser_jobs +- cron.host - cron.log_run - cron.log_statement - cron.max_running_jobs +- cron.timezone - cron.use_background_workers - data_directory +- data_sync_retry +- dynamic_shared_memory_type +- event_source +- external_pid_file - hba_file +- hot_standby - huge_pages - huge_page_size - ident_file - ignore_invalid_pages +- jit_provider - listen_addresses - logging_collector - max_connections @@ -36,10 +49,21 @@ - port - postgis.gdal_enabled_drivers - recovery_init_sync_method +- recovery_target +- recovery_target_action +- recovery_target_inclusive +- recovery_target_lsn +- recovery_target_name +- recovery_target_time +- recovery_target_timeline +- recovery_target_xid - session_preload_libraries - shared_buffers +- shared_memory_type - shared_preload_libraries - superuser_reserved_connections +- timescaledb.bgw_launcher_poll_time +- timescaledb.max_background_workers - track_activity_query_size - track_commit_timestamp - unix_socket_directories @@ -47,4 +71,8 @@ - unix_socket_permissions - wal_buffers - wal_compression -- wal_decode_buffer_size \ No newline at end of file +- wal_decode_buffer_size +- wal_level +- wal_log_hints +- wal_keep_segments +- wal_keep_size \ No newline at end of file diff --git a/deploy/postgresql/templates/backuppolicytemplate.yaml b/deploy/postgresql/templates/backuppolicytemplate.yaml index 48a8be568..a763aed7f 100644 --- a/deploy/postgresql/templates/backuppolicytemplate.yaml +++ b/deploy/postgresql/templates/backuppolicytemplate.yaml @@ -1,25 +1,71 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-postgresql + name: postgresql-backup-policy-template labels: clusterdefinition.kubeblocks.io/name: postgresql {{- include "postgresql.labels" . | nindent 4 }} + annotations: + dataprotection.kubeblocks.io/reconfigure-ref: | + { + "name": "postgresql-configuration", + "key": "postgresql.conf", + "enable": { + "logfile": [{"key": "archive_command","value": "''"}] + }, + "disable": { + "logfile": [{ "key": "archive_command","value": "'/bin/true'"}] + } + } spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s - - credentialKeyword: - userKeyword: username - passwordKeyword: password - - hooks: - containerName: postgresql - preCommands: - - psql -c "CHECKPOINT" - backupStatusUpdates: - - path: manifests.backupLog - containerName: postgresql - script: /scripts/backup-log-collector.sh - updateStage: pre + clusterDefinitionRef: postgresql + backupPolicies: + - componentDefRef: postgresql + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * *" + datafile: + enable: false + cronExpression: "0 18 * * *" + logfile: + enable: false + cronExpression: "*/5 * * * *" + snapshot: + target: + connectionCredentialKey: + passwordKey: password + usernameKey: username + hooks: + containerName: postgresql + preCommands: + - psql -c "CHECKPOINT;" + backupStatusUpdates: + - path: manifests.backupLog + containerName: postgresql + script: /kb-scripts/backup-log-collector.sh true + updateStage: post + datafile: + backupToolName: postgres-basebackup + backupStatusUpdates: + - path: manifests.backupLog + containerName: postgresql + script: /kb-scripts/backup-log-collector.sh true + updateStage: post + - containerName: postgresql + script: /kb-scripts/filesize-collector.sh basebackup.info + updateStage: post + logfile: + backupToolName: postgres-pitr + target: + role: primary + backupStatusUpdates: + - path: manifests.backupLog + containerName: postgresql + script: /kb-scripts/backup-log-collector.sh false + updateStage: post + - containerName: postgresql + script: /kb-scripts/filesize-collector.sh logfile.info + updateStage: post diff --git a/deploy/postgresql/templates/backuptool-pitr.yaml b/deploy/postgresql/templates/backuptool-pitr.yaml new file mode 100644 index 000000000..2b36b5dfa --- /dev/null +++ b/deploy/postgresql/templates/backuptool-pitr.yaml @@ -0,0 +1,94 @@ +apiVersion: dataprotection.kubeblocks.io/v1alpha1 +kind: BackupTool +metadata: + labels: + clusterdefinition.kubeblocks.io/name: postgresql + kubeblocks.io/backup-tool-type: pitr + {{- include "postgresql.labels" . | nindent 4 }} + name: postgres-pitr +spec: + deployKind: job + env: + - name: VOLUME_DATA_DIR + value: /home/postgres/pgdata + - name: RESTORE_SCRIPT_DIR + value: "$(VOLUME_DATA_DIR)/kb_restore" + - name: PITR_DIR + value: "$(VOLUME_DATA_DIR)/pitr" + - name: DATA_DIR + value: "$(VOLUME_DATA_DIR)/pgroot/data" + - name: CONF_DIR + value: "$(VOLUME_DATA_DIR)/conf" + - name: RECOVERY_TIME + value: $KB_RECOVERY_TIME + - name: TIME_FORMAT + value: 2006-01-02 15:04:05 MST + - name: LOG_DIR + value: $(VOLUME_DATA_DIR)/pgroot/data/pg_wal + image: "" + logical: + restoreCommands: + - | + set -e; + rm -f ${CONF_DIR}/recovery.conf; + rm -rf ${PITR_DIR}; + physical: + restoreCommands: + - | + set -e; + if [ -d ${DATA_DIR}.old ]; then echo "${DATA_DIR}.old directory already exists, skip restore."; exit 0; fi + mkdir -p ${PITR_DIR}; + cd ${PITR_DIR} + for i in $(find ${BACKUP_DIR} -name "*.gz"); do + echo "copying ${i}"; + cp ${i} $(basename $i); + gzip -df $(basename $i); + done + chmod 777 -R ${PITR_DIR}; + touch ${DATA_DIR}/recovery.signal; + mkdir -p ${CONF_DIR}; + chmod 777 -R ${CONF_DIR}; + mkdir -p ${RESTORE_SCRIPT_DIR}; + echo "#!/bin/bash" > ${RESTORE_SCRIPT_DIR}/kb_restore.sh; + echo "[[ -d '${DATA_DIR}.old' ]] && mv -f ${DATA_DIR}.old/* ${DATA_DIR}/;" >> ${RESTORE_SCRIPT_DIR}/kb_restore.sh; + echo "sync;" >> ${RESTORE_SCRIPT_DIR}/kb_restore.sh; + chmod +x ${RESTORE_SCRIPT_DIR}/kb_restore.sh; + echo "restore_command='case "%f" in *history) cp ${PITR_DIR}/%f %p ;; *) mv ${PITR_DIR}/%f %p ;; esac'" > ${CONF_DIR}/recovery.conf; + echo "recovery_target_time='${RECOVERY_TIME}'" >> ${CONF_DIR}/recovery.conf; + echo "recovery_target_action='promote'" >> ${CONF_DIR}/recovery.conf; + echo "recovery_target_timeline='latest'" >> ${CONF_DIR}/recovery.conf; + mv ${DATA_DIR} ${DATA_DIR}.old; + echo "done."; + sync; + backupCommands: + - | + set -e; + EXPIRED_INCR_LOG=${BACKUP_DIR}/$(date -d"7 day ago" +%Y%m%d); + if [ -d ${EXPIRED_INCR_LOG} ]; then rm -rf ${EXPIRED_INCR_LOG}; fi + export PGPASSWORD=${DB_PASSWORD} + PSQL="psql -h ${DB_HOST} -U ${DB_USER}" + LAST_TRANS=$(pg_waldump $(${PSQL} -Atc "select pg_walfile_name(pg_current_wal_lsn())") --rmgr=Transaction |tail -n 1) + if [ "${LAST_TRANS}" != "" ] && [ "$(find ${LOG_DIR}/archive_status/ -name '*.ready')" = "" ]; then + echo "switch wal file" + ${PSQL} -c "select pg_switch_wal()" + for i in $(seq 1 60); do + echo "waiting wal ready ..." + if [ "$(find ${LOG_DIR}/archive_status/ -name '*.ready')" != "" ]; then break; fi + sleep 1 + done + fi + TODAY_INCR_LOG=${BACKUP_DIR}/$(date +%Y%m%d); + mkdir -p ${TODAY_INCR_LOG}; + cd ${LOG_DIR} + for i in $(ls -tr ./archive_status/*.ready); do + wal_ready_name="${i##*/}" + wal_name=${wal_ready_name%.*} + echo "uploading ${wal_name}"; + gzip -kqc ${wal_name} > ${TODAY_INCR_LOG}/${wal_name}.gz; + mv -f ${i} ./archive_status/${wal_name}.done; + done + echo "done." + sync; + echo "TOTAL SIZE: $(du -shx ${BACKUP_DIR}|awk '{print $1}')" > ${DATA_DIR}/logfile.info; + + type: pitr \ No newline at end of file diff --git a/deploy/postgresql/templates/backuptool.yaml b/deploy/postgresql/templates/backuptool.yaml index 90e618194..5a5d89e11 100644 --- a/deploy/postgresql/templates/backuptool.yaml +++ b/deploy/postgresql/templates/backuptool.yaml @@ -6,15 +6,8 @@ metadata: clusterdefinition.kubeblocks.io/name: postgresql {{- include "postgresql.labels" . | nindent 4 }} spec: - image: registry.cn-hangzhou.aliyuncs.com/apecloud/postgresql:14.7.0 + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} deployKind: job - resources: - limits: - cpu: "1" - memory: 2Gi - requests: - cpu: "1" - memory: 128Mi env: - name: RESTORE_DATA_DIR value: /home/postgres/pgdata/kb_restore @@ -28,23 +21,12 @@ spec: restoreCommands: - | #!/bin/sh - set -e - # create a new directory for restore - mkdir -p ${RESTORE_DATA_DIR} && rm -rf ${RESTORE_DATA_DIR}/* - cd ${RESTORE_DATA_DIR} - touch kb_restore.sh && touch kb_restore.signal - echo "mkdir -p ${DATA_DIR}/../arch" >> kb_restore.sh - echo "mv -f ${TMP_DATA_DIR}/* ${DATA_DIR}/" >> kb_restore.sh - echo "mv -f ${TMP_ARCH_DATA_DIR}/* ${DATA_DIR}/../arch" >> kb_restore.sh - echo "rm -rf ${RESTORE_DATA_DIR}" >> kb_restore.sh - - # extract the data file to the temporary data directory - mkdir -p ${TMP_DATA_DIR} && mkdir -p ${TMP_ARCH_DATA_DIR} - rm -rf ${TMP_ARCH_DATA_DIR}/* && rm -rf ${TMP_DATA_DIR}/* - cd ${BACKUP_DIR}/${BACKUP_NAME} - tar -xvf base.tar.gz -C ${TMP_DATA_DIR}/ - tar -xvf pg_wal.tar.gz -C ${TMP_ARCH_DATA_DIR}/ - echo "done!" + set -e; + cd ${BACKUP_DIR}; + mkdir -p ${DATA_DIR}; + tar -xvf base.tar.gz -C ${DATA_DIR}/; + tar -xvf pg_wal.tar.gz -C ${DATA_DIR}/pg_wal/; + echo "done!"; incrementalRestoreCommands: [] logical: restoreCommands: [] @@ -52,6 +34,7 @@ spec: backupCommands: - > set -e; - mkdir -p ${BACKUP_DIR}/${BACKUP_NAME}/; - echo ${DB_PASSWORD} | pg_basebackup -Ft -Pv -Xs -z -D ${BACKUP_DIR}/${BACKUP_NAME} -Z5 -h ${DB_HOST} -U standby -W; + mkdir -p ${BACKUP_DIR}; + echo ${DB_PASSWORD} | pg_basebackup -Ft -Pv -Xs -z -D ${BACKUP_DIR} -Z5 -h ${DB_HOST} -U standby -W; + echo "TOTAL SIZE: $(du -shx ${BACKUP_DIR}|awk '{print $1}')" > ${DATA_DIR}/basebackup.info; incrementalBackupCommands: [] diff --git a/deploy/postgresql/templates/clusterdefinition.yaml b/deploy/postgresql/templates/clusterdefinition.yaml index 30642c4d3..ffdd1b27f 100644 --- a/deploy/postgresql/templates/clusterdefinition.yaml +++ b/deploy/postgresql/templates/clusterdefinition.yaml @@ -18,7 +18,7 @@ spec: characterType: postgresql customLabelSpecs: - key: apps.kubeblocks.postgres.patroni/scope - value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-patroni" + value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-patroni$(KB_CLUSTER_UID_POSTFIX_8)" resources: - gvk: "v1/Pod" selector: @@ -27,7 +27,7 @@ spec: selector: app.kubernetes.io/managed-by: kubeblocks probes: - roleChangedProbe: + roleProbe: failureThreshold: 2 periodSeconds: 1 timeoutSeconds: 1 @@ -50,8 +50,15 @@ spec: namespace: {{ .Release.Namespace }} volumeName: postgresql-config defaultMode: 0777 + - name: pgbouncer-configuration + templateRef: pgbouncer-configuration + keys: + - pgbouncer.ini + namespace: {{ .Release.Namespace }} + volumeName: pgbouncer-config + defaultMode: 0777 - name: postgresql-custom-metrics - templateRef: postgresql-custom-metrics + templateRef: postgresql14-custom-metrics namespace: {{ .Release.Namespace }} volumeName: postgresql-custom-metrics defaultMode: 0777 @@ -66,14 +73,13 @@ spec: - name: tcp-postgresql port: 5432 targetPort: tcp-postgresql - - name: http-metrics-postgresql - port: 9187 - targetPort: http-metrics + - name: tcp-pgbouncer + port: 6432 + targetPort: tcp-pgbouncer volumeTypes: - name: data type: data podSpec: - serviceAccountName: operator securityContext: runAsUser: 0 fsGroup: 103 @@ -86,13 +92,10 @@ spec: volumeMounts: - name: data mountPath: /home/postgres/pgdata - mode: 0777 - name: postgresql-config mountPath: /home/postgres/conf - mode: 0777 - name: scripts mountPath: /kb-scripts - mode: 0777 containers: - name: postgresql imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} @@ -101,9 +104,9 @@ spec: command: - /kb-scripts/setup.sh readinessProbe: - failureThreshold: 6 + failureThreshold: 3 initialDelaySeconds: 10 - periodSeconds: 10 + periodSeconds: 30 successThreshold: 1 timeoutSeconds: 5 exec: @@ -119,10 +122,8 @@ spec: mountPath: /dev/shm - name: data mountPath: /home/postgres/pgdata - mode: 0777 - name: postgresql-config mountPath: /home/postgres/conf - mode: 0777 - name: scripts mountPath: /kb-scripts ports: @@ -136,21 +137,24 @@ spec: - name: KUBERNETES_USE_CONFIGMAPS value: "true" - name: SCOPE - value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-patroni" + value: "$(KB_CLUSTER_NAME)-$(KB_COMP_NAME)-patroni$(KB_CLUSTER_UID_POSTFIX_8)" - name: KUBERNETES_SCOPE_LABEL value: "apps.kubeblocks.postgres.patroni/scope" - name: KUBERNETES_ROLE_LABEL value: "apps.kubeblocks.postgres.patroni/role" - name: KUBERNETES_LABELS - value: '{"app.kubernetes.io/instance":"$(KB_CLUSTER_NAME)","apps.kubeblocks.io/component-name":"$(KB_COMP_NAME)","app.kubernetes.io/managed-by":"kubeblocks"}' + value: '{"app.kubernetes.io/instance":"$(KB_CLUSTER_NAME)","apps.kubeblocks.io/component-name":"$(KB_COMP_NAME)"}' - name: RESTORE_DATA_DIR value: /home/postgres/pgdata/kb_restore + - name: KB_PG_CONFIG_PATH + value: /home/postgres/conf/postgresql.conf - name: SPILO_CONFIGURATION value: | ## https://github.com/zalando/patroni#yaml-configuration bootstrap: initdb: - auth-host: md5 - auth-local: trust + - wal-segsize: "1024" - name: ALLOW_NOSSL value: "true" - name: PGROOT @@ -170,11 +174,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: PGPASSWORD_SUPERUSER valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: PGUSER_ADMIN value: superadmin - name: PGPASSWORD_ADMIN @@ -182,21 +188,83 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false + - name: PGUSER_STANDBY + value: standby - name: PGPASSWORD_STANDBY valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: PGUSER valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: PGPASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false + - name: pgbouncer + imagePullPolicy: {{ .Values.pgbouncer.image.pullPolicy | quote }} + securityContext: + runAsUser: 0 + ports: + - name: tcp-pgbouncer + containerPort: 6432 + volumeMounts: + - name: pgbouncer-config + mountPath: /home/pgbouncer/conf + - name: scripts + mountPath: /kb-scripts + command: + - /kb-scripts/pgbouncer_setup.sh + livenessProbe: + failureThreshold: 3 + initialDelaySeconds: 15 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + tcpSocket: + port: tcp-pgbouncer + readinessProbe: + failureThreshold: 3 + initialDelaySeconds: 15 + periodSeconds: 30 + successThreshold: 1 + timeoutSeconds: 5 + tcpSocket: + port: tcp-pgbouncer + env: + - name: PGBOUNCER_AUTH_TYPE + value: md5 + - name: POSTGRESQL_USERNAME + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: username + optional: false + - name: POSTGRESQL_PASSWORD + valueFrom: + secretKeyRef: + name: $(CONN_CREDENTIAL_SECRET_NAME) + key: password + optional: false + - name: POSTGRESQL_PORT + value: "5432" + - name: POSTGRESQL_HOST + valueFrom: + fieldRef: + apiVersion: v1 + fieldPath: status.podIP + - name: PGBOUNCER_PORT + value: "6432" + - name: PGBOUNCER_BIND_ADDRESS + value: "0.0.0.0" - name: metrics image: {{ .Values.metrics.image.registry | default "docker.io" }}/{{ .Values.metrics.image.repository }}:{{ .Values.metrics.image.tag }} imagePullPolicy: {{ .Values.metrics.image.pullPolicy | quote }} @@ -212,11 +280,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false - name: DATA_SOURCE_USER valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false command: - "/opt/bitnami/postgres-exporter/bin/postgres_exporter" - "--auto-discover-databases" @@ -269,11 +339,13 @@ spec: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: username + optional: false - name: PGPASSWORD valueFrom: secretKeyRef: name: $(CONN_CREDENTIAL_SECRET_NAME) key: password + optional: false passwordConfig: length: 10 numDigits: 5 @@ -281,37 +353,27 @@ spec: letterCase: MixedCases accounts: - name: kbadmin - provisionPolicy: + provisionPolicy: &kbAdminAcctRef type: CreateByStmt scope: AnyPods statements: creation: CREATE USER $(USERNAME) SUPERUSER PASSWORD '$(PASSWD)'; - deletion: DROP USER IF EXISTS $(USERNAME); + update: ALTER USER $(USERNAME) WITH PASSWORD '$(PASSWD)'; - name: kbdataprotection - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: CREATE USER $(USERNAME) SUPERUSER PASSWORD '$(PASSWD)'; - deletion: DROP USER IF EXISTS $(USERNAME); + provisionPolicy: *kbAdminAcctRef - name: kbprobe - provisionPolicy: + provisionPolicy: &kbReadonlyAcctRef type: CreateByStmt scope: AnyPods statements: creation: CREATE USER $(USERNAME) WITH PASSWORD '$(PASSWD)'; GRANT pg_monitor TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + update: ALTER USER $(USERNAME) WITH PASSWORD '$(PASSWD)'; - name: kbmonitoring - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: CREATE USER $(USERNAME) WITH PASSWORD '$(PASSWD)'; GRANT pg_monitor TO $(USERNAME); - deletion: DROP USER IF EXISTS $(USERNAME); + provisionPolicy: *kbReadonlyAcctRef - name: kbreplicator provisionPolicy: type: CreateByStmt scope: AnyPods statements: creation: CREATE USER $(USERNAME) WITH REPLICATION PASSWORD '$(PASSWD)'; - deletion: DROP USER IF EXISTS $(USERNAME); + update: ALTER USER $(USERNAME) WITH PASSWORD '$(PASSWD)'; diff --git a/deploy/postgresql/templates/clusterversion.yaml b/deploy/postgresql/templates/clusterversion.yaml index 9b611bd20..3fbbcf04d 100644 --- a/deploy/postgresql/templates/clusterversion.yaml +++ b/deploy/postgresql/templates/clusterversion.yaml @@ -1,7 +1,8 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: ClusterVersion metadata: - name: postgresql-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} + # major version of the component defined in values.yaml + name: postgresql-{{ .Values.image.tag }} labels: {{- include "postgresql.labels" . | nindent 4 }} spec: @@ -15,4 +16,49 @@ spec: containers: - name: postgresql image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} + - name: pgbouncer + image: {{ .Values.pgbouncer.image.repository }}:{{ .Values.pgbouncer.image.tag }} + systemAccountSpec: + cmdExecutorConfig: + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ .Values.image.tag }} + --- +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterVersion +metadata: + name: postgresql-12.14.1 + annotations: + kubeblocks.io/is-default-cluster-version: "true" + labels: + {{- include "postgresql.labels" . | nindent 4 }} +spec: + clusterDefinitionRef: postgresql + componentVersions: + - componentDefRef: postgresql + configSpecs: + # name needs to consistent with the name of the configmap defined in clusterDefinition, and replace the templateRef with postgres v12.14.0 configmap + - name: postgresql-configuration + templateRef: postgresql12-configuration + constraintRef: postgresql12-cc + keys: + - postgresql.conf + namespace: {{ .Release.Namespace }} + volumeName: postgresql-config + defaultMode: 0777 + - name: postgresql-custom-metrics + templateRef: postgresql12-custom-metrics + namespace: {{ .Release.Namespace }} + volumeName: postgresql-custom-metrics + defaultMode: 0777 + versionsContext: + initContainers: + - name: pg-init-container + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:12.14.1 + containers: + - name: postgresql + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:12.14.1 + - name: pgbouncer + image: {{ .Values.pgbouncer.image.repository }}:{{ .Values.pgbouncer.image.tag }} + systemAccountSpec: + cmdExecutorConfig: + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:12.14.1 diff --git a/deploy/postgresql/templates/configconstraint-12.yaml b/deploy/postgresql/templates/configconstraint-12.yaml new file mode 100644 index 000000000..13bf07a13 --- /dev/null +++ b/deploy/postgresql/templates/configconstraint-12.yaml @@ -0,0 +1,53 @@ +{{- $cc := .Files.Get "config/pg12-config-effect-scope.yaml" | fromYaml }} +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ConfigConstraint +metadata: + name: postgresql12-cc + labels: + {{- include "postgresql.labels" . | nindent 4 }} +spec: + reloadOptions: + tplScriptTrigger: + sync: true + scriptConfigMapRef: patroni-reload-script + namespace: {{ .Release.Namespace }} + + # update patroni master + selector: + matchLabels: + "apps.kubeblocks.postgres.patroni/role": "master" + + # top level mysql configuration type + cfgSchemaTopLevelName: PGParameter + + # ConfigurationSchema that impose restrictions on engine parameter's rule + configurationSchema: + # schema: auto generate from mmmcue scripts + # example: ../../internal/configuration/testdata/mysql_openapi.json + cue: |- + {{- .Files.Get "config/pg12-config-constraint.cue" | nindent 6 }} + + ## require db instance restart + ## staticParameters + {{- if hasKey $cc "staticParameters" }} + staticParameters: + {{- $params := get $cc "staticParameters" }} + {{- range $params }} + - {{ . }} + {{- end }} + {{- end}} + + ## define immutable parameter list, this feature is not currently supported. + {{- if hasKey $cc "immutableParameters" }} + immutableParameters: + {{- $params := get $cc "immutableParameters" }} + {{- range $params }} + - {{ . }} + {{- end }} + {{- end}} + + + + # configuration file format + formatterConfig: + format: properties diff --git a/deploy/postgresql/templates/configconstraint.yaml b/deploy/postgresql/templates/configconstraint-14.yaml similarity index 91% rename from deploy/postgresql/templates/configconstraint.yaml rename to deploy/postgresql/templates/configconstraint-14.yaml index d5da741fa..97c1787e4 100644 --- a/deploy/postgresql/templates/configconstraint.yaml +++ b/deploy/postgresql/templates/configconstraint-14.yaml @@ -8,9 +8,15 @@ metadata: spec: reloadOptions: tplScriptTrigger: + sync: true scriptConfigMapRef: patroni-reload-script namespace: {{ .Release.Namespace }} + # update patroni master + selector: + matchLabels: + "apps.kubeblocks.postgres.patroni/role": "master" + # top level mysql configuration type cfgSchemaTopLevelName: PGParameter diff --git a/deploy/postgresql/templates/configmap-12.yaml b/deploy/postgresql/templates/configmap-12.yaml new file mode 100644 index 000000000..b09f48162 --- /dev/null +++ b/deploy/postgresql/templates/configmap-12.yaml @@ -0,0 +1,25 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgresql12-configuration + labels: + {{- include "postgresql.labels" . | nindent 4 }} +data: + postgresql.conf: |- + {{- .Files.Get "config/pg12-config.tpl" | nindent 4 }} + # TODO: check if it should trust all + pg_hba.conf: | + host all all 0.0.0.0/0 md5 + host all all ::/0 md5 + local all all trust + host all all 127.0.0.1/32 trust + host all all ::1/128 trust + local replication all trust + host replication all 0.0.0.0/0 md5 + host replication all ::/0 md5 + kb_pitr.conf: | + method: kb_restore_from_time + kb_restore_from_time: + command: bash /home/postgres/pgdata/kb_restore/kb_restore.sh + keep_existing_recovery_conf: false + recovery_conf: {} \ No newline at end of file diff --git a/deploy/postgresql/templates/configmap.yaml b/deploy/postgresql/templates/configmap-14.yaml similarity index 74% rename from deploy/postgresql/templates/configmap.yaml rename to deploy/postgresql/templates/configmap-14.yaml index f4965bf73..b54b64497 100644 --- a/deploy/postgresql/templates/configmap.yaml +++ b/deploy/postgresql/templates/configmap-14.yaml @@ -9,18 +9,17 @@ data: {{- .Files.Get "config/pg14-config.tpl" | nindent 4 }} # TODO: check if it should trust all pg_hba.conf: | - host all all 0.0.0.0/0 trust - host all all ::/0 trust + host all all 0.0.0.0/0 md5 + host all all ::/0 md5 local all all trust host all all 127.0.0.1/32 trust host all all ::1/128 trust + local replication all trust host replication all 0.0.0.0/0 md5 host replication all ::/0 md5 - kb_restore.conf: | - method: kb_restore_from_backup - kb_restore_from_backup: - command: sh /home/postgres/pgdata/kb_restore/kb_restore.sh + kb_pitr.conf: | + method: kb_restore_from_time + kb_restore_from_time: + command: bash /home/postgres/pgdata/kb_restore/kb_restore.sh keep_existing_recovery_conf: false - recovery_conf: - restore_command: cp /home/postgres/pgdata/pgroot/arch/%f %p - recovery_target_timeline: latest + recovery_conf: {} \ No newline at end of file diff --git a/deploy/postgresql/templates/metrics-configmap-12.yaml b/deploy/postgresql/templates/metrics-configmap-12.yaml new file mode 100644 index 000000000..338b865a0 --- /dev/null +++ b/deploy/postgresql/templates/metrics-configmap-12.yaml @@ -0,0 +1,279 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgresql12-custom-metrics + labels: + {{- include "postgresql.labels" . | nindent 4 }} +data: + custom-metrics.yaml: |- + pg_postmaster: + query: "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()" + master: true + metrics: + - start_time_seconds: + usage: "GAUGE" + description: "Time at which postmaster started" + + pg_replication: + query: | + SELECT + (case when (not pg_is_in_recovery() or pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()) then 0 else greatest (0, extract(epoch from (now() - pg_last_xact_replay_timestamp()))) end) as lag, + (case when pg_is_in_recovery() then 0 else 1 end) as is_master + master: true + metrics: + - lag: + usage: "GAUGE" + description: "Replication lag behind master in seconds" + - is_master: + usage: "GAUGE" + description: "Instance is master or slave" + + pg_stat_user_tables: + query: | + SELECT + current_database() datname, + schemaname, + relname, + seq_scan, + seq_tup_read, + idx_scan, + idx_tup_fetch, + n_tup_ins, + n_tup_upd, + n_tup_del, + n_tup_hot_upd, + n_live_tup, + n_dead_tup, + n_mod_since_analyze, + COALESCE(last_vacuum, '1970-01-01Z') as last_vacuum, + COALESCE(last_autovacuum, '1970-01-01Z') as last_autovacuum, + COALESCE(last_analyze, '1970-01-01Z') as last_analyze, + COALESCE(last_autoanalyze, '1970-01-01Z') as last_autoanalyze, + vacuum_count, + autovacuum_count, + analyze_count, + autoanalyze_count + FROM + pg_stat_user_tables + metrics: + - datname: + usage: "LABEL" + description: "Name of current database" + - schemaname: + usage: "LABEL" + description: "Name of the schema that this table is in" + - relname: + usage: "LABEL" + description: "Name of this table" + - seq_scan: + usage: "COUNTER" + description: "Number of sequential scans initiated on this table" + - seq_tup_read: + usage: "COUNTER" + description: "Number of live rows fetched by sequential scans" + - idx_scan: + usage: "COUNTER" + description: "Number of index scans initiated on this table" + - idx_tup_fetch: + usage: "COUNTER" + description: "Number of live rows fetched by index scans" + - n_tup_ins: + usage: "COUNTER" + description: "Number of rows inserted" + - n_tup_upd: + usage: "COUNTER" + description: "Number of rows updated" + - n_tup_del: + usage: "COUNTER" + description: "Number of rows deleted" + - n_tup_hot_upd: + usage: "COUNTER" + description: "Number of rows HOT updated (i.e., with no separate index update required)" + - n_live_tup: + usage: "GAUGE" + description: "Estimated number of live rows" + - n_dead_tup: + usage: "GAUGE" + description: "Estimated number of dead rows" + - n_mod_since_analyze: + usage: "GAUGE" + description: "Estimated number of rows changed since last analyze" + - last_vacuum: + usage: "GAUGE" + description: "Last time at which this table was manually vacuumed (not counting VACUUM FULL)" + - last_autovacuum: + usage: "GAUGE" + description: "Last time at which this table was vacuumed by the autovacuum daemon" + - last_analyze: + usage: "GAUGE" + description: "Last time at which this table was manually analyzed" + - last_autoanalyze: + usage: "GAUGE" + description: "Last time at which this table was analyzed by the autovacuum daemon" + - vacuum_count: + usage: "COUNTER" + description: "Number of times this table has been manually vacuumed (not counting VACUUM FULL)" + - autovacuum_count: + usage: "COUNTER" + description: "Number of times this table has been vacuumed by the autovacuum daemon" + - analyze_count: + usage: "COUNTER" + description: "Number of times this table has been manually analyzed" + - autoanalyze_count: + usage: "COUNTER" + description: "Number of times this table has been analyzed by the autovacuum daemon" + + pg_statio_user_tables: + query: | + SELECT + current_database() datname, + schemaname, + relname, + heap_blks_read, + heap_blks_hit, + idx_blks_read, + idx_blks_hit, + toast_blks_read, + toast_blks_hit, + tidx_blks_read, + tidx_blks_hit + FROM + pg_statio_user_tables + metrics: + - datname: + usage: "LABEL" + description: "Name of current database" + - schemaname: + usage: "LABEL" + description: "Name of the schema that this table is in" + - relname: + usage: "LABEL" + description: "Name of this table" + - heap_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table" + - heap_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table" + - idx_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from all indexes on this table" + - idx_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in all indexes on this table" + - toast_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table's TOAST table (if any)" + - toast_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table's TOAST table (if any)" + - tidx_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table's TOAST table indexes (if any)" + - tidx_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table's TOAST table indexes (if any)" + + # WARNING: This set of metrics can be very expensive on a busy server as every unique query executed will create an additional time series + pg_stat_statements: + query: | + SELECT + t2.rolname, + t3.datname, + queryid, + calls, + total_time / 1000 as total_exec_time_seconds, + min_time / 1000 as min_exec_time_seconds, + max_time / 1000 as max_exec_time_seconds, + mean_time / 1000 as mean_exec_time_seconds, + stddev_time / 1000 as stddev_exec_time_seconds, + rows, + shared_blks_hit, + shared_blks_read, + shared_blks_dirtied, + shared_blks_written, + local_blks_hit, + local_blks_read, + local_blks_dirtied, + local_blks_written, + temp_blks_read, + temp_blks_written, + blk_read_time / 1000 as blk_read_time_seconds, + blk_write_time / 1000 as blk_write_time_seconds + FROM + pg_stat_statements t1 + JOIN + pg_roles t2 + ON (t1.userid=t2.oid) + JOIN + pg_database t3 + ON (t1.dbid=t3.oid) + WHERE t2.rolname != 'rdsadmin' + master: true + metrics: + - rolname: + usage: "LABEL" + description: "Name of user" + - datname: + usage: "LABEL" + description: "Name of database" + - queryid: + usage: "LABEL" + description: "Query ID" + - calls: + usage: "COUNTER" + description: "Number of times executed" + - total_exec_time_seconds: + usage: "COUNTER" + description: "Total time spent in the statement" + - min_exec_time_seconds: + usage: "GAUGE" + description: "Minimum time spent in the statement" + - max_exec_time_seconds: + usage: "GAUGE" + description: "Maximum time spent in the statement" + - mean_exec_time_seconds: + usage: "GAUGE" + description: "Mean time spent in the statement" + - stddev_exec_time_seconds: + usage: "GAUGE" + description: "Population standard deviation of time spent in the statement" + - rows: + usage: "COUNTER" + description: "Total number of rows retrieved or affected by the statement" + - shared_blks_hit: + usage: "COUNTER" + description: "Total number of shared block cache hits by the statement" + - shared_blks_read: + usage: "COUNTER" + description: "Total number of shared blocks read by the statement" + - shared_blks_dirtied: + usage: "COUNTER" + description: "Total number of shared blocks dirtied by the statement" + - shared_blks_written: + usage: "COUNTER" + description: "Total number of shared blocks written by the statement" + - local_blks_hit: + usage: "COUNTER" + description: "Total number of local block cache hits by the statement" + - local_blks_read: + usage: "COUNTER" + description: "Total number of local blocks read by the statement" + - local_blks_dirtied: + usage: "COUNTER" + description: "Total number of local blocks dirtied by the statement" + - local_blks_written: + usage: "COUNTER" + description: "Total number of local blocks written by the statement" + - temp_blks_read: + usage: "COUNTER" + description: "Total number of temp blocks read by the statement" + - temp_blks_written: + usage: "COUNTER" + description: "Total number of temp blocks written by the statement" + - blk_read_time_seconds: + usage: "COUNTER" + description: "Total time the statement spent reading blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" + - blk_write_time_seconds: + usage: "COUNTER" + description: "Total time the statement spent writing blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" diff --git a/deploy/postgresql/templates/metrics-configmap-14.yaml b/deploy/postgresql/templates/metrics-configmap-14.yaml new file mode 100644 index 000000000..39232565b --- /dev/null +++ b/deploy/postgresql/templates/metrics-configmap-14.yaml @@ -0,0 +1,319 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: postgresql14-custom-metrics + labels: + {{- include "postgresql.labels" . | nindent 4 }} +data: + custom-metrics.yaml: |- + pg_postmaster: + query: "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()" + master: true + metrics: + - start_time_seconds: + usage: "GAUGE" + description: "Time at which postmaster started" + + pg_replication: + query: | + SELECT + (case when (not pg_is_in_recovery() or pg_last_wal_receive_lsn() = pg_last_wal_replay_lsn()) then 0 else greatest (0, extract(epoch from (now() - pg_last_xact_replay_timestamp()))) end) as lag, + (case when pg_is_in_recovery() then 0 else 1 end) as is_master + master: true + metrics: + - lag: + usage: "GAUGE" + description: "Replication lag behind master in seconds" + - is_master: + usage: "GAUGE" + description: "Instance is master or slave" + + pg_stat_user_tables: + query: | + SELECT + current_database() datname, + schemaname, + relname, + seq_scan, + seq_tup_read, + idx_scan, + idx_tup_fetch, + n_tup_ins, + n_tup_upd, + n_tup_del, + n_tup_hot_upd, + n_live_tup, + n_dead_tup, + n_mod_since_analyze, + n_ins_since_vacuum, + COALESCE(last_vacuum, '1970-01-01Z') as last_vacuum, + COALESCE(last_autovacuum, '1970-01-01Z') as last_autovacuum, + COALESCE(last_analyze, '1970-01-01Z') as last_analyze, + COALESCE(last_autoanalyze, '1970-01-01Z') as last_autoanalyze, + vacuum_count, + autovacuum_count, + analyze_count, + autoanalyze_count + FROM + pg_stat_user_tables + metrics: + - datname: + usage: "LABEL" + description: "Name of current database" + - schemaname: + usage: "LABEL" + description: "Name of the schema that this table is in" + - relname: + usage: "LABEL" + description: "Name of this table" + - seq_scan: + usage: "COUNTER" + description: "Number of sequential scans initiated on this table" + - seq_tup_read: + usage: "COUNTER" + description: "Number of live rows fetched by sequential scans" + - idx_scan: + usage: "COUNTER" + description: "Number of index scans initiated on this table" + - idx_tup_fetch: + usage: "COUNTER" + description: "Number of live rows fetched by index scans" + - n_tup_ins: + usage: "COUNTER" + description: "Number of rows inserted" + - n_tup_upd: + usage: "COUNTER" + description: "Number of rows updated" + - n_tup_del: + usage: "COUNTER" + description: "Number of rows deleted" + - n_tup_hot_upd: + usage: "COUNTER" + description: "Number of rows HOT updated (i.e., with no separate index update required)" + - n_live_tup: + usage: "GAUGE" + description: "Estimated number of live rows" + - n_dead_tup: + usage: "GAUGE" + description: "Estimated number of dead rows" + - n_mod_since_analyze: + usage: "GAUGE" + description: "Estimated number of rows changed since last analyze" + - n_ins_since_vacuum: + usage: "GAUGE" + description: "Estimated number of rows inserted since this table was last vacuumed" + - last_vacuum: + usage: "GAUGE" + description: "Last time at which this table was manually vacuumed (not counting VACUUM FULL)" + - last_autovacuum: + usage: "GAUGE" + description: "Last time at which this table was vacuumed by the autovacuum daemon" + - last_analyze: + usage: "GAUGE" + description: "Last time at which this table was manually analyzed" + - last_autoanalyze: + usage: "GAUGE" + description: "Last time at which this table was analyzed by the autovacuum daemon" + - vacuum_count: + usage: "COUNTER" + description: "Number of times this table has been manually vacuumed (not counting VACUUM FULL)" + - autovacuum_count: + usage: "COUNTER" + description: "Number of times this table has been vacuumed by the autovacuum daemon" + - analyze_count: + usage: "COUNTER" + description: "Number of times this table has been manually analyzed" + - autoanalyze_count: + usage: "COUNTER" + description: "Number of times this table has been analyzed by the autovacuum daemon" + + pg_statio_user_tables: + query: | + SELECT + current_database() datname, + schemaname, + relname, + heap_blks_read, + heap_blks_hit, + idx_blks_read, + idx_blks_hit, + toast_blks_read, + toast_blks_hit, + tidx_blks_read, + tidx_blks_hit + FROM + pg_statio_user_tables + metrics: + - datname: + usage: "LABEL" + description: "Name of current database" + - schemaname: + usage: "LABEL" + description: "Name of the schema that this table is in" + - relname: + usage: "LABEL" + description: "Name of this table" + - heap_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table" + - heap_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table" + - idx_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from all indexes on this table" + - idx_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in all indexes on this table" + - toast_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table's TOAST table (if any)" + - toast_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table's TOAST table (if any)" + - tidx_blks_read: + usage: "COUNTER" + description: "Number of disk blocks read from this table's TOAST table indexes (if any)" + - tidx_blks_hit: + usage: "COUNTER" + description: "Number of buffer hits in this table's TOAST table indexes (if any)" + + # WARNING: This set of metrics can be very expensive on a busy server as every unique query executed will create an additional time series + pg_stat_statements: + query: | + SELECT + t2.rolname, + t3.datname, + queryid, + plans, + total_plan_time / 1000 as total_plan_time_seconds, + min_plan_time / 1000 as min_plan_time_seconds, + max_plan_time / 1000 as max_plan_time_seconds, + mean_plan_time / 1000 as mean_plan_time_seconds, + stddev_plan_time / 1000 as stddev_plan_time_seconds, + calls, + total_exec_time / 1000 as total_exec_time_seconds, + min_exec_time / 1000 as min_exec_time_seconds, + max_exec_time / 1000 as max_exec_time_seconds, + mean_exec_time / 1000 as mean_exec_time_seconds, + stddev_exec_time / 1000 as stddev_exec_time_seconds, + rows, + shared_blks_hit, + shared_blks_read, + shared_blks_dirtied, + shared_blks_written, + local_blks_hit, + local_blks_read, + local_blks_dirtied, + local_blks_written, + temp_blks_read, + temp_blks_written, + blk_read_time / 1000 as blk_read_time_seconds, + blk_write_time / 1000 as blk_write_time_seconds, + wal_records, + wal_fpi, + wal_bytes + FROM + pg_stat_statements t1 + JOIN + pg_roles t2 + ON (t1.userid=t2.oid) + JOIN + pg_database t3 + ON (t1.dbid=t3.oid) + WHERE t2.rolname != 'rdsadmin' + master: true + metrics: + - rolname: + usage: "LABEL" + description: "Name of user" + - datname: + usage: "LABEL" + description: "Name of database" + - queryid: + usage: "LABEL" + description: "Query ID" + - plans: + usage: "COUNTER" + description: "Number of times the statement was planned" + - total_plan_time_seconds: + usage: "COUNTER" + description: "Total time spent planning the statement" + - min_plan_time_seconds: + usage: "GAUGE" + description: "Minimum time spent planning the statement" + - max_plan_time_seconds: + usage: "GAUGE" + description: "Maximum time spent planning the statement" + - mean_plan_time_seconds: + usage: "GAUGE" + description: "Mean time spent planning the statement" + - stddev_plan_time_seconds: + usage: "GAUGE" + description: "Population standard deviation of time spent planning the statement" + - calls: + usage: "COUNTER" + description: "Number of times executed" + - total_exec_time_seconds: + usage: "COUNTER" + description: "Total time spent in the statement" + - min_exec_time_seconds: + usage: "GAUGE" + description: "Minimum time spent in the statement" + - max_exec_time_seconds: + usage: "GAUGE" + description: "Maximum time spent in the statement" + - mean_exec_time_seconds: + usage: "GAUGE" + description: "Mean time spent in the statement" + - stddev_exec_time_seconds: + usage: "GAUGE" + description: "Population standard deviation of time spent in the statement" + - rows: + usage: "COUNTER" + description: "Total number of rows retrieved or affected by the statement" + - shared_blks_hit: + usage: "COUNTER" + description: "Total number of shared block cache hits by the statement" + - shared_blks_read: + usage: "COUNTER" + description: "Total number of shared blocks read by the statement" + - shared_blks_dirtied: + usage: "COUNTER" + description: "Total number of shared blocks dirtied by the statement" + - shared_blks_written: + usage: "COUNTER" + description: "Total number of shared blocks written by the statement" + - local_blks_hit: + usage: "COUNTER" + description: "Total number of local block cache hits by the statement" + - local_blks_read: + usage: "COUNTER" + description: "Total number of local blocks read by the statement" + - local_blks_dirtied: + usage: "COUNTER" + description: "Total number of local blocks dirtied by the statement" + - local_blks_written: + usage: "COUNTER" + description: "Total number of local blocks written by the statement" + - temp_blks_read: + usage: "COUNTER" + description: "Total number of temp blocks read by the statement" + - temp_blks_written: + usage: "COUNTER" + description: "Total number of temp blocks written by the statement" + - blk_read_time_seconds: + usage: "COUNTER" + description: "Total time the statement spent reading blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" + - blk_write_time_seconds: + usage: "COUNTER" + description: "Total time the statement spent writing blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" + - wal_records: + usage: "COUNTER" + description: "Total number of WAL records generated by the statement" + - wal_fpi: + usage: "COUNTER" + description: "Total number of WAL full page images generated by the statement" + - wal_bytes: + usage: "COUNTER" + description: "Total amount of WAL generated by the statement in bytes" diff --git a/deploy/postgresql/templates/metrics-configmap.yaml b/deploy/postgresql/templates/metrics-configmap.yaml deleted file mode 100644 index 4b26e3207..000000000 --- a/deploy/postgresql/templates/metrics-configmap.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: ConfigMap -metadata: - name: postgresql-custom-metrics - labels: - {{- include "postgresql.labels" . | nindent 4 }} -data: - custom-metrics.yaml: {{ toYaml .Values.metrics.customMetrics | quote }} diff --git a/deploy/postgresql/templates/patroni-rbac.yaml b/deploy/postgresql/templates/patroni-rbac.yaml deleted file mode 100644 index f5f22c5e5..000000000 --- a/deploy/postgresql/templates/patroni-rbac.yaml +++ /dev/null @@ -1,74 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - namespace: default - name: operator - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRole -metadata: - name: operator -rules: - - apiGroups: - - "" - resources: - - configmaps - verbs: - - create - - get - - list - - patch - - update - - watch - # delete is required only for 'patronictl remove' - - delete - - apiGroups: - - "" - resources: - - endpoints - verbs: - - get - - patch - - update - # the following three privileges are necessary only when using endpoints - - create - - list - - watch - # delete is required only for for 'patronictl remove' - - delete - - apiGroups: - - "" - resources: - - pods - verbs: - - get - - list - - patch - - update - - watch - # The following privilege is only necessary for creation of headless service - # for patronidemo-config endpoint, in order to prevent cleaning it up by the - # k8s master. You can avoid giving this privilege by explicitly creating the - # service like it is done in this manifest (lines 160..169) - - apiGroups: - - "" - resources: - - services - verbs: - - create - ---- -apiVersion: rbac.authorization.k8s.io/v1 -kind: ClusterRoleBinding -metadata: - name: operator -roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: operator -subjects: - - kind: ServiceAccount - name: operator - namespace: default \ No newline at end of file diff --git a/deploy/postgresql/templates/pgbouncer-configmap.yaml b/deploy/postgresql/templates/pgbouncer-configmap.yaml new file mode 100644 index 000000000..0b54bbccb --- /dev/null +++ b/deploy/postgresql/templates/pgbouncer-configmap.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: pgbouncer-configuration + labels: + {{- include "postgresql.labels" . | nindent 4 }} +data: + pgbouncer.ini: |- + {{- .Files.Get "config/pgbouncer-ini.tpl" | nindent 4 }} \ No newline at end of file diff --git a/deploy/postgresql/templates/scripts.yaml b/deploy/postgresql/templates/scripts.yaml index 45f775fad..a49ed45c5 100644 --- a/deploy/postgresql/templates/scripts.yaml +++ b/deploy/postgresql/templates/scripts.yaml @@ -32,6 +32,19 @@ data: if line and not line.startswith('#'): ret.append(line) return ret + def postgresql_conf_to_dict(file_path): + with open(file_path, 'r') as f: + content = f.read() + lines = content.splitlines() + result = {} + for line in lines: + if line.startswith('#'): + continue + if '=' not in line: + continue + key, value = line.split('=', 1) + result[key.strip()] = value.strip().strip("'") + return result def main(filename): restore_dir = os.environ.get('RESTORE_DATA_DIR', '') local_config = yaml.safe_load( @@ -41,7 +54,6 @@ data: postgresql = local_config['postgresql'] postgresql['config_dir'] = '/home/postgres/pgdata/conf' postgresql['custom_conf'] = '/home/postgres/conf/postgresql.conf' - # TODO add local postgresql.parameters # add pg_hba.conf with open('/home/postgres/conf/pg_hba.conf', 'r') as f: lines = read_file_lines(f) @@ -52,6 +64,14 @@ data: local_config['bootstrap'] = {} with open('/home/postgres/conf/kb_restore.conf', 'r') as f: local_config['bootstrap'].update(yaml.safe_load(f)) + # point in time recovery(PITR) + data_dir = os.environ.get('PGDATA', '') + if os.path.isfile("/home/postgres/pgdata/conf/recovery.conf"): + with open('/home/postgres/conf/kb_pitr.conf', 'r') as f: + pitr_config = yaml.safe_load(f) + re_config = postgresql_conf_to_dict("/home/postgres/pgdata/conf/recovery.conf") + pitr_config[pitr_config['method']]['recovery_conf'].update(re_config) + local_config['bootstrap'].update(pitr_config) write_file(yaml.dump(local_config, default_flow_style=False), filename, True) if __name__ == '__main__': main(sys.argv[1]) @@ -61,9 +81,67 @@ data: set -ex KB_PRIMARY_POD_NAME_PREFIX=${KB_PRIMARY_POD_NAME%%\.*} if [ "$KB_PRIMARY_POD_NAME_PREFIX" != "$KB_POD_NAME" ]; then - sleep 3 + # waiting for primary pod to be ready + until pg_isready -U {{ default "postgres" | quote }} -h $KB_PRIMARY_POD_NAME -p 5432; do + sleep 5 + done + fi + if [ -f ${RESTORE_DATA_DIR}/kb_restore.signal ]; then + chown -R postgres ${RESTORE_DATA_DIR} fi python3 /kb-scripts/generate_patroni_yaml.py tmp_patroni.yaml export SPILO_CONFIGURATION=$(cat tmp_patroni.yaml) # export SCOPE="$KB_CLUSTER_NAME-$KB_CLUSTER_NAME" - exec /launch.sh init \ No newline at end of file + exec /launch.sh init + pgbouncer_setup.sh: | + #!/bin/bash + set -o errexit + set -ex + mkdir -p /opt/bitnami/pgbouncer/conf/ /opt/bitnami/pgbouncer/logs/ /opt/bitnami/pgbouncer/tmp/ + cp /home/pgbouncer/conf/pgbouncer.ini /opt/bitnami/pgbouncer/conf/ + echo "\"$POSTGRESQL_USERNAME\" \"$POSTGRESQL_PASSWORD\"" > /opt/bitnami/pgbouncer/conf/userlist.txt + echo -e "\\n[databases]" >> /opt/bitnami/pgbouncer/conf/pgbouncer.ini + echo "postgres=host=$KB_POD_IP port=5432 dbname=postgres" >> /opt/bitnami/pgbouncer/conf/pgbouncer.ini + chmod +777 /opt/bitnami/pgbouncer/conf/pgbouncer.ini + chmod +777 /opt/bitnami/pgbouncer/conf/userlist.txt + useradd pgbouncer + chown -R pgbouncer:pgbouncer /opt/bitnami/pgbouncer/conf/ /opt/bitnami/pgbouncer/logs/ /opt/bitnami/pgbouncer/tmp/ + /opt/bitnami/scripts/pgbouncer/run.sh + backup-log-collector.sh: | + #!/bin/bash + set -o errexit + set -o nounset + SHOW_START_TIME=$1 + LOG_START_TIME="" + LOG_STOP_TIME="" + if [ "$SHOW_START_TIME" == "false" ]; then + latest_done_wal=$(ls -t ${PGDATA}/pg_wal/archive_status/|grep ".done"|head -n 1) + if [ "${latest_done_wal}" != "" ]; then + LOG_STOP_TIME=$(pg_waldump ${latest_done_wal%.*} --rmgr=Transaction 2>/dev/null |tail -n 1|awk -F ' COMMIT ' '{print $2}'|awk -F ';' '{print $1}') + fi + [[ "${LOG_STOP_TIME}" != "" ]] && printf "{\"stopTime\": \"$(date -d "$LOG_STOP_TIME" -u '+%Y-%m-%dT%H:%M:%SZ')\"}" || printf "{}" + else + LOG_START_TIME=$(pg_waldump $(ls -Ftr $PGDATA/pg_wal/ | grep '[[:xdigit:]]$\|.partial$'|head -n 1) --rmgr=Transaction 2>/dev/null |head -n 1|awk -F ' COMMIT ' '{print $2}'|awk -F ';' '{print $1}') + for i in $(ls -Ft $PGDATA/pg_wal/ | grep '[[:xdigit:]]$\|.partial$'); do LOG_STOP_TIME=$(pg_waldump $i --rmgr=Transaction 2>/dev/null|tail -n 1); [[ "$LOG_STOP_TIME" != "" ]] && break; done + LOG_STOP_TIME=$(echo $LOG_STOP_TIME |awk -F ' COMMIT ' '{print $2}'|awk -F ';' '{print $1}') + if [ "${LOG_START_TIME}" == "" ]; then LOG_START_TIME=${LOG_STOP_TIME}; fi + LOG_START_TIME=$(date -d "$LOG_START_TIME" -u '+%Y-%m-%dT%H:%M:%SZ') + LOG_STOP_TIME=$(date -d "$LOG_STOP_TIME" -u '+%Y-%m-%dT%H:%M:%SZ') + printf "{\"startTime\": \"$LOG_START_TIME\" ,\"stopTime\": \"$LOG_STOP_TIME\"}" + fi + filesize-collector.sh: | + #!/bin/bash + set -e; + function getProperty() { + file=$1; key=$2; + echo $(grep "${key}: " ${file} | awk -F ': ' '{print $2}') + } + filename=$1 + fileinfo=${PGDATA}/${filename} + if [ -f ${fileinfo} ]; then + TOTAL_SIZE=$(getProperty ${fileinfo} "TOTAL SIZE") + rm -f ${fileinfo} + printf "{\"totalSize\":\"${TOTAL_SIZE}\",\"manifests\":{\"backupTool\":{\"uploadTotalSize\":\"${TOTAL_SIZE}\"}}}" + else + printf "{}" + fi \ No newline at end of file diff --git a/deploy/postgresql/values.yaml b/deploy/postgresql/values.yaml index 33cf5582a..851843180 100644 --- a/deploy/postgresql/values.yaml +++ b/deploy/postgresql/values.yaml @@ -11,7 +11,7 @@ image: registry: registry.cn-hangzhou.aliyuncs.com repository: apecloud/spilo - tag: 15.2.0 + tag: 14.7.2 digest: "" ## Specify a imagePullPolicy ## Defaults to 'Always' if image tag is 'latest', else set to 'IfNotPresent' @@ -109,316 +109,14 @@ metrics: ## pullSecrets: [ ] - ## @param metrics.customMetrics Define additional custom metrics - ## ref: https://github.com/wrouesnel/postgres_exporter#adding-new-metrics-via-a-config-file - ## customMetrics: - ## pg_database: - ## query: "SELECT d.datname AS name, CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT') THEN pg_catalog.pg_database_size(d.datname) ELSE 0 END AS size_bytes FROM pg_catalog.pg_database d where datname not in ('template0', 'template1', 'postgres')" - ## metrics: - ## - name: - ## usage: "LABEL" - ## description: "Name of the database" - ## - size_bytes: - ## usage: "GAUGE" - ## description: "Size of the database in bytes" - ## - customMetrics: - pg_postmaster: - query: "SELECT pg_postmaster_start_time as start_time_seconds from pg_postmaster_start_time()" - master: true - metrics: - - start_time_seconds: - usage: "GAUGE" - description: "Time at which postmaster started" - - pg_stat_user_tables: - query: | - SELECT - current_database() datname, - schemaname, - relname, - seq_scan, - seq_tup_read, - idx_scan, - idx_tup_fetch, - n_tup_ins, - n_tup_upd, - n_tup_del, - n_tup_hot_upd, - n_live_tup, - n_dead_tup, - n_mod_since_analyze, - n_ins_since_vacuum, - COALESCE(last_vacuum, '1970-01-01Z') as last_vacuum, - COALESCE(last_autovacuum, '1970-01-01Z') as last_autovacuum, - COALESCE(last_analyze, '1970-01-01Z') as last_analyze, - COALESCE(last_autoanalyze, '1970-01-01Z') as last_autoanalyze, - vacuum_count, - autovacuum_count, - analyze_count, - autoanalyze_count - FROM - pg_stat_user_tables - metrics: - - datname: - usage: "LABEL" - description: "Name of current database" - - schemaname: - usage: "LABEL" - description: "Name of the schema that this table is in" - - relname: - usage: "LABEL" - description: "Name of this table" - - seq_scan: - usage: "COUNTER" - description: "Number of sequential scans initiated on this table" - - seq_tup_read: - usage: "COUNTER" - description: "Number of live rows fetched by sequential scans" - - idx_scan: - usage: "COUNTER" - description: "Number of index scans initiated on this table" - - idx_tup_fetch: - usage: "COUNTER" - description: "Number of live rows fetched by index scans" - - n_tup_ins: - usage: "COUNTER" - description: "Number of rows inserted" - - n_tup_upd: - usage: "COUNTER" - description: "Number of rows updated" - - n_tup_del: - usage: "COUNTER" - description: "Number of rows deleted" - - n_tup_hot_upd: - usage: "COUNTER" - description: "Number of rows HOT updated (i.e., with no separate index update required)" - - n_live_tup: - usage: "GAUGE" - description: "Estimated number of live rows" - - n_dead_tup: - usage: "GAUGE" - description: "Estimated number of dead rows" - - n_mod_since_analyze: - usage: "GAUGE" - description: "Estimated number of rows changed since last analyze" - - n_ins_since_vacuum: - usage: "GAUGE" - description: "Estimated number of rows inserted since this table was last vacuumed" - - last_vacuum: - usage: "GAUGE" - description: "Last time at which this table was manually vacuumed (not counting VACUUM FULL)" - - last_autovacuum: - usage: "GAUGE" - description: "Last time at which this table was vacuumed by the autovacuum daemon" - - last_analyze: - usage: "GAUGE" - description: "Last time at which this table was manually analyzed" - - last_autoanalyze: - usage: "GAUGE" - description: "Last time at which this table was analyzed by the autovacuum daemon" - - vacuum_count: - usage: "COUNTER" - description: "Number of times this table has been manually vacuumed (not counting VACUUM FULL)" - - autovacuum_count: - usage: "COUNTER" - description: "Number of times this table has been vacuumed by the autovacuum daemon" - - analyze_count: - usage: "COUNTER" - description: "Number of times this table has been manually analyzed" - - autoanalyze_count: - usage: "COUNTER" - description: "Number of times this table has been analyzed by the autovacuum daemon" - - pg_statio_user_tables: - query: | - SELECT - current_database() datname, - schemaname, - relname, - heap_blks_read, - heap_blks_hit, - idx_blks_read, - idx_blks_hit, - toast_blks_read, - toast_blks_hit, - tidx_blks_read, - tidx_blks_hit - FROM - pg_statio_user_tables - metrics: - - datname: - usage: "LABEL" - description: "Name of current database" - - schemaname: - usage: "LABEL" - description: "Name of the schema that this table is in" - - relname: - usage: "LABEL" - description: "Name of this table" - - heap_blks_read: - usage: "COUNTER" - description: "Number of disk blocks read from this table" - - heap_blks_hit: - usage: "COUNTER" - description: "Number of buffer hits in this table" - - idx_blks_read: - usage: "COUNTER" - description: "Number of disk blocks read from all indexes on this table" - - idx_blks_hit: - usage: "COUNTER" - description: "Number of buffer hits in all indexes on this table" - - toast_blks_read: - usage: "COUNTER" - description: "Number of disk blocks read from this table's TOAST table (if any)" - - toast_blks_hit: - usage: "COUNTER" - description: "Number of buffer hits in this table's TOAST table (if any)" - - tidx_blks_read: - usage: "COUNTER" - description: "Number of disk blocks read from this table's TOAST table indexes (if any)" - - tidx_blks_hit: - usage: "COUNTER" - description: "Number of buffer hits in this table's TOAST table indexes (if any)" +## @section pgbouncer Parameters +pgbouncer: + image: + # refer: https://hub.docker.com/r/bitnami/pgbouncer + registry: hub.docker.com + repository: bitnami/pgbouncer + tag: 1.19.0 + pullPolicy: IfNotPresent - # WARNING: This set of metrics can be very expensive on a busy server as every unique query executed will create an additional time series - pg_stat_statements: - query: | - SELECT - t2.rolname, - t3.datname, - queryid, - plans, - total_plan_time / 1000 as total_plan_time_seconds, - min_plan_time / 1000 as min_plan_time_seconds, - max_plan_time / 1000 as max_plan_time_seconds, - mean_plan_time / 1000 as mean_plan_time_seconds, - stddev_plan_time / 1000 as stddev_plan_time_seconds, - calls, - total_exec_time / 1000 as total_exec_time_seconds, - min_exec_time / 1000 as min_exec_time_seconds, - max_exec_time / 1000 as max_exec_time_seconds, - mean_exec_time / 1000 as mean_exec_time_seconds, - stddev_exec_time / 1000 as stddev_exec_time_seconds, - rows, - shared_blks_hit, - shared_blks_read, - shared_blks_dirtied, - shared_blks_written, - local_blks_hit, - local_blks_read, - local_blks_dirtied, - local_blks_written, - temp_blks_read, - temp_blks_written, - blk_read_time / 1000 as blk_read_time_seconds, - blk_write_time / 1000 as blk_write_time_seconds, - wal_records, - wal_fpi, - wal_bytes - FROM - pg_stat_statements t1 - JOIN - pg_roles t2 - ON (t1.userid=t2.oid) - JOIN - pg_database t3 - ON (t1.dbid=t3.oid) - WHERE t2.rolname != 'rdsadmin' - master: true - metrics: - - rolname: - usage: "LABEL" - description: "Name of user" - - datname: - usage: "LABEL" - description: "Name of database" - - queryid: - usage: "LABEL" - description: "Query ID" - - plans: - usage: "COUNTER" - description: "Number of times the statement was planned" - - total_plan_time_seconds: - usage: "COUNTER" - description: "Total time spent planning the statement" - - min_plan_time_seconds: - usage: "GAUGE" - description: "Minimum time spent planning the statement" - - max_plan_time_seconds: - usage: "GAUGE" - description: "Maximum time spent planning the statement" - - mean_plan_time_seconds: - usage: "GAUGE" - description: "Mean time spent planning the statement" - - stddev_plan_time_seconds: - usage: "GAUGE" - description: "Population standard deviation of time spent planning the statement" - - calls: - usage: "COUNTER" - description: "Number of times executed" - - total_exec_time_seconds: - usage: "COUNTER" - description: "Total time spent in the statement" - - min_exec_time_seconds: - usage: "GAUGE" - description: "Minimum time spent in the statement" - - max_exec_time_seconds: - usage: "GAUGE" - description: "Maximum time spent in the statement" - - mean_exec_time_seconds: - usage: "GAUGE" - description: "Mean time spent in the statement" - - stddev_exec_time_seconds: - usage: "GAUGE" - description: "Population standard deviation of time spent in the statement" - - rows: - usage: "COUNTER" - description: "Total number of rows retrieved or affected by the statement" - - shared_blks_hit: - usage: "COUNTER" - description: "Total number of shared block cache hits by the statement" - - shared_blks_read: - usage: "COUNTER" - description: "Total number of shared blocks read by the statement" - - shared_blks_dirtied: - usage: "COUNTER" - description: "Total number of shared blocks dirtied by the statement" - - shared_blks_written: - usage: "COUNTER" - description: "Total number of shared blocks written by the statement" - - local_blks_hit: - usage: "COUNTER" - description: "Total number of local block cache hits by the statement" - - local_blks_read: - usage: "COUNTER" - description: "Total number of local blocks read by the statement" - - local_blks_dirtied: - usage: "COUNTER" - description: "Total number of local blocks dirtied by the statement" - - local_blks_written: - usage: "COUNTER" - description: "Total number of local blocks written by the statement" - - temp_blks_read: - usage: "COUNTER" - description: "Total number of temp blocks read by the statement" - - temp_blks_written: - usage: "COUNTER" - description: "Total number of temp blocks written by the statement" - - blk_read_time_seconds: - usage: "COUNTER" - description: "Total time the statement spent reading blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" - - blk_write_time_seconds: - usage: "COUNTER" - description: "Total time the statement spent writing blocks, in milliseconds (if track_io_timing is enabled, otherwise zero)" - - wal_records: - usage: "COUNTER" - description: "Total number of WAL records generated by the statement" - - wal_fpi: - usage: "COUNTER" - description: "Total number of WAL full page images generated by the statement" - - wal_bytes: - usage: "COUNTER" - description: "Total amount of WAL generated by the statement in bytes" logConfigs: - running: /postgresql/data/log/postgresql-* \ No newline at end of file + running: /home/postgres/pgdata/pgroot/data/log/postgresql-* \ No newline at end of file diff --git a/deploy/qdrant-cluster/Chart.yaml b/deploy/qdrant-cluster/Chart.yaml index 84fbf4dbd..67fa57059 100644 --- a/deploy/qdrant-cluster/Chart.yaml +++ b/deploy/qdrant-cluster/Chart.yaml @@ -4,6 +4,6 @@ description: A Qdrant cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: "1.1.0" diff --git a/deploy/qdrant-cluster/templates/_helpers.tpl b/deploy/qdrant-cluster/templates/_helpers.tpl index 2ae432f3e..9b230fcdd 100644 --- a/deploy/qdrant-cluster/templates/_helpers.tpl +++ b/deploy/qdrant-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "qdrant.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "qdrant.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "qdrant.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "qdrant.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/qdrant-cluster/templates/cluster.yaml b/deploy/qdrant-cluster/templates/cluster.yaml index e088659e1..e67460b81 100644 --- a/deploy/qdrant-cluster/templates/cluster.yaml +++ b/deploy/qdrant-cluster/templates/cluster.yaml @@ -1,10 +1,10 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "qdrant.labels" . | nindent 4 }} spec: - clusterDefinitionRef: qdrant-standalone # ref clusterdefinition.name + clusterDefinitionRef: qdrant # ref clusterdefinition.name clusterVersionRef: qdrant-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} # ref clusterversion.name terminationPolicy: {{ .Values.terminationPolicy }} affinity: diff --git a/deploy/qdrant-cluster/values.yaml b/deploy/qdrant-cluster/values.yaml index 3f14e3c39..cad11f117 100644 --- a/deploy/qdrant-cluster/values.yaml +++ b/deploy/qdrant-cluster/values.yaml @@ -6,6 +6,8 @@ replicaCount: 1 terminationPolicy: Delete clusterVersionOverride: "" +nameOverride: "" +fullnameOverride: "" monitor: enabled: false @@ -27,3 +29,6 @@ persistence: data: storageClassName: size: 10Gi + +serviceAccount: + name: \ No newline at end of file diff --git a/deploy/qdrant/Chart.yaml b/deploy/qdrant/Chart.yaml index 8898897de..08bf597f1 100644 --- a/deploy/qdrant/Chart.yaml +++ b/deploy/qdrant/Chart.yaml @@ -1,11 +1,11 @@ apiVersion: v2 -name: qdrant-standalone +name: qdrant description: . type: application # This is the chart version. -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 # This is the version number of qdrant. appVersion: "1.1.0" diff --git a/deploy/qdrant/templates/backuppolicytemplate.yaml b/deploy/qdrant/templates/backuppolicytemplate.yaml index b07273534..553f8ee16 100644 --- a/deploy/qdrant/templates/backuppolicytemplate.yaml +++ b/deploy/qdrant/templates/backuppolicytemplate.yaml @@ -1,15 +1,22 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-qdrant + name: qdrant-backup-policy-template labels: - clusterdefinition.kubeblocks.io/name: qdrant-standalone + clusterdefinition.kubeblocks.io/name: qdrant {{- include "qdrant.labels" . | nindent 4 }} spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s - - credentialKeyword: - userKeyword: username - passwordKeyword: password + clusterDefinitionRef: qdrant + backupPolicies: + - componentDefRef: qdrant + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * 0" + snapshot: + target: + connectionCredentialKey: + passwordKey: password + usernameKey: username \ No newline at end of file diff --git a/deploy/qdrant/templates/clusterdefinition.yaml b/deploy/qdrant/templates/clusterdefinition.yaml index e3f9533ab..996d7ead2 100644 --- a/deploy/qdrant/templates/clusterdefinition.yaml +++ b/deploy/qdrant/templates/clusterdefinition.yaml @@ -2,7 +2,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: ClusterDefinition metadata: - name: qdrant-standalone + name: qdrant labels: {{- include "qdrant.labels" . | nindent 4 }} spec: @@ -25,8 +25,8 @@ spec: scrapePort: 9187 logConfigs: configSpecs: - - name: qdrant-standalone-config-template - templateRef: qdrant-standalone-config-template + - name: qdrant-config-template + templateRef: qdrant-config-template volumeName: qdrant-config namespace: {{ .Release.Namespace }} service: diff --git a/deploy/qdrant/templates/clusterversion.yaml b/deploy/qdrant/templates/clusterversion.yaml index c57d1e9bc..6b19fba16 100644 --- a/deploy/qdrant/templates/clusterversion.yaml +++ b/deploy/qdrant/templates/clusterversion.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "qdrant.labels" . | nindent 4 }} spec: - clusterDefinitionRef: qdrant-standalone + clusterDefinitionRef: qdrant componentVersions: - componentDefRef: qdrant versionsContext: diff --git a/deploy/qdrant/templates/configmap.yaml b/deploy/qdrant/templates/configmap.yaml index 913fdeef2..0e5ae146b 100644 --- a/deploy/qdrant/templates/configmap.yaml +++ b/deploy/qdrant/templates/configmap.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: qdrant-standalone-config-template + name: qdrant-config-template namespace: {{ .Release.Namespace | quote }} labels: {{- include "qdrant.labels" . | nindent 4 }} diff --git a/deploy/redis-cluster/Chart.yaml b/deploy/redis-cluster/Chart.yaml index 55783b46c..ce8d5498f 100644 --- a/deploy/redis-cluster/Chart.yaml +++ b/deploy/redis-cluster/Chart.yaml @@ -4,7 +4,7 @@ description: An Redis Replication Cluster Helm chart for KubeBlocks. type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: "7.0.6" diff --git a/deploy/redis-cluster/templates/_helpers.tpl b/deploy/redis-cluster/templates/_helpers.tpl index 637303914..a5ec51aca 100644 --- a/deploy/redis-cluster/templates/_helpers.tpl +++ b/deploy/redis-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "redis-cluster.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "redis-cluster.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "redis-cluster.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "redis-cluster.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/redis-cluster/templates/cluster.yaml b/deploy/redis-cluster/templates/cluster.yaml index d24ee37d9..9316aec75 100644 --- a/deploy/redis-cluster/templates/cluster.yaml +++ b/deploy/redis-cluster/templates/cluster.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "redis-cluster.labels" . | nindent 4 }} spec: clusterDefinitionRef: redis # ref clusterDefinition.name @@ -20,6 +20,7 @@ spec: monitor: {{ .Values.monitor.enabled | default false }} enabledLogs: {{ .Values.enabledLogs | toJson | indent 4 }} replicas: {{ .Values.replicaCount | default 2 }} + serviceAccountName: {{ include "redis-cluster.serviceAccountName" . }} primaryIndex: {{ .Values.primaryIndex | default 0 }} switchPolicy: type: {{ .Values.switchPolicy.type}} diff --git a/deploy/redis-cluster/templates/role.yaml b/deploy/redis-cluster/templates/role.yaml new file mode 100644 index 000000000..dc29dac96 --- /dev/null +++ b/deploy/redis-cluster/templates/role.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-{{ include "clustername" . }} + namespace: {{ .Release.Namespace }} + labels: + {{ include "redis-cluster.labels" . | nindent 4 }} +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create diff --git a/deploy/redis-cluster/templates/rolebinding.yaml b/deploy/redis-cluster/templates/rolebinding.yaml new file mode 100644 index 000000000..ac70b5b2d --- /dev/null +++ b/deploy/redis-cluster/templates/rolebinding.yaml @@ -0,0 +1,14 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-{{ include "clustername" . }} + labels: + {{ include "redis-cluster.labels" . | nindent 4 }} +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-{{ include "clustername" . }} +subjects: + - kind: ServiceAccount + name: {{ include "redis-cluster.serviceAccountName" . }} + namespace: {{ .Release.Namespace }} diff --git a/deploy/redis-cluster/templates/serviceaccount.yaml b/deploy/redis-cluster/templates/serviceaccount.yaml new file mode 100644 index 000000000..c76c9743e --- /dev/null +++ b/deploy/redis-cluster/templates/serviceaccount.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include "redis-cluster.serviceAccountName" . }} + labels: + {{ include "redis-cluster.labels" . | nindent 4 }} diff --git a/deploy/redis-cluster/values.yaml b/deploy/redis-cluster/values.yaml index 6cf767984..b7c6b8d6f 100644 --- a/deploy/redis-cluster/values.yaml +++ b/deploy/redis-cluster/values.yaml @@ -2,6 +2,9 @@ # This is a YAML-formatted file. # Declare variables to be passed into your templates. +nameOverride: "" +fullnameOverride: "" + replicaCount: 2 sentinelReplicaCount: 3 @@ -16,20 +19,19 @@ monitor: primaryIndex: 0 switchPolicy: - type: MaximumAvailability + type: Noop -resources: { } +resources: # We usually recommend not to specify default resources and to leave this as a conscious # choice for the user. This also increases chances charts run on environments with little # resources, such as Minikube. If you do want to specify resources, uncomment the following # lines, adjust them as necessary, and remove the curly braces after 'resources:'. - - # limits: - # cpu: 500m - # memory: 2Gi - # requests: - # cpu: 100m - # memory: 1Gi + limits: + cpu: 500m + memory: 3Gi + requests: + cpu: 500m + memory: 1Gi persistence: enabled: true @@ -47,3 +49,7 @@ tolerations: [ ] enabledLogs: - running + +# The RABC permission used by cluster component pod, now include event.create +serviceAccount: + name: "" diff --git a/deploy/redis/Chart.yaml b/deploy/redis/Chart.yaml index 5950941a2..3d5f3d19e 100644 --- a/deploy/redis/Chart.yaml +++ b/deploy/redis/Chart.yaml @@ -4,7 +4,7 @@ description: A Redis cluster definition Helm chart for Kubernetes type: application -version: 0.5.0-alpha.3 +version: 0.5.1-beta.0 appVersion: "7.0.6" diff --git a/deploy/redis/config/redis7-config-constraint.cue b/deploy/redis/config/redis7-config-constraint.cue index 46974f0f1..57b77d67e 100644 --- a/deploy/redis/config/redis7-config-constraint.cue +++ b/deploy/redis/config/redis7-config-constraint.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #RedisParameter: { @@ -148,3 +151,6 @@ ... } + +configuration: #RedisParameter & { +} diff --git a/deploy/redis/config/redis7-config.tpl b/deploy/redis/config/redis7-config.tpl index 4f86d27fa..2000cd63d 100644 --- a/deploy/redis/config/redis7-config.tpl +++ b/deploy/redis/config/redis7-config.tpl @@ -5,8 +5,10 @@ timeout 0 tcp-keepalive 300 daemonize no pidfile /var/run/redis_6379.pid +{{ block "logsBlock" . }} loglevel notice logfile "/data/running.log" +{{ end }} databases 16 always-show-logo no set-proc-title yes @@ -16,7 +18,7 @@ rdbcompression yes rdbchecksum yes dbfilename dump.rdb rdb-del-sync-files no -dir ./ +dir /data replica-serve-stale-data yes replica-read-only yes repl-diskless-sync yes @@ -70,4 +72,10 @@ rdb-save-incremental-fsync yes jemalloc-bg-thread yes enable-debug-command yes protected-mode no +aclfile /etc/redis/users.acl +# maxmemory +{{- $request_memory := getContainerRequestMemory ( index $.podSpec.containers 0 ) }} +{{- if gt $request_memory 0 }} +maxmemory {{ $request_memory }} +{{- end -}} \ No newline at end of file diff --git a/deploy/redis/scripts/redis-sentinel-setup.sh.tpl b/deploy/redis/scripts/redis-sentinel-setup.sh.tpl index 440d5cec9..e1afcef92 100644 --- a/deploy/redis/scripts/redis-sentinel-setup.sh.tpl +++ b/deploy/redis/scripts/redis-sentinel-setup.sh.tpl @@ -20,18 +20,15 @@ set -ex {{- end }} {{- end }} {{- /* build primary pod message, because currently does not support cross-component acquisition of environment variables, the service of the redis master node is assembled here through specific rules */}} -{{- $primary_pod = printf "%s-%s-0.%s-%s-headless.%s.svc" $clusterName $redis_component.name $clusterName $redis_component.name $namespace }} -{{- if ne $primary_index 0 }} - {{- $primary_pod = printf "%s-%s-%d-0.%s-%s-headless.%s.svc" $clusterName $redis_component.name $primary_index $clusterName $redis_component.name $namespace }} -{{- end }} -{{- $sentinel_monitor := printf "%s-%s %s" $clusterName $sentinel_component.name $primary_pod }} +{{- $primary_pod = printf "%s-%s-%d.%s-%s-headless.%s.svc" $clusterName $redis_component.name $primary_index $clusterName $redis_component.name $namespace }} +{{- $sentinel_monitor := printf "%s-%s %s" $clusterName $redis_component.name $primary_pod }} cat>/etc/sentinel/redis-sentinel.conf<> /etc/redis/redis.conf +echo "replica-announce-ip $KB_POD_FQDN" >> /etc/redis/redis.conf +{{- $data_root := getVolumePathByName ( index $.podSpec.containers 0 ) "data" }} +touch {{ $data_root }}/users.acl +echo "aclfile /data/users.acl" >> /etc/redis/redis.conf +exec redis-server /etc/redis/redis.conf \ +--loadmodule /opt/redis-stack/lib/redisearch.so ${REDISEARCH_ARGS} \ +--loadmodule /opt/redis-stack/lib/redisgraph.so ${REDISGRAPH_ARGS} \ +--loadmodule /opt/redis-stack/lib/redistimeseries.so ${REDISTIMESERIES_ARGS} \ +--loadmodule /opt/redis-stack/lib/rejson.so ${REDISJSON_ARGS} \ +--loadmodule /opt/redis-stack/lib/redisbloom.so ${REDISBLOOM_ARGS} \ No newline at end of file diff --git a/deploy/redis/templates/backuppolicytemplate.yaml b/deploy/redis/templates/backuppolicytemplate.yaml index f1b2b7e29..dff9af636 100644 --- a/deploy/redis/templates/backuppolicytemplate.yaml +++ b/deploy/redis/templates/backuppolicytemplate.yaml @@ -1,12 +1,22 @@ - -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-redis + name: redis-backup-policy-template labels: clusterdefinition.kubeblocks.io/name: redis {{- include "redis.labels" . | nindent 4 }} spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s + clusterDefinitionRef: redis + backupPolicies: + - componentDefRef: redis + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * 0" + snapshot: + target: + connectionCredentialKey: + passwordKey: password + usernameKey: username \ No newline at end of file diff --git a/deploy/redis/templates/clusterdefinition.yaml b/deploy/redis/templates/clusterdefinition.yaml index a9bda7e01..77927d242 100644 --- a/deploy/redis/templates/clusterdefinition.yaml +++ b/deploy/redis/templates/clusterdefinition.yaml @@ -16,6 +16,11 @@ spec: - name: redis workloadType: Replication characterType: redis + probes: + roleProbe: + failureThreshold: 2 + periodSeconds: 2 + timeoutSeconds: 1 replicationSpec: switchPolicies: - type: MaximumAvailability @@ -60,9 +65,6 @@ spec: - name: redis port: 6379 targetPort: redis - - name: metrics - port: 9121 - targetPort: metrics configSpecs: - name: redis-replication-config templateRef: redis7-config-template @@ -131,12 +133,12 @@ spec: # to pass $(KB_ACCOUNT_STATEMENT) to redis-cli without causing parsing error. # Instead, using a shell script to wrap redis-cli and pass $(KB_ACCOUNT_STATEMENT) to it will do. cmdExecutorConfig: - image: docker.io/redis:7.0.5 + image: {{ .Values.image.registry | default "docker.io" }}/{{ .Values.image.repository }}:{{ default .Values.image.tag }} command: - sh - -c args: - - "redis-cli -h $(KB_ACCOUNT_ENDPOINT) $(KB_ACCOUNT_STATEMENT)" + - "redis-cli -h $(KB_ACCOUNT_ENDPOINT) $(KB_ACCOUNT_STATEMENT) | redis-cli -h $(KB_ACCOUNT_ENDPOINT) acl save " passwordConfig: length: 10 numDigits: 5 @@ -144,35 +146,30 @@ spec: letterCase: MixedCases accounts: - name: kbadmin - provisionPolicy: + provisionPolicy: &kbadminAcctRef type: CreateByStmt - scope: AnyPods + scope: AllPods statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allcommands allkeys + creation: ACL SETUSER $(USERNAME) ON \>$(PASSWD) allcommands allkeys + update: ACL SETUSER $(USERNAME) ON \>$(PASSWD) - name: kbdataprotection - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allcommands allkeys + provisionPolicy: *kbadminAcctRef - name: kbmonitoring - provisionPolicy: + provisionPolicy: &kbReadOnlyAcctRef type: CreateByStmt - scope: AnyPods + scope: AllPods statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allkeys +get + creation: ACL SETUSER $(USERNAME) ON \>$(PASSWD) allkeys +get + update: ACL SETUSER $(USERNAME) ON \>$(PASSWD) - name: kbprobe - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allkeys +get + provisionPolicy: *kbReadOnlyAcctRef - name: kbreplicator provisionPolicy: type: CreateByStmt - scope: AnyPods + scope: AllPods statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) +psync +replconf +ping + creation: ACL SETUSER $(USERNAME) ON \>$(PASSWD) +psync +replconf +ping + update: ACL SETUSER $(USERNAME) ON \>$(PASSWD) - name: redis-sentinel workloadType: Stateful characterType: redis @@ -253,4 +250,4 @@ spec: command: - sh - -c - - /scripts/redis-sentinel-ping.sh 1 \ No newline at end of file + - /scripts/redis-sentinel-ping.sh 1 diff --git a/deploy/redis/templates/clusterversion.yaml b/deploy/redis/templates/clusterversion.yaml index 4ee5aba1d..54608aa13 100644 --- a/deploy/redis/templates/clusterversion.yaml +++ b/deploy/redis/templates/clusterversion.yaml @@ -13,6 +13,9 @@ spec: - name: redis image: {{ .Values.image.repository }}:{{ .Values.image.tag }} imagePullPolicy: {{ default .Values.image.pullPolicy "IfNotPresent" }} + systemAccountSpec: + cmdExecutorConfig: + image: {{ .Values.image.repository }}:{{ .Values.image.tag }} - componentDefRef: redis-sentinel versionsContext: initContainers: diff --git a/deploy/redis/templates/scripts.yaml b/deploy/redis/templates/scripts.yaml index 3b3e8c794..eb8427629 100644 --- a/deploy/redis/templates/scripts.yaml +++ b/deploy/redis/templates/scripts.yaml @@ -2,6 +2,8 @@ apiVersion: v1 kind: ConfigMap metadata: name: redis-scripts + labels: + {{- include "redis.labels" . | nindent 4 }} data: setup.sh: | #!/bin/sh @@ -15,16 +17,7 @@ data: redis-cli -h 127.0.0.1 -p 6379 replicaof $KB_PRIMARY_POD_NAME 6379 || exit 1 fi redis-start.sh: | - #!/bin/sh - set -ex - echo "include /etc/conf/redis.conf" >> /etc/redis/redis.conf - echo "replica-announce-ip $KB_POD_FQDN" >> /etc/redis/redis.conf - exec redis-server /etc/redis/redis.conf \ - --loadmodule /opt/redis-stack/lib/redisearch.so ${REDISEARCH_ARGS} \ - --loadmodule /opt/redis-stack/lib/redisgraph.so ${REDISGRAPH_ARGS} \ - --loadmodule /opt/redis-stack/lib/redistimeseries.so ${REDISTIMESERIES_ARGS} \ - --loadmodule /opt/redis-stack/lib/rejson.so ${REDISJSON_ARGS} \ - --loadmodule /opt/redis-stack/lib/redisbloom.so ${REDISBLOOM_ARGS} + {{- .Files.Get "scripts/redis7-start.sh.tpl" | nindent 4 }} redis-sentinel-setup.sh: |- {{- .Files.Get "scripts/redis-sentinel-setup.sh.tpl" | nindent 4 }} redis-sentinel-start.sh: |- diff --git a/deploy/weaviate-cluster/Chart.yaml b/deploy/weaviate-cluster/Chart.yaml index a9395d6dd..97b409d0d 100644 --- a/deploy/weaviate-cluster/Chart.yaml +++ b/deploy/weaviate-cluster/Chart.yaml @@ -4,6 +4,6 @@ description: A weaviate cluster Helm chart for KubeBlocks. type: application -version: 0.1.0 +version: 0.5.1-beta.0 appVersion: "1.18.0" diff --git a/deploy/weaviate-cluster/templates/_helpers.tpl b/deploy/weaviate-cluster/templates/_helpers.tpl index 82d09cdb0..783b8bbd4 100644 --- a/deploy/weaviate-cluster/templates/_helpers.tpl +++ b/deploy/weaviate-cluster/templates/_helpers.tpl @@ -50,13 +50,13 @@ app.kubernetes.io/name: {{ include "weaviate.name" . }} app.kubernetes.io/instance: {{ .Release.Name }} {{- end }} +{{- define "clustername" -}} +{{ include "weaviate.fullname" .}} +{{- end}} + {{/* Create the name of the service account to use */}} {{- define "weaviate.serviceAccountName" -}} -{{- if .Values.serviceAccount.create }} -{{- default (include "weaviate.fullname" .) .Values.serviceAccount.name }} -{{- else }} -{{- default "default" .Values.serviceAccount.name }} -{{- end }} +{{- default (printf "kb-%s" (include "clustername" .)) .Values.serviceAccount.name }} {{- end }} diff --git a/deploy/weaviate-cluster/templates/cluster.yaml b/deploy/weaviate-cluster/templates/cluster.yaml index 0bf4a0804..8432a6543 100644 --- a/deploy/weaviate-cluster/templates/cluster.yaml +++ b/deploy/weaviate-cluster/templates/cluster.yaml @@ -1,10 +1,10 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: {{ .Release.Name }} + name: {{ include "clustername" . }} labels: {{ include "weaviate.labels" . | nindent 4 }} spec: - clusterDefinitionRef: weaviate-standalone # ref clusterdefinition.name + clusterDefinitionRef: weaviate # ref clusterdefinition.name clusterVersionRef: weaviate-{{ default .Chart.AppVersion .Values.clusterVersionOverride }} # ref clusterversion.name terminationPolicy: {{ .Values.terminationPolicy }} affinity: diff --git a/deploy/weaviate-cluster/values.yaml b/deploy/weaviate-cluster/values.yaml index 3f14e3c39..adb074952 100644 --- a/deploy/weaviate-cluster/values.yaml +++ b/deploy/weaviate-cluster/values.yaml @@ -6,6 +6,8 @@ replicaCount: 1 terminationPolicy: Delete clusterVersionOverride: "" +nameOverride: "" +fullnameOverride: "" monitor: enabled: false @@ -27,3 +29,7 @@ persistence: data: storageClassName: size: 10Gi + + +serviceAccount: + name: \ No newline at end of file diff --git a/deploy/weaviate/Chart.yaml b/deploy/weaviate/Chart.yaml index c198dced1..7f8b32fa3 100644 --- a/deploy/weaviate/Chart.yaml +++ b/deploy/weaviate/Chart.yaml @@ -1,11 +1,11 @@ apiVersion: v2 -name: weaviate-standalone -description: . +name: weaviate +description: Weaviate is an open-source vector database. It allows you to store data objects and vector embeddings from your favorite ML-models, and scale seamlessly into billions of data objects. type: application # This is the chart version. -version: 0.1.0 +version: 0.5.1-beta.0 # This is the version number of weaviate. appVersion: "1.18.0" diff --git a/deploy/weaviate/config/weaviate-env-constraint.cue b/deploy/weaviate/config/weaviate-env-constraint.cue new file mode 100644 index 000000000..4f415e93d --- /dev/null +++ b/deploy/weaviate/config/weaviate-env-constraint.cue @@ -0,0 +1,80 @@ +//Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +//This file is part of KubeBlocks project +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . + +#WeaviateEnvs: { + + // Which modules to enable in the setup? + ENABLE_MODULES?: string + + // The endpoint where to reach the transformers module if enabled + TRANSFORMERS_INFERENCE_API?: string + + // The endpoint where to reach the clip module if enabled + CLIP_INFERENCE_API?: string + + // The endpoint where to reach the img2vec-neural module if enabled + IMAGE_INFERENCE_API?: string + + // The id of the AWS access key for the desired account. + AWS_ACCESS_KEY_ID?: string + + // The secret AWS access key for the desired account. + AWS_SECRET_ACCESS_KEY?: string + + // The path to the secret GCP service account or workload identity file. + GOOGLE_APPLICATION_CREDENTIALS?: string + + // The name of your Azure Storage account. + AZURE_STORAGE_ACCOUNT?: string + + // An access key for your Azure Storage account. + AZURE_STORAGE_KEY?: string + + // A string that includes the authorization information required. + AZURE_STORAGE_CONNECTION_STRING?: string + + QNA_INFERENCE_API?: string + + SPELLCHECK_INFERENCE_API?: string + + NER_INFERENCE_API?: string + + SUM_INFERENCE_API?: string + + OPENAI_APIKEY?: string + + HUGGINGFACE_APIKEY?: string + + COHERE_APIKEY?: string + + PALM_APIKEY?: string + + // Enables API key authentication. + AUTHENTICATION_APIKEY_ENABLED?: string + + // List one or more keys, separated by commas. Each key corresponds to a specific user identity below. + AUTHENTICATION_APIKEY_ALLOWED_KEYS?: string + + // List one or more user identities, separated by commas. Each identity corresponds to a specific key above. + AUTHENTICATION_APIKEY_USERS?: string + + AUTHORIZATION_ADMINLIST_ENABLED?: string + + AUTHORIZATION_ADMINLIST_USERS?: string + + AUTHORIZATION_ADMINLIST_READONLY_USERS?: string +} diff --git a/deploy/weaviate/templates/backuppolicytemplate.yaml b/deploy/weaviate/templates/backuppolicytemplate.yaml index 778b6181d..579493cea 100644 --- a/deploy/weaviate/templates/backuppolicytemplate.yaml +++ b/deploy/weaviate/templates/backuppolicytemplate.yaml @@ -1,15 +1,22 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 +apiVersion: apps.kubeblocks.io/v1alpha1 kind: BackupPolicyTemplate metadata: - name: backup-policy-template-weaviate + name: weaviate-backup-policy-template labels: - clusterdefinition.kubeblocks.io/name: weaviate-standalone + clusterdefinition.kubeblocks.io/name: weaviate {{- include "weaviate.labels" . | nindent 4 }} spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s - - credentialKeyword: - userKeyword: username - passwordKeyword: password + clusterDefinitionRef: weaviate + backupPolicies: + - componentDefRef: weaviate + retention: + ttl: 7d + schedule: + snapshot: + enable: false + cronExpression: "0 18 * * 0" + snapshot: + target: + connectionCredentialKey: + passwordKey: password + usernameKey: username \ No newline at end of file diff --git a/deploy/weaviate/templates/clusterdefinition.yaml b/deploy/weaviate/templates/clusterdefinition.yaml index 500dd3f66..ac2a16bcd 100644 --- a/deploy/weaviate/templates/clusterdefinition.yaml +++ b/deploy/weaviate/templates/clusterdefinition.yaml @@ -2,7 +2,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: ClusterDefinition metadata: - name: weaviate-standalone + name: weaviate labels: {{- include "weaviate.labels" . | nindent 4 }} spec: @@ -22,13 +22,18 @@ spec: builtIn: false exporterConfig: scrapePath: /metrics - scrapePort: 9187 + scrapePort: 2112 logConfigs: configSpecs: - - name: weaviate-standalone-config-template - templateRef: weaviate-standalone-config-template + - name: weaviate-config-template + templateRef: weaviate-config-template volumeName: weaviate-config namespace: {{ .Release.Namespace }} + - name: weaviate-env-template + templateRef: weaviate-env-template + constraintRef: weaviate-env-constraints + volumeName: weaviate-env + namespace: {{ .Release.Namespace }} service: ports: - name: tcp-weaviate @@ -44,18 +49,11 @@ spec: - name: weaviate imagePullPolicy: {{default .Values.images.pullPolicy "IfNotPresent"}} command: - - /bin/weaviate - args: - - --host - - 0.0.0.0 - - --port - - "8080" - - --scheme - - http - - --config-file - - /weaviate-config/conf.yaml - - --read-timeout=60s - - --write-timeout=60s + - /bin/sh + - -c + - | + export $(cat /weaviate-env/envs | xargs) + /bin/weaviate --host 0.0.0.0 --port "8080" --scheme http --config-file /weaviate-config/conf.yaml --read-timeout=60s --write-timeout=60s securityContext: runAsUser: 0 livenessProbe: @@ -92,6 +90,8 @@ spec: volumeMounts: - mountPath: /weaviate-config name: weaviate-config + - mountPath: /weaviate-env + name: weaviate-env - mountPath: /var/lib/weaviate name: data dnsPolicy: ClusterFirst @@ -100,7 +100,11 @@ spec: - name: tcp-weaviate containerPort: 8080 - name: tcp-metrics - containerPort: 9091 + containerPort: 2112 + - name: tcp-gossip-bind + containerPort: 7000 + - name: tcp-data-bind + containerPort: 7001 env: - name: CLUSTER_DATA_BIND_PORT value: "7001" @@ -109,20 +113,20 @@ spec: - name: GOGC value: "100" - name: PROMETHEUS_MONITORING_ENABLED - value: "false" + value: "true" + - name: PROMETHEUS_MONITORING_PORT + value: "2112" - name: QUERY_MAXIMUM_RESULTS value: "100000" - name: REINDEX_VECTOR_DIMENSIONS_AT_STARTUP value: "false" - name: TRACK_VECTOR_DIMENSIONS value: "false" - - name: STANDALONE_MODE - value: 'true' - name: PERSISTENCE_DATA_PATH value: '/var/lib/weaviate' - name: DEFAULT_VECTORIZER_MODULE value: none - - name: CLUSTER_GOSSIP_BIND_PORT - value: "7000" - - name: CLUSTER_DATA_BIND_PORT - value: "7001" + - name: CLUSTER_HOSTNAME + value: "$(KB_POD_NAME)" + - name: CLUSTER_JOIN + value: "$(KB_CLUSTER_COMP_NAME)-headless.$(KB_NAMESPACE).svc.cluster.local" diff --git a/deploy/weaviate/templates/clusterversion.yaml b/deploy/weaviate/templates/clusterversion.yaml index 1b3a69b78..a74d2d4d9 100644 --- a/deploy/weaviate/templates/clusterversion.yaml +++ b/deploy/weaviate/templates/clusterversion.yaml @@ -5,7 +5,7 @@ metadata: labels: {{- include "weaviate.labels" . | nindent 4 }} spec: - clusterDefinitionRef: weaviate-standalone + clusterDefinitionRef: weaviate componentVersions: - componentDefRef: weaviate versionsContext: diff --git a/deploy/weaviate/templates/configconstraint.yaml b/deploy/weaviate/templates/configconstraint.yaml new file mode 100644 index 000000000..5abc3f863 --- /dev/null +++ b/deploy/weaviate/templates/configconstraint.yaml @@ -0,0 +1,44 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ConfigConstraint +metadata: + name: weaviate-env-constraints + labels: + {{- include "weaviate.labels" . | nindent 4 }} +spec: + # top level weaviate configuration type + cfgSchemaTopLevelName: WeaviateEnvs + + # ConfigurationSchema that impose restrictions on engine parameter's rule + configurationSchema: + cue: |- + {{- .Files.Get "config/weaviate-env-constraint.cue" | nindent 6 }} + + ## define static parameter list + staticParameters: + - ENABLE_MODULES + - TRANSFORMERS_INFERENCE_API + - CLIP_INFERENCE_API + - QNA_INFERENCE_API + - IMAGE_INFERENCE_API + - SPELLCHECK_INFERENCE_API + - NER_INFERENCE_API + - SUM_INFERENCE_API + - OPENAI_APIKEY + - HUGGINGFACE_APIKEY + - COHERE_APIKEY + - PALM_APIKEY + - AWS_ACCESS_KEY_ID + - AWS_SECRET_ACCESS_KEY + - GOOGLE_APPLICATION_CREDENTIALS + - AZURE_STORAGE_ACCOUNT + - AZURE_STORAGE_KEY + - AZURE_STORAGE_CONNECTION_STRING + - AUTHENTICATION_APIKEY_ENABLED + - AUTHENTICATION_APIKEY_ALLOWED_KEYS + - AUTHENTICATION_APIKEY_USERS + - AUTHORIZATION_ADMINLIST_ENABLED + - AUTHORIZATION_ADMINLIST_USERS + - AUTHORIZATION_ADMINLIST_READONLY_USERS + + formatterConfig: + format: dotenv \ No newline at end of file diff --git a/deploy/weaviate/templates/configmap.yaml b/deploy/weaviate/templates/configmap.yaml index 69e621073..d934a918d 100644 --- a/deploy/weaviate/templates/configmap.yaml +++ b/deploy/weaviate/templates/configmap.yaml @@ -1,7 +1,7 @@ apiVersion: v1 kind: ConfigMap metadata: - name: weaviate-standalone-config-template + name: weaviate-config-template namespace: {{ .Release.Namespace | quote }} labels: {{- include "weaviate.labels" . | nindent 4 }} @@ -17,4 +17,40 @@ data: enabled: false query_defaults: limit: 100 - debug: false \ No newline at end of file + debug: false + +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: weaviate-env-template + namespace: {{ .Release.Namespace | quote }} + labels: + {{- include "weaviate.labels" . | nindent 4 }} + +data: + envs: |- + ENABLE_MODULES="" + TRANSFORMERS_INFERENCE_API="" + CLIP_INFERENCE_API="" + QNA_INFERENCE_API="" + IMAGE_INFERENCE_API="" + SPELLCHECK_INFERENCE_API="" + NER_INFERENCE_API="" + SUM_INFERENCE_API="" + OPENAI_APIKEY="" + HUGGINGFACE_APIKEY="" + COHERE_APIKEY="" + PALM_APIKEY="" + AWS_ACCESS_KEY_ID="" + AWS_SECRET_ACCESS_KEY="" + GOOGLE_APPLICATION_CREDENTIALS="" + AZURE_STORAGE_ACCOUNT="" + AZURE_STORAGE_KEY="" + AZURE_STORAGE_CONNECTION_STRING="" + AUTHENTICATION_APIKEY_ENABLED="" + AUTHENTICATION_APIKEY_ALLOWED_KEYS="" + AUTHENTICATION_APIKEY_USERS="" + AUTHORIZATION_ADMINLIST_ENABLED="" + AUTHORIZATION_ADMINLIST_USERS="" + AUTHORIZATION_ADMINLIST_READONLY_USERS="" \ No newline at end of file diff --git a/deploy/weaviate/values.yaml b/deploy/weaviate/values.yaml index b69d52ff0..f4d6126a3 100644 --- a/deploy/weaviate/values.yaml +++ b/deploy/weaviate/values.yaml @@ -18,7 +18,7 @@ images: pullPolicy: IfNotPresent weaviate: repository: docker.io/semitechnologies/weaviate - tag: 1.18.0 + tag: 1.19.6 ## @param debugEnabled enables containers' debug logging ## @@ -61,7 +61,7 @@ resources: {} # memory: '1Gi' -# Add a service account ot the Weaviate pods if you need Weaviate to have permissions to +# Add a service account to the Weaviate pods if you need Weaviate to have permissions to # access kubernetes resources or cloud provider resources. For example for it to have # access to a backup up bucket, or if you want to restrict Weaviate pod in any way. # By default, use the default ServiceAccount diff --git a/docker/Dockerfile b/docker/Dockerfile index 23d12f5b3..4969f0632 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -1,7 +1,13 @@ # Build the manager binary -FROM --platform=${BUILDPLATFORM} golang:1.19 as builder +ARG DIST_IMG=gcr.io/distroless/static:nonroot +# use following dist image arg if you have problem access gcr.io registry, i.e., China region. +#ARG DIST_IMG=katanomi/distroless-static:nonroot -## docker buildx buid injected build-args: +ARG GO_VERSION=1.20 + +FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} as builder + +## docker buildx build injected build-args: #BUILDPLATFORM — matches the current machine. (e.g. linux/amd64) #BUILDOS — os component of BUILDPLATFORM, e.g. linux #BUILDARCH — e.g. amd64, arm64, riscv64 @@ -14,7 +20,8 @@ FROM --platform=${BUILDPLATFORM} golang:1.19 as builder ARG TARGETOS ARG TARGETARCH -ARG GOPROXY=https://goproxy.cn +ARG GOPROXY +#ARG GOPROXY=https://goproxy.cn ARG LD_FLAGS="-s -w" ENV GONOPROXY=github.com/apecloud @@ -22,31 +29,34 @@ ENV GONOSUMDB=github.com/apecloud ENV GOPRIVATE=github.com/apecloud ENV GOPROXY=${GOPROXY} -WORKDIR /workspace +WORKDIR /src # Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum +#COPY go.mod go.mod +#COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer # RUN go mod download # Copy the go source -COPY cmd/manager/main.go cmd/manager/main.go -COPY cmd/manager/ cmd/manager/ -COPY apis/ apis/ -COPY internal/ internal/ -COPY controllers/ controllers/ -COPY test/testdata/testdata.go test/testdata/testdata.go - -## have manager as last build step due to its volatility -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o manager cmd/manager/main.go +#COPY cmd/manager/main.go cmd/manager/main.go +#COPY cmd/manager/ cmd/manager/ +#COPY apis/ apis/ +#COPY internal/ internal/ +#COPY controllers/ controllers/ +#COPY test/testdata/testdata.go test/testdata/testdata.go + +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go env && \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -o /out/manager ./cmd/manager/main.go # Use distroless as minimal base image to package the manager binary # Refer to https://github.com/GoogleContainerTools/distroless for more details -FROM katanomi/distroless-static:nonroot +FROM ${DIST_IMG} as dist WORKDIR / -COPY --from=builder /workspace/manager . +COPY --from=builder /out/manager . USER 65532:65532 ENTRYPOINT ["/manager"] diff --git a/docker/Dockerfile-dev b/docker/Dockerfile-dev index d96661027..8ed81f28e 100644 --- a/docker/Dockerfile-dev +++ b/docker/Dockerfile-dev @@ -1,7 +1,7 @@ # Based on https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/go/.devcontainer/base.Dockerfile # [Choice] Go version: 1, 1.19, 1.18, etc -ARG GOVERSION=1.19 +ARG GOVERSION=1.20 FROM golang:${GOVERSION}-bullseye # Copy library scripts to execute diff --git a/docker/Dockerfile-tools b/docker/Dockerfile-tools index 3c7f90e1b..16230ffaa 100644 --- a/docker/Dockerfile-tools +++ b/docker/Dockerfile-tools @@ -1,21 +1,34 @@ # Build the kubeblocks tools binaries # includes kbcli, kubectl, and manager tools. -FROM --platform=${BUILDPLATFORM} golang:1.19 as builder -## docker buildx buid injected build-args: +## docker buildx build injected build-args: #BUILDPLATFORM — matches the current machine. (e.g. linux/amd64) #BUILDOS — os component of BUILDPLATFORM, e.g. linux #BUILDARCH — e.g. amd64, arm64, riscv64 -#BUILDVARIANT — used to set ARM variant, e.g. v7 +#BUILDVARIANT — used to set build ARM variant, e.g. v7 #TARGETPLATFORM — The value set with --platform flag on build #TARGETOS - OS component from --platform, e.g. linux #TARGETARCH - Architecture from --platform, e.g. arm64 -#TARGETVARIANT +#TARGETVARIANT - used to set target ARM variant, e.g. v7 +ARG GO_VERSION=1.20 + +FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} as bin-downloader ARG TARGETOS ARG TARGETARCH +ARG KUBECTL_VERSION=1.26.3 + +WORKDIR /workspace -ARG GOPROXY=https://goproxy.cn +# Download binaries +RUN curl -fsSL https://dl.k8s.io/v${KUBECTL_VERSION}/kubernetes-client-${TARGETOS}-${TARGETARCH}.tar.gz | tar -zxv + + +FROM --platform=${BUILDPLATFORM} golang:${GO_VERSION} as builder +ARG TARGETOS +ARG TARGETARCH +ARG GOPROXY +#ARG GOPROXY=https://goproxy.cn ARG LD_FLAGS="-s -w" ENV GONOPROXY=github.com/apecloud @@ -23,53 +36,69 @@ ENV GONOSUMDB=github.com/apecloud ENV GOPRIVATE=github.com/apecloud ENV GOPROXY=${GOPROXY} -WORKDIR /workspace +WORKDIR /src # Copy the Go Modules manifests -COPY go.mod go.mod -COPY go.sum go.sum +#COPY go.mod go.mod +#COPY go.sum go.sum # cache deps before building and copying source so that we don't need to re-download as much # and so that source changes don't invalidate our downloaded layer # RUN go mod download # Copy the go source -COPY internal/ internal/ -COPY controllers/ controllers/ -COPY cmd/reloader/ cmd/reloader/ -COPY cmd/probe/ cmd/probe/ -COPY externalapis/ externalapis/ -COPY version/ version/ -COPY cmd/cli/ cmd/cli/ -COPY apis/ apis/ - -# Download binaries -RUN curl -fsSL https://dl.k8s.io/v1.26.3/kubernetes-client-${TARGETOS}-${TARGETARCH}.tar.gz | tar -zxv +#COPY internal/ internal/ +#COPY controllers/ controllers/ +#COPY cmd/reloader/ cmd/reloader/ +#COPY cmd/probe/ cmd/probe/ +#COPY externalapis/ externalapis/ +#COPY version/ version/ +#COPY cmd/cli/ cmd/cli/ +#COPY apis/ apis/ +#COPY test/testdata/testdata.go test/testdata/testdata.go # Build -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o reloader cmd/reloader/main.go -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o killer cmd/reloader/container_killer/killer.go -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o probe cmd/probe/main.go -RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o kbcli cmd/cli/main.go +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + go env && \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/killer cmd/reloader/container_killer/killer.go + +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/reloader cmd/reloader/main.go + +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/probe cmd/probe/main.go + +RUN --mount=type=bind,target=. \ + --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -ldflags="${LD_FLAGS}" -a -o /out/kbcli cmd/cli/main.go # Use alpine -FROM docker.io/alpine:3.17 +FROM docker.io/alpine:3.17 as dist +ARG APK_MIRROR +#ARG APK_MIRROR="mirrors.aliyun.com" # install tools via apk -RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories +ENV APK_MIRROR=${APK_MIRROR} +RUN if [ -n "${APK_MIRROR}" ]; then sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories; fi RUN apk add --no-cache curl helm \ && rm -rf /var/cache/apk/* # use apk to install kubectl in the next alpine version. -COPY --from=builder /workspace/kubernetes/client/bin/kubectl /bin +COPY --from=bin-downloader /workspace/kubernetes/client/bin/kubectl /bin # copy kubeblocks tools -COPY --from=builder /workspace/killer /bin -COPY --from=builder /workspace/reloader /bin COPY config/probe config/probe -COPY --from=builder /workspace/probe /bin -COPY --from=builder /workspace/kbcli /bin +COPY --from=builder /out/killer /bin +COPY --from=builder /out/reloader /bin +COPY --from=builder /out/probe /bin +COPY --from=builder /out/kbcli /bin -WORKDIR / # mkdir kbcli config dir and helm cache dir. RUN mkdir /.kbcli && chown -R 65532:65532 /.kbcli \ && mkdir /.cache && chown -R 65532:65532 /.cache diff --git a/docker/custom-scripts/kubectl-helm-debian.sh b/docker/custom-scripts/kubectl-helm-debian.sh index e0ee37dc4..0bf9d8f02 100644 --- a/docker/custom-scripts/kubectl-helm-debian.sh +++ b/docker/custom-scripts/kubectl-helm-debian.sh @@ -26,7 +26,7 @@ docker/#!/usr/bin/env bash # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/kubectl-helm.md # Maintainer: The VS Code and Codespaces Teams # -# Syntax: ./kubectl-helm-debian.sh [kubectl verison] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256] +# Syntax: ./kubectl-helm-debian.sh [kubectl version] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256] set -e diff --git a/docker/custom-scripts/setup-user.sh b/docker/custom-scripts/setup-user.sh index 6f5122cfa..4f06f1f94 100644 --- a/docker/custom-scripts/setup-user.sh +++ b/docker/custom-scripts/setup-user.sh @@ -1,6 +1,5 @@ #!/usr/bin/env bash # -# Copyright 2022 The KubeBlocks Authors # Copyright 2021 The Dapr Authors # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/docker/docker.mk b/docker/docker.mk index 2a61c35ab..944fa904a 100644 --- a/docker/docker.mk +++ b/docker/docker.mk @@ -1,14 +1,20 @@ # -# Copyright ApeCloud, Inc. -# 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. +#Copyright (C) 2022-2023 ApeCloud Co., Ltd +# +#This file is part of KubeBlocks project +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . # # To use buildx: https://github.com/docker/buildx#docker-ce @@ -18,11 +24,15 @@ DEBIAN_MIRROR=mirrors.aliyun.com # Docker image build and push setting -DOCKER:=docker +DOCKER:=DOCKER_BUILDKIT=1 docker DOCKERFILE_DIR?=./docker +# BUILDX_PLATFORMS ?= $(subst -,/,$(ARCH)) +BUILDX_PLATFORMS ?= linux/amd64,linux/arm64 + # Image URL to use all building/pushing image targets IMG ?= docker.io/apecloud/$(APP_NAME) +TOOL_IMG ?= docker.io/apecloud/$(APP_NAME)-tools CLI_IMG ?= docker.io/apecloud/kbcli CLI_TAG ?= v$(CLI_VERSION) @@ -40,9 +50,9 @@ BUILDX_ARGS ?= build-dev-image: DOCKER_BUILD_ARGS += --build-arg DEBIAN_MIRROR=$(DEBIAN_MIRROR) --build-arg GITHUB_PROXY=$(GITHUB_PROXY) --build-arg GOPROXY=$(GOPROXY) build-dev-image: ## Build dev container image. ifneq ($(BUILDX_ENABLED), true) - docker build $(DOCKERFILE_DIR)/. $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/${DEV_CONTAINER_DOCKERFILE} -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) + $(DOCKER) $(DOCKERFILE_DIR)/. $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/${DEV_CONTAINER_DOCKERFILE} -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) else - docker buildx build $(DOCKERFILE_DIR)/. $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -f $(DOCKERFILE_DIR)/$(DEV_CONTAINER_DOCKERFILE) -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) $(BUILDX_ARGS) + $(DOCKER) buildx build $(DOCKERFILE_DIR)/. $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -f $(DOCKERFILE_DIR)/$(DEV_CONTAINER_DOCKERFILE) -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) $(BUILDX_ARGS) endif @@ -50,65 +60,69 @@ endif push-dev-image: DOCKER_BUILD_ARGS += --build-arg DEBIAN_MIRROR=$(DEBIAN_MIRROR) --build-arg GITHUB_PROXY=$(GITHUB_PROXY) --build-arg GOPROXY=$(GOPROXY) push-dev-image: ## Push dev container image. ifneq ($(BUILDX_ENABLED), true) - docker push $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) + $(DOCKER) push $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) else - docker buildx build . $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -f $(DOCKERFILE_DIR)/$(DEV_CONTAINER_DOCKERFILE) -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) --push $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -f $(DOCKERFILE_DIR)/$(DEV_CONTAINER_DOCKERFILE) -t $(DEV_CONTAINER_IMAGE_NAME):$(DEV_CONTAINER_VERSION_TAG) --push $(BUILDX_ARGS) endif .PHONY: build-manager-image +build-manager-image: DOCKER_BUILD_ARGS += --cache-to type=gha,scope=${GITHUB_REF_NAME}-manager-image --cache-from type=gha,scope=${GITHUB_REF_NAME}-manager-image build-manager-image: generate ## Build Operator manager container image. ifneq ($(BUILDX_ENABLED), true) - docker build . -t ${IMG}:${VERSION} -f $(DOCKERFILE_DIR)/Dockerfile -t ${IMG}:latest + $(DOCKER) build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile -t ${IMG}:${VERSION} -t ${IMG}:latest else ifeq ($(TAG_LATEST), true) - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest $(BUILDX_ARGS) else - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} $(BUILDX_ARGS) endif endif .PHONY: push-manager-image +push-manager-image: DOCKER_BUILD_ARGS += --cache-to type=gha,scope=${GITHUB_REF_NAME}-manager-image --cache-from type=gha,scope=${GITHUB_REF_NAME}-manager-image push-manager-image: generate ## Push Operator manager container image. ifneq ($(BUILDX_ENABLED), true) ifeq ($(TAG_LATEST), true) - docker push ${IMG}:latest + $(DOCKER) push ${IMG}:latest else - docker push ${IMG}:${VERSION} + $(DOCKER) push ${IMG}:${VERSION} endif else ifeq ($(TAG_LATEST), true) - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest --push $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest --push $(BUILDX_ARGS) else - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} --push $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} --push $(BUILDX_ARGS) endif endif .PHONY: build-tools-image -build-tools-image: generate ## Build tools container image. +build-tools-image: DOCKER_BUILD_ARGS += --cache-to type=gha,scope=${GITHUB_REF_NAME}-tools-image --cache-from type=gha,scope=${GITHUB_REF_NAME}-tools-image +build-tools-image: generate test-go-generate ## Build tools container image. ifneq ($(BUILDX_ENABLED), true) - docker build . -t ${IMG}:${VERSION} -f $(DOCKERFILE_DIR)/Dockerfile-tools -t ${IMG}:latest + $(DOCKER) build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile-tools -t ${TOOL_IMG}:${VERSION} -t ${TOOL_IMG}:latest else ifeq ($(TAG_LATEST), true) - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${TOOL_IMG}:latest $(BUILDX_ARGS) else - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${TOOL_IMG}:${VERSION} $(BUILDX_ARGS) endif endif .PHONY: push-tools-image -push-tools-image: generate ## Push tools container image. +push-tools-image: DOCKER_BUILD_ARGS += --cache-to type=gha,scope=${GITHUB_REF_NAME}-tools-image --cache-from type=gha,scope=${GITHUB_REF_NAME}-tools-image +push-tools-image: generate test-go-generate ## Push tools container image. ifneq ($(BUILDX_ENABLED), true) ifeq ($(TAG_LATEST), true) - docker push ${IMG}:latest + $(DOCKER) push ${TOOL_IMG}:latest else - docker push ${IMG}:${VERSION} + $(DOCKER) push ${TOOL_IMG}:${VERSION} endif else ifeq ($(TAG_LATEST), true) - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:latest --push $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile-tools --platform $(BUILDX_PLATFORMS) -t ${TOOL_IMG}:latest --push $(BUILDX_ARGS) else - docker buildx build . -f $(DOCKERFILE_DIR)/Dockerfile-tools $(DOCKER_BUILD_ARGS) --platform $(BUILDX_PLATFORMS) -t ${IMG}:${VERSION} --push $(BUILDX_ARGS) + $(DOCKER) buildx build . $(DOCKER_BUILD_ARGS) -f $(DOCKERFILE_DIR)/Dockerfile-tools --platform $(BUILDX_PLATFORMS) -t ${TOOL_IMG}:${VERSION} --push $(BUILDX_ARGS) endif endif diff --git a/docker/library-scripts/go-debian.sh b/docker/library-scripts/go-debian.sh index 0e3400d92..f109b289f 100644 --- a/docker/library-scripts/go-debian.sh +++ b/docker/library-scripts/go-debian.sh @@ -156,7 +156,7 @@ fi usermod -a -G golang "${USERNAME}" mkdir -p "${TARGET_GOROOT}" "${TARGET_GOPATH}" if [ "${TARGET_GO_VERSION}" != "none" ] && ! type go > /dev/null 2>&1; then - # Use a temporary locaiton for gpg keys to avoid polluting image + # Use a temporary location for gpg keys to avoid polluting image export GNUPGHOME="/tmp/tmp-gnupg" mkdir -p ${GNUPGHOME} chmod 700 ${GNUPGHOME} diff --git a/docker/library-scripts/kubectl-helm-debian.sh b/docker/library-scripts/kubectl-helm-debian.sh index 6e4a7ca2c..0c1bd32a5 100644 --- a/docker/library-scripts/kubectl-helm-debian.sh +++ b/docker/library-scripts/kubectl-helm-debian.sh @@ -26,7 +26,7 @@ docker/#!/usr/bin/env bash # Docs: https://github.com/microsoft/vscode-dev-containers/blob/main/script-library/docs/kubectl-helm.md # Maintainer: The VS Code and Codespaces Teams # -# Syntax: ./kubectl-helm-debian.sh [kubectl verison] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256] +# Syntax: ./kubectl-helm-debian.sh [kubectl version] [Helm version] [minikube version] [kubectl SHA256] [Helm SHA256] [minikube SHA256] set -e diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index fa1087bf7..0442b6212 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,4 +1,4 @@ -# Contributing KubeBlocks +# Contributing to KubeBlocks First, thank you for contributing to KubeBlocks! This document provides the guidelines for how to contribute to KubeBlocks. diff --git a/docs/DEVELOPING.md b/docs/DEVELOPING.md index 1dc766e08..fb06aba1f 100644 --- a/docs/DEVELOPING.md +++ b/docs/DEVELOPING.md @@ -46,7 +46,7 @@ To build `KubeBlocks` on your own host, needs to install the following tools: - Make #### Install Go -Download and install [Go 1.19 or later](https://go.dev/doc/install). +Download and install [Go 1.20 or later](https://go.dev/doc/install). #### Install Make `KubeBlocks` uses `make` for a variety of build and test actions, and needs to be installed as appropriate for your platform: diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 2c5009d95..000000000 --- a/docs/README.md +++ /dev/null @@ -1,19 +0,0 @@ -# KubeBlocks Documentation - - Here store design documents, release notes and user documents of KubeBlocks. - -## What is KubeBlocks -KubeBlocks, running on K8S, offers a universal view through multi-clouds and on-premises, provides a consistent experience for fully managing multiple databases, and relieves the burden of maintaining miscellaneous operators. -![image](https://user-images.githubusercontent.com/110531738/202367695-babd2ebc-8b7f-4a3d-b1d7-2d7b1b69f2bc.png#width="60%") -## Find documents -* Refer to [design docs](./design_docs) for our design motivation and methodology. -* Refer to [user docs](./user_docs) for instructions to use KubeBlocks. -* Refer to [release notes](./release_notes) for the latest updates. - - -To write a design document, refer to design document templates. (in addition to your godoc generated documentation). -#### Examples: - -* https://github.com/gohugoio/hugo/tree/master/docs -* https://github.com/openshift/origin/tree/master/docs -* https://github.com/dapr/dapr/tree/master/docs diff --git a/docs/img/banner-readme.png b/docs/img/banner-readme.png new file mode 100644 index 000000000..729367956 Binary files /dev/null and b/docs/img/banner-readme.png differ diff --git a/docs/img/kubeblocks-structure-new.jpg b/docs/img/kubeblocks-structure-new.jpg new file mode 100644 index 000000000..43ba24aa8 Binary files /dev/null and b/docs/img/kubeblocks-structure-new.jpg differ diff --git a/docs/img/pgsql-ha-after.png b/docs/img/pgsql-ha-after.png new file mode 100644 index 000000000..5c916fea7 Binary files /dev/null and b/docs/img/pgsql-ha-after.png differ diff --git a/docs/img/pgsql-ha-before.png b/docs/img/pgsql-ha-before.png new file mode 100644 index 000000000..ce3718844 Binary files /dev/null and b/docs/img/pgsql-ha-before.png differ diff --git a/docs/img/pgsql-ha-pg_stat_replication.png b/docs/img/pgsql-ha-pg_stat_replication.png new file mode 100644 index 000000000..f58cd812f Binary files /dev/null and b/docs/img/pgsql-ha-pg_stat_replication.png differ diff --git a/docs/img/pgsql-migration-describe-task.png b/docs/img/pgsql-migration-describe-task.png new file mode 100644 index 000000000..a28ef5594 Binary files /dev/null and b/docs/img/pgsql-migration-describe-task.png differ diff --git a/docs/img/pgsql-migration-sink.png b/docs/img/pgsql-migration-sink.png new file mode 100644 index 000000000..4ce2b3690 Binary files /dev/null and b/docs/img/pgsql-migration-sink.png differ diff --git a/docs/img/pgsql-migration-timestamp.png b/docs/img/pgsql-migration-timestamp.png new file mode 100644 index 000000000..aa233a030 Binary files /dev/null and b/docs/img/pgsql-migration-timestamp.png differ diff --git a/docs/img/redis-ha-after.png b/docs/img/redis-ha-after.png new file mode 100644 index 000000000..b1c06f1d3 Binary files /dev/null and b/docs/img/redis-ha-after.png differ diff --git a/docs/img/redis-ha-before.png b/docs/img/redis-ha-before.png new file mode 100644 index 000000000..8f30a1824 Binary files /dev/null and b/docs/img/redis-ha-before.png differ diff --git a/docs/img/redis-ha-info-replication.png b/docs/img/redis-ha-info-replication.png new file mode 100644 index 000000000..234671fec Binary files /dev/null and b/docs/img/redis-ha-info-replication.png differ diff --git a/docs/release_notes/kbcli_template.md b/docs/release_notes/kbcli_template.md new file mode 100644 index 000000000..e48d05300 --- /dev/null +++ b/docs/release_notes/kbcli_template.md @@ -0,0 +1,3 @@ +# KBCLI $kbcli_version ($today) + +## sha256 sumcheck diff --git a/docs/release_notes/template.md b/docs/release_notes/template.md new file mode 100644 index 000000000..c81747402 --- /dev/null +++ b/docs/release_notes/template.md @@ -0,0 +1,35 @@ +# KubeBlocks $kubeblocks_version ($today) + +We're happy to announce the release of KubeBlocks $kubeblocks_version! 🚀 🎉 🎈 + +We would like to extend our appreciation to all contributors who helped make this release happen. + +**Breaking changes** + + +**Highlights** + + +**Known issues and limitations** + * Limitations of cluster's horizontal scale operation: + * Only support VolumeSnapshot API to make a clone of Cluster's PV for syncing data when horizontal scaling. + * Only 1st pod container and 1st volume mount associated PV will be processed for VolumeSnapshot, do assure that data volume is placed in 1st pod container's 1st volume mount. + * Unused PVCs will be deleted in 30 minutes after scale in. + +If you're new to KubeBlocks, visit the [getting started](https://github.com/apecloud/kubeblocks/blob/v$kubeblocks_version/docs/user_docs/quick_start_guide.md) page and get a quick start with KubeBlocks. + +$warnings + +See [this](#upgrading-to-kubeblocks-$kubeblocks_version) section to upgrade KubeBlocks to version $kubeblocks_version. + +## Acknowledgements + +Thanks to everyone who made this release possible! + +$kubeblocks_contributors + +## What's Changed +$kubeblocks_changes + +## Upgrading to KubeBlocks $kubeblocks_version + diff --git a/docs/release_notes/v0.1.0/template.md b/docs/release_notes/v0.1.0/template.md index 14324067d..cab736d94 100644 --- a/docs/release_notes/v0.1.0/template.md +++ b/docs/release_notes/v0.1.0/template.md @@ -19,7 +19,7 @@ We would like to extend our appreciation to all contributors who helped make thi **Highlights** * Automatic pod container environment variables updates: - * [NEW] KB_POD_FQDN - KubeBlock Cluster component workload associated headless service name, N/A if workloadType=Stateless. + * [NEW] KB_POD_FQDN - KubeBlocks Cluster component workload associated headless service name, N/A if workloadType=Stateless. * [NEW] KB_POD_IP - Pod IP address * [NEW] KB_POD_IPS - Pod IP addresses * [NEW] KB_HOST_IP - Host IP address @@ -30,16 +30,16 @@ We would like to extend our appreciation to all contributors who helped make thi * KB_NAMESPACE - Namespace * KB_SA_NAME - Service Account Name * KB_NODENAME - Node Name - * KB_CLUSTER_NAME - KubeBlock Cluster API object name - * KB_COMP_NAME - Running pod's KubeBlock Cluster API object's `.spec.components.name` - * KB_CLUSTER_COMP_NAME - Running pod's KubeBlock Cluster API object's `<.metadata.name>-<.spec.components.name>`, same name is used for Deployment or StatefulSet workload name, and Service object name + * KB_CLUSTER_NAME - KubeBlocks Cluster API object name + * KB_COMP_NAME - Running pod's KubeBlocks Cluster API object's `.spec.components.name` + * KB_CLUSTER_COMP_NAME - Running pod's KubeBlocks Cluster API object's `<.metadata.name>-<.spec.components.name>`, same name is used for Deployment or StatefulSet workload name, and Service object name * New KubeBlocks addon extensions management (an addon components are part of KubeBlocks control plane extensions). Highlights include: * New addons.extensions.kubeblocks.io API that provide running cluster installable check and auto-installation settings. * Following addons are provided: * Prometheus and Alertmanager * AlertManager Webhook Adaptor * Grafana - * Kubeblocks CSI driver + * KubeBlocks CSI driver * S3 CSI driver * Snapshot Controller * KubeBlocks private network Load Balancer diff --git a/docs/release_notes/v0.1.0/v0.1.0.md b/docs/release_notes/v0.1.0/v0.1.0.md index ede0ea97d..c184417fe 100644 --- a/docs/release_notes/v0.1.0/v0.1.0.md +++ b/docs/release_notes/v0.1.0/v0.1.0.md @@ -105,10 +105,10 @@ Thanks to everyone who made this release possible! - migrate KubeBlocks core driver operator ([#78](https://github.com/apecloud/kubeblocks/pull/78), @nashtsai) - support appVersion, clusterDefinition and cluster CR validating webhook ([#83](https://github.com/apecloud/kubeblocks/pull/83), @wangyelei) - fix ci-test and add badges ([#88](https://github.com/apecloud/kubeblocks/pull/88), @JashBook) -- Feature/unified dbcluser lifecycle ([#89](https://github.com/apecloud/kubeblocks/pull/89), @lynnleelhl) +- Feature/unified dbcluster lifecycle ([#89](https://github.com/apecloud/kubeblocks/pull/89), @lynnleelhl) - fix release publish ([#95](https://github.com/apecloud/kubeblocks/pull/95), @JashBook) - Support/csi driver volume testing ([#99](https://github.com/apecloud/kubeblocks/pull/99), @nashtsai) -- Feature/unified dbcluser lifecycle ([#100](https://github.com/apecloud/kubeblocks/pull/100), @lynnleelhl) +- Feature/unified dbcluster lifecycle ([#100](https://github.com/apecloud/kubeblocks/pull/100), @lynnleelhl) - add Cluster Status handling. ([#101](https://github.com/apecloud/kubeblocks/pull/101), @wangyelei) - Refactor/container to podspec ([#102](https://github.com/apecloud/kubeblocks/pull/102), @lynnleelhl) - CICD add staticcheck ([#106](https://github.com/apecloud/kubeblocks/pull/106), @JashBook) @@ -140,7 +140,7 @@ Thanks to everyone who made this release possible! - Remove unused field declaration from cue template ([#219](https://github.com/apecloud/kubeblocks/pull/219), @heng4fun) - Inject prometheus exporter as sidecar ([#224](https://github.com/apecloud/kubeblocks/pull/224), @yimeisun) - check dependency binary existence when exec make command ([#227](https://github.com/apecloud/kubeblocks/pull/227), @yimeisun) -- integrate simplified monitor stack to kubeblock helm chart ([#228](https://github.com/apecloud/kubeblocks/pull/228), @yimeisun) +- integrate simplified monitor stack to KubeBlocks helm chart ([#228](https://github.com/apecloud/kubeblocks/pull/228), @yimeisun) - Improve reviewable checks ([#236](https://github.com/apecloud/kubeblocks/pull/236), @heng4fun) - change PR label ([#240](https://github.com/apecloud/kubeblocks/pull/240), @JashBook) - integrate OpsRequest dbctl Cli and fix the ops bug ([#247](https://github.com/apecloud/kubeblocks/pull/247), @wangyelei) diff --git a/docs/release_notes/v0.2.0/v0.2.0.md b/docs/release_notes/v0.2.0/v0.2.0.md index 024819e69..24bd97a71 100644 --- a/docs/release_notes/v0.2.0/v0.2.0.md +++ b/docs/release_notes/v0.2.0/v0.2.0.md @@ -19,9 +19,9 @@ We would like to extend our thanks to all the new and existing contributors who * KB_HOSTIP - Host IP address * KB_PODIP - Pod IP address * KB_PODIPS - Pod IP addresses - * KB_CLUSTER_NAME - KubeBlock Cluster API object name - * KB_COMP_NAME - Running pod's KubeBlock Cluster API object's `.spec.components.name` - * KB_CLUSTER_COMP_NAME - Running pod's KubeBlock Cluster API object's `<.metadata.name>-<.spec.components.name>`, same name is used for Deployment or StatefulSet workload name, and Service object name + * KB_CLUSTER_NAME - KubeBlocks Cluster API object name + * KB_COMP_NAME - Running pod's KubeBlocks Cluster API object's `.spec.components.name` + * KB_CLUSTER_COMP_NAME - Running pod's KubeBlocks Cluster API object's `<.metadata.name>-<.spec.components.name>`, same name is used for Deployment or StatefulSet workload name, and Service object name * ClusterDefinition API support following automatic variable names: * under `.spec.connectionCredential`: * random 8 characters `$(RANDOM_PASSWD)` placeholder, diff --git a/docs/release_notes/v0.4.0/v0.4.0.md b/docs/release_notes/v0.4.0/v0.4.0.md index 8845aafc8..edb423a57 100644 --- a/docs/release_notes/v0.4.0/v0.4.0.md +++ b/docs/release_notes/v0.4.0/v0.4.0.md @@ -52,7 +52,7 @@ Thanks to everyone who made this release possible! #### Resource Isolation -- KubeBlocks has built-in affinity and toleration configurations on both data plane and control plane to prevent stateless applications from competing for KubeBlock's runtime resources. [#1533](https://github.com/apecloud/kubeblocks/issues/1533) +- KubeBlocks has built-in affinity and toleration configurations on both data plane and control plane to prevent stateless applications from competing for KubeBlocks' runtime resources. [#1533](https://github.com/apecloud/kubeblocks/issues/1533) - KubeBlocks' database cluster introduces tenant types, where dedicated tenancy can avoid the behavior of database clusters competing for runtime resources with each other. [#931](https://github.com/apecloud/kubeblocks/issues/931) #### Observability diff --git a/docs/release_notes/v0.5.0.md b/docs/release_notes/v0.5.0.md new file mode 100644 index 000000000..8f02c5f61 --- /dev/null +++ b/docs/release_notes/v0.5.0.md @@ -0,0 +1,86 @@ +# KubeBlocks 0.5.0 (2023-04-20) + +We're happy to announce the release of KubeBlocks 0.5.0! 🚀 🎉 🎈 + +We would like to extend our appreciation to all contributors who helped make this release happen. + +**Breaking changes** + + +**Highlights** + + +**Known issues and limitations** + * Limitations of cluster's horizontal scale operation: + * Only support VolumeSnapshot API to make a clone of Cluster's PV for syncing data when horizontal scaling. + * Only 1st pod container and 1st volume mount associated PV will be processed for VolumeSnapshot, do assure that data volume is placed in 1st pod container's 1st volume mount. + * Unused PVCs will be deleted in 30 minutes after scale in. + +If you're new to KubeBlocks, visit the [getting started](https://github.com/apecloud/kubeblocks/blob/v0.5.0/docs/user_docs/quick_start_guide.md) page and get a quick start with KubeBlocks. + +> **Note: This release contains a few [breaking changes](#breaking-changes).** + +See [this](#upgrading-to-kubeblocks-0.5.0) section to upgrade KubeBlocks to version 0.5.0. + +## Acknowledgements + +Thanks to everyone who made this release possible! + +@1aal, @free6om, @heng4fun, @iziang, @ldming, @nayutah, @sophon-zt, @TalktoCrystal, @xuriwuyun, @Y-Rookie, @ZhaoDiankui + +## What's Changed + +### New Features +- support Redis snapshot backup and restore ([#1886](https://github.com/apecloud/kubeblocks/pull/1886), @heng4fun) +- sql channel support postgres ([#1898](https://github.com/apecloud/kubeblocks/pull/1898), @xuriwuyun) +- sql channel support pg checkstatus ([#2043](https://github.com/apecloud/kubeblocks/pull/2043), @xuriwuyun) +- support vitess ([#2116](https://github.com/apecloud/kubeblocks/pull/2116), @ZhaoDiankui) +- support mongodb ([#2182](https://github.com/apecloud/kubeblocks/pull/2182), @xuriwuyun) +- cli playground supports more cloud provider ([#2241](https://github.com/apecloud/kubeblocks/pull/2241), @ldming) +- support milvus standalone mode ([#2310](https://github.com/apecloud/kubeblocks/pull/2310), @nayutah) +- highly available Postgresql using our own image that support pgvector ([#2406](https://github.com/apecloud/kubeblocks/pull/2406), @ldming) +- delete clusters and uninstall kubeblocks when playground destroy ([#2457](https://github.com/apecloud/kubeblocks/pull/2457), @ldming) +- support mongodb backup ([#2682](https://github.com/apecloud/kubeblocks/pull/2682), @xuriwuyun) + +### Bug Fixes +- cli playground use default kubeconfig file ([#2150](https://github.com/apecloud/kubeblocks/pull/2150), @ldming) +- update running check ([#2174](https://github.com/apecloud/kubeblocks/pull/2174), @xuriwuyun) +- set cluster default storage size to 20Gi ([#2254](https://github.com/apecloud/kubeblocks/pull/2254), @ldming) +- cli kubeblocks upgrade command output dashboard info ([#2290](https://github.com/apecloud/kubeblocks/pull/2290), @ldming) +- set default storage size to 10Gi for TKE ([#2317](https://github.com/apecloud/kubeblocks/pull/2317), @ldming) +- cli playground pull latest cloud provider repo ([#2373](https://github.com/apecloud/kubeblocks/pull/2373), @ldming) +- cli playground does not output error message when kubernetes cluster is not ready ([#2391](https://github.com/apecloud/kubeblocks/pull/2391), @ldming) +- github action uploads kbcli asset for windows and add powershell script to install on windows ([#2449](https://github.com/apecloud/kubeblocks/pull/2449), @1aal) +- trim single quotes for the parameters value in the pg config file (#2523) ([#2527](https://github.com/apecloud/kubeblocks/pull/2527), @sophon-zt) +- config change does not take effect (#2511) ([#2543](https://github.com/apecloud/kubeblocks/pull/2543), @sophon-zt) +- KB_FOLLOWERS env inconsistent with cluster status after scale-in ([#2565](https://github.com/apecloud/kubeblocks/pull/2565), @free6om) +- BackupPolicyTemplate name of mysql-scale error ([#2583](https://github.com/apecloud/kubeblocks/pull/2583), @ZhaoDiankui) +- probe pg checkrole ([#2638](https://github.com/apecloud/kubeblocks/pull/2638), @xuriwuyun) +- adjust vtgate healthcheck options ([#2650](https://github.com/apecloud/kubeblocks/pull/2650), @ZhaoDiankui) +- h-scale pvc unexpected deleting ([#2680](https://github.com/apecloud/kubeblocks/pull/2680), @free6om) +- support mongodb backup ([#2683](https://github.com/apecloud/kubeblocks/pull/2683), @xuriwuyun) +- replicationSet cluster stop failed fix ([#2691](https://github.com/apecloud/kubeblocks/pull/2691), @Y-Rookie) +- h-scale pvc unexpected deleting (#2680) ([#2730](https://github.com/apecloud/kubeblocks/pull/2730), @free6om) + +### Miscellaneous +- lifecycle dag ([#1571](https://github.com/apecloud/kubeblocks/pull/1571), @free6om) +- add cluster default webhook for `PrimaryIndex` ([#1677](https://github.com/apecloud/kubeblocks/pull/1677), @heng4fun) +- refactor labels usage ([#1696](https://github.com/apecloud/kubeblocks/pull/1696), @heng4fun) +- update probe mysql tests ([#1808](https://github.com/apecloud/kubeblocks/pull/1808), @xuriwuyun) +- update pg probe url ([#2115](https://github.com/apecloud/kubeblocks/pull/2115), @xuriwuyun) +- cli support to output addon install progress ([#2132](https://github.com/apecloud/kubeblocks/pull/2132), @ldming) +- rewrite overview ([#2266](https://github.com/apecloud/kubeblocks/pull/2266), @TalktoCrystal) +- move loadbalancer sub-module to a separate repo https ([#2354](https://github.com/apecloud/kubeblocks/pull/2354), @iziang) +- use gitlab helm repo if failed to get ip location ([#2421](https://github.com/apecloud/kubeblocks/pull/2421), @ldming) +- update redis role probe ([#2554](https://github.com/apecloud/kubeblocks/pull/2554), @xuriwuyun) +- update mongodb helm ([#2575](https://github.com/apecloud/kubeblocks/pull/2575), @xuriwuyun) +- kbcli support mongodb ([#2580](https://github.com/apecloud/kubeblocks/pull/2580), @xuriwuyun) +- support xengine for apecloud-mysql ([#2608](https://github.com/apecloud/kubeblocks/pull/2608), @sophon-zt) +- support postgresql 14.7 instead of 15.2 ([#2613](https://github.com/apecloud/kubeblocks/pull/2613), @ldming) +- improve cluster create examples ([#2641](https://github.com/apecloud/kubeblocks/pull/2641), @ldming) +- ut for nil backup policy ([#2654](https://github.com/apecloud/kubeblocks/pull/2654), @free6om) +- sqlchannel add test ([#2694](https://github.com/apecloud/kubeblocks/pull/2694), @xuriwuyun) +- configure does not take effect ([#2735](https://github.com/apecloud/kubeblocks/pull/2735), @sophon-zt) + +## Upgrading to KubeBlocks 0.5.0 + diff --git a/docs/release_notes/v0.5.0/v0.5.0.md b/docs/release_notes/v0.5.0/v0.5.0.md index e62720bb7..e07383137 100644 --- a/docs/release_notes/v0.5.0/v0.5.0.md +++ b/docs/release_notes/v0.5.0/v0.5.0.md @@ -10,10 +10,83 @@ Thanks to everyone who made this release possible! ## What's Changed +- New APIs: + - backuppolicytemplates.apps.kubeblocks.io + - componentclassdefinitions.apps.kubeblocks.io + - componentresourceconstraints.apps.kubeblocks.io + +- Deleted APIs: + - backuppolicytemplates.dataprotection.kubeblocks.io + +- New API attributes: + - clusterdefinitions.apps.kubeblocks.io API + - spec.type + - spec.componentDefs.customLabelSpecs + - clusters.apps.kubeblocks.io API + - spec.componentSpecs.classDefRef + - configconstraints.apps.kubeblocks.io API + - spec.reloadOptions.shellTrigger.namespace + - spec.reloadOptions.shellTrigger.scriptConfigMapRef + - spec.reloadOptions.tplScriptTrigger.sync + - spec.selector + - opsrequests.apps.kubeblocks.io API + - spec.restoreFrom + - spec.verticalScaling.class + - status.reconfiguringStatus.configurationStatus.updatePolicy + - backuppolicies.dataprotection.kubeblocks.io API + - spec.full + - backups.dataprotection.kubeblocks.io + - status.manifests + - backuptools.dataprotection.kubeblocks.io + - spec.type + +- Renamed API attributes: + - clusterdefinitions.apps.kubeblocks.io API + - spec.componentDefs.horizontalScalePolicy.backupTemplateSelector -> spec.componentDefs.horizontalScalePolicy.backupPolicyTemplateName + - spec.componentDefs.probe.roleChangedProbe -> spec.componentDefs.probe.roleProbe + - restorejobs.dataprotection.kubeblocks.io API + - spec.target.secret.passwordKeyword -> spec.target.secret.passwordKey + - spec.target.secret.userKeyword -> spec.target.secret.usernameKey + - addons.extensions.kubeblocks.io API + - spec.helm.installValues.secretsRefs -> spec.helm.installValues.secretRefs + +- Deleted API attributes: + - opsrequests.apps.kubeblocks.io API + - status.observedGeneration + - backuppolicies.dataprotection.kubeblocks.io API + - spec.backupPolicyTemplateName + - spec.backupToolName + - spec.backupType + - spec.backupsHistoryLimit + - spec.hooks + - backups.dataprotection.kubeblocks.io API + - spec.ttl + - status.CheckPoint + - status.checkSum + - addons.extensions.kubeblocks.io API + - spec.helm.valuesMapping.jsonMap.additionalProperties + - spec.helm.valuesMapping.valueMap.additionalProperties + - spec.helm.valuesMapping.extras.jsonMap.additionalProperties + - spec.helm.valuesMapping.extras.valueMap.additionalProperties + + +- Updates API Status info: + - clusters.apps.kubeblocks.io API + - status.components.phase valid values are Running, Stopped, Failed, Abnormal, Creating, Updating; REMOVED phases are SpecUpdating, Deleting, Deleted, VolumeExpanding, Reconfiguring, HorizontalScaling, VerticalScaling, VersionUpgrading, Rebooting, Stopping, Starting. + - status.phase valid values are Running, Stopped, Failed, Abnormal, Creating, Updating; REMOVED phases are ConditionsError, SpecUpdating, Deleting, Deleted, VolumeExpanding, Reconfiguring, HorizontalScaling, VerticalScaling, VersionUpgrading, Rebooting, Stopping, Starting. + - opsrequests.apps.kubeblocks.io API + - status.components.phase valid values are Running, Stopped, Failed, Abnormal, Creating, Updating; REMOVED phases are SpecUpdating, Deleting, Deleted, VolumeExpanding, Reconfiguring, HorizontalScaling, VerticalScaling, VersionUpgrading, Rebooting, Stopping, Starting, Exposing. + - status.phase added 'Creating' phase. + + + ### New Features + + #### PostgreSQL +- Support incremental migration from AWS RDS to KubeBlocks, support pre-check, full migration and incremental synchronization #### Redis @@ -25,6 +98,9 @@ Thanks to everyone who made this release possible! #### Easy of Use +* ClusterDefinition API `spec.connectionCredential` add following built-in variables: + * Headless service FQDN `$(HEADLESS_SVC_FQDN)` placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc, where 1ST_COMP_NAME is the 1st component that provide `ClusterDefinition.spec.componentDefs[].service` attribute + #### Resource Isolation @@ -35,3 +111,18 @@ Thanks to everyone who made this release possible! ## Breaking changes + +- Breaking changes between v0.5 and v0.4. Uninstall v0.4 before installing v0.5. + - Move the backupPolicyTemplate API from dataprotection group to apps group. + Before installing v0.5, please ensure that the resources have been cleaned up: + ``` + kubectl delete backuppolicytemplates.dataprotection.kubeblocks.io --all + kubectl delete backuppolicies.dataprotection.kubeblocks.io --all + ``` + - redefines the phase of cluster and component. + Before installing v0.5, please ensure that the resources have been cleaned up: + ``` + kubectl delete clusters.apps.kubeblocks.io --all + kubectl delete opsrequets.apps.kubeblocks.io --all + ``` +- `addons.extensions.kubeblocks.io` API deleted `spec.helm.valuesMapping.jsonMap.additionalProperties`, `spec.helm.valuesMapping.valueMap.additionalProperties`, `spec.helm.valuesMapping.extras.jsonMap.additionalProperties` and `spec.helm.valuesMapping.extras.valueMap.additionalProperties` attributes that was introduced by CRD generator, all existing Addons API YAML shouldn't have referenced these attributes. diff --git a/docs/release_notes/v0.6.0/_category_.yml b/docs/release_notes/v0.6.0/_category_.yml new file mode 100644 index 000000000..a916d53d5 --- /dev/null +++ b/docs/release_notes/v0.6.0/_category_.yml @@ -0,0 +1,4 @@ +position: 1 +label: v0.6.0 +collapsible: true +collapsed: true diff --git a/docs/release_notes/v0.6.0/v0.6.0.md b/docs/release_notes/v0.6.0/v0.6.0.md new file mode 100644 index 000000000..d00981c84 --- /dev/null +++ b/docs/release_notes/v0.6.0/v0.6.0.md @@ -0,0 +1,24 @@ +# KubeBlocks 0.6.0 (TBD) + +We are happy to announce the release of KubeBlocks 0.6.0 with some exciting new features and improvements. + +## Highlights + +## Acknowledgements + +Thanks to everyone who made this release possible! + + +## What's Changed + + +### New Features + + + +#### Compatibility +- Pass the AWS EKS v1.22 / v1.23 / v1.24 / v1.25 compatibility test. + +#### Maintainability +* Automatic pod container environment variables: + * KB_POD_UID - Pod UID diff --git a/docs/user_docs/README.md b/docs/user_docs/README.md deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/user_docs/api-reference/_category_.yml b/docs/user_docs/api-reference/_category_.yml new file mode 100644 index 000000000..1ebe94560 --- /dev/null +++ b/docs/user_docs/api-reference/_category_.yml @@ -0,0 +1,4 @@ +position: 13 +label: API Reference +collapsible: true +collapsed: true diff --git a/docs/user_docs/api-reference/add-on.md b/docs/user_docs/api-reference/add-on.md new file mode 100644 index 000000000..3993181a6 --- /dev/null +++ b/docs/user_docs/api-reference/add-on.md @@ -0,0 +1,1395 @@ +--- +title: Add-On API Reference +description: Add-On API Reference +keywords: [add-on, api] +sidebar_position: 3 +sidebar_label: Add-On +--- +
+

Packages:

+ +

extensions.kubeblocks.io/v1alpha1

+
+
+Resource Types: + +

Addon +

+
+

Addon is the Schema for the add-ons API.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+extensions.kubeblocks.io/v1alpha1 +
+kind
+string +
Addon
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +AddonSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+description
+ +string + +
+(Optional) +

Addon description.


+
+type
+ + +AddonType + + +
+

Add-on type. The valid value is helm.


+
+helm
+ + +HelmTypeInstallSpec + + +
+(Optional) +

Helm installation spec. It’s processed only when type=helm.


+
+defaultInstallValues
+ + +[]AddonDefaultInstallSpecItem + + +
+

Default installation parameters.


+
+install
+ + +AddonInstallSpec + + +
+(Optional) +

Installation parameters.


+
+installable
+ + +InstallableSpec + + +
+(Optional) +

Addon installable spec. It provides selector and auto-install settings.


+
+cliPlugins
+ + +[]CliPlugin + + +
+(Optional) +

Plugin installation spec.


+
+
+status
+ + +AddonStatus + + +
+
+

AddonDefaultInstallSpecItem +

+

+(Appears on:AddonSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+AddonInstallSpec
+ + +AddonInstallSpec + + +
+

+(Members of AddonInstallSpec are embedded into this type.) +

+
+selectors
+ + +[]SelectorRequirement + + +
+(Optional) +

Addon installs parameters selectors by default. If multiple selectors are provided,
all selectors must evaluate to true.


+
+

AddonInstallExtraItem +

+

+(Appears on:AddonInstallSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+AddonInstallSpecItem
+ + +AddonInstallSpecItem + + +
+

+(Members of AddonInstallSpecItem are embedded into this type.) +

+
+name
+ +string + +
+

Name of the item.


+
+

AddonInstallSpec +

+

+(Appears on:AddonDefaultInstallSpecItem, AddonSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+AddonInstallSpecItem
+ + +AddonInstallSpecItem + + +
+

+(Members of AddonInstallSpecItem are embedded into this type.) +

+
+enabled
+ +bool + +
+(Optional) +

enabled can be set if there are no specific installation attributes to be set.


+
+extras
+ + +[]AddonInstallExtraItem + + +
+(Optional) +

Installs spec. for extra items.


+
+

AddonInstallSpecItem +

+

+(Appears on:AddonInstallExtraItem, AddonInstallSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+replicas
+ +int32 + +
+(Optional) +

Replicas value.


+
+persistentVolumeEnabled
+ +bool + +
+(Optional) +

Persistent Volume Enabled value.


+
+storageClass
+ +string + +
+(Optional) +

Storage class name.


+
+tolerations
+ +string + +
+(Optional) +

Tolerations JSON array string value.


+
+resources
+ + +ResourceRequirements + + +
+(Optional) +

Resource requirements.


+
+

AddonPhase +(string alias)

+

+(Appears on:AddonStatus) +

+
+

AddonPhase defines addon phases.


+
+ + + + + + + + + + + + + + + + + + +
ValueDescription

"Disabled"

"Disabling"

"Enabled"

"Enabling"

"Failed"

+

AddonSelectorKey +(string alias)

+

+(Appears on:SelectorRequirement) +

+
+

AddonSelectorKey are selector requirement key types.


+
+ + + + + + + + + + + + +
ValueDescription

"KubeGitVersion"

"KubeVersion"

+

AddonSpec +

+

+(Appears on:Addon) +

+
+

AddonSpec defines the desired state of an add-on.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+description
+ +string + +
+(Optional) +

Addon description.


+
+type
+ + +AddonType + + +
+

Add-on type. The valid value is helm.


+
+helm
+ + +HelmTypeInstallSpec + + +
+(Optional) +

Helm installation spec. It’s processed only when type=helm.


+
+defaultInstallValues
+ + +[]AddonDefaultInstallSpecItem + + +
+

Default installation parameters.


+
+install
+ + +AddonInstallSpec + + +
+(Optional) +

Installation parameters.


+
+installable
+ + +InstallableSpec + + +
+(Optional) +

Addon installable spec. It provides selector and auto-install settings.


+
+cliPlugins
+ + +[]CliPlugin + + +
+(Optional) +

Plugin installation spec.


+
+

AddonStatus +

+

+(Appears on:Addon) +

+
+

AddonStatus defines the observed state of an add-on.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +AddonPhase + + +
+

Add-on installation phases. Valid values are Disabled, Enabled, Failed, Enabling, Disabling.


+
+conditions
+ + +[]Kubernetes meta/v1.Condition + + +
+(Optional) +

Describes the current state of add-on API installation conditions.


+
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the most recent generation observed for this
add-on. It corresponds to the add-on’s generation, which is
updated on mutation by the API Server.


+
+

AddonType +(string alias)

+

+(Appears on:AddonSpec) +

+
+

AddonType defines the addon types.


+
+ + + + + + + + + + +
ValueDescription

"Helm"

+

CliPlugin +

+

+(Appears on:AddonSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the plugin.


+
+indexRepository
+ +string + +
+

The index repository of the plugin.


+
+description
+ +string + +
+(Optional) +

The description of the plugin.


+
+

DataObjectKeySelector +

+

+(Appears on:HelmInstallValues) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Object name of the referent.


+
+key
+ +string + +
+

The key to select.


+
+

HelmInstallOptions +(map[string]string alias)

+

+(Appears on:HelmTypeInstallSpec) +

+
+
+

HelmInstallValues +

+

+(Appears on:HelmTypeInstallSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+urls
+ +[]string + +
+(Optional) +
+configMapRefs
+ + +[]DataObjectKeySelector + + +
+(Optional) +

Selects a key of a ConfigMap item list. The value of ConfigMap can be
a JSON or YAML string content. Use a key name with “.json” or “.yaml” or “.yml”
extension name to specify a content type.


+
+secretRefs
+ + +[]DataObjectKeySelector + + +
+(Optional) +

Selects a key of a Secrets item list. The value of Secrets can be
a JSON or YAML string content. Use a key name with “.json” or “.yaml” or “.yml”
extension name to specify a content type.


+
+setValues
+ +[]string + +
+(Optional) +

Helm install set values. It can specify multiple or separate values with commas(key1=val1,key2=val2).


+
+setJSONValues
+ +[]string + +
+(Optional) +

Helm install set JSON values. It can specify multiple or separate values with commas(key1=jsonval1,key2=jsonval2).


+
+

HelmJSONValueMapType +

+

+(Appears on:HelmValuesMappingItem) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+tolerations
+ +string + +
+(Optional) +

tolerations sets the toleration mapping key.


+
+

HelmTypeInstallSpec +

+

+(Appears on:AddonSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+chartLocationURL
+ +string + +
+

A Helm Chart location URL.


+
+installOptions
+ + +HelmInstallOptions + + +
+(Optional) +

installOptions defines Helm release installation options.


+
+installValues
+ + +HelmInstallValues + + +
+(Optional) +

HelmInstallValues defines Helm release installation set values.


+
+valuesMapping
+ + +HelmValuesMapping + + +
+(Optional) +

valuesMapping defines add-on normalized resources parameters mapped to Helm values’ keys.


+
+

HelmValueMapType +

+

+(Appears on:HelmValuesMappingItem) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+replicaCount
+ +string + +
+(Optional) +

replicaCount sets the replicaCount value mapping key.


+
+persistentVolumeEnabled
+ +string + +
+(Optional) +

persistentVolumeEnabled sets the persistent volume enabled mapping key.


+
+storageClass
+ +string + +
+(Optional) +

storageClass sets the storageClass mapping key.


+
+

HelmValuesMapping +

+

+(Appears on:HelmTypeInstallSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+HelmValuesMappingItem
+ + +HelmValuesMappingItem + + +
+

+(Members of HelmValuesMappingItem are embedded into this type.) +

+
+extras
+ + +[]HelmValuesMappingExtraItem + + +
+(Optional) +

Helm value mapping items for extra items.


+
+

HelmValuesMappingExtraItem +

+

+(Appears on:HelmValuesMapping) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+HelmValuesMappingItem
+ + +HelmValuesMappingItem + + +
+

+(Members of HelmValuesMappingItem are embedded into this type.) +

+
+name
+ +string + +
+

Name of the item.


+
+

HelmValuesMappingItem +

+

+(Appears on:HelmValuesMapping, HelmValuesMappingExtraItem) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+valueMap
+ + +HelmValueMapType + + +
+(Optional) +

valueMap define the “key” mapping values. Valid keys are replicaCount,
persistentVolumeEnabled, and storageClass. Enum values explained:
"replicaCount" sets the replicaCount value mapping key.
"persistentVolumeEnabled" sets the persistent volume enabled mapping key.
"storageClass" sets the storageClass mapping key.


+
+jsonMap
+ + +HelmJSONValueMapType + + +
+(Optional) +

jsonMap defines the “key” mapping values. The valid key is tolerations.
Enum values explained:
"tolerations" sets the toleration mapping key.


+
+resources
+ + +ResourceMappingItem + + +
+(Optional) +

resources sets resources related mapping keys.


+
+

InstallableSpec +

+

+(Appears on:AddonSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+selectors
+ + +[]SelectorRequirement + + +
+(Optional) +

Add-on installable selectors. If multiple selectors are provided,
all selectors must evaluate to true.


+
+autoInstall
+ +bool + +
+

autoInstall defines an add-on should be installed automatically.


+
+

LineSelectorOperator +(string alias)

+

+(Appears on:SelectorRequirement) +

+
+

LineSelectorOperator defines line selector operators.


+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Contains"

"DoesNotContain"

"DoesNotMatchRegex"

"MatchRegex"

+

ResourceMappingItem +

+

+(Appears on:HelmValuesMappingItem) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+storage
+ +string + +
+(Optional) +

storage sets the storage size value mapping key.


+
+cpu
+ + +ResourceReqLimItem + + +
+(Optional) +

cpu sets CPU requests and limits mapping keys.


+
+memory
+ + +ResourceReqLimItem + + +
+(Optional) +

memory sets Memory requests and limits mapping keys.


+
+

ResourceReqLimItem +

+

+(Appears on:ResourceMappingItem) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+requests
+ +string + +
+(Optional) +

Requests value mapping key.


+
+limits
+ +string + +
+(Optional) +

Limits value mapping key.


+
+

ResourceRequirements +

+

+(Appears on:AddonInstallSpecItem) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+limits
+ + +Kubernetes core/v1.ResourceList + + +
+(Optional) +

Limits describes the maximum amount of compute resources allowed.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.


+
+requests
+ + +Kubernetes core/v1.ResourceList + + +
+(Optional) +

Requests describes the minimum amount of compute resources required.
If Requests is omitted for a container, it defaults to Limits if that is explicitly specified;
otherwise, it defaults to an implementation-defined value.
More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/.


+
+

SelectorRequirement +

+

+(Appears on:AddonDefaultInstallSpecItem, InstallableSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+key
+ + +AddonSelectorKey + + +
+

The selector key. Valid values are KubeVersion, KubeGitVersion.
“KubeVersion” the semver expression of Kubernetes versions, i.e., v1.24.
“KubeGitVersion” may contain distro. info., i.e., v1.24.4+eks.


+
+operator
+ + +LineSelectorOperator + + +
+

Represents a key’s relationship to a set of values.
Valid operators are Contains, NotIn, DoesNotContain, MatchRegex, and DoesNoteMatchRegex.



Possible enum values:
"Contains" line contains a string.
"DoesNotContain" line does not contain a string.
"MatchRegex" line contains a match to the regular expression.
"DoesNotMatchRegex" line does not contain a match to the regular expression.


+
+values
+ +[]string + +
+(Optional) +

An array of string values. It serves as an “OR” expression to the operator.


+
+
+

+Generated with gen-crd-api-reference-docs +

diff --git a/docs/user_docs/api-reference/backup.md b/docs/user_docs/api-reference/backup.md new file mode 100644 index 000000000..f4cb4936a --- /dev/null +++ b/docs/user_docs/api-reference/backup.md @@ -0,0 +1,2381 @@ +--- +title: Backup API Reference +description: Backup API Reference +keywords: [backup, api] +sidebar_position: 2 +sidebar_label: Backup +--- +
+

Packages:

+ +

dataprotection.kubeblocks.io/v1alpha1

+
+
+Resource Types: + +

Backup +

+
+

Backup is the Schema for the backups API (defined by User).


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+dataprotection.kubeblocks.io/v1alpha1 +
+kind
+string +
Backup
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +BackupSpec + + +
+
+
+ + + + + + + + + + + + + +
+backupPolicyName
+ +string + +
+

Which backupPolicy is applied to perform this backup


+
+backupType
+ + +BackupType + + +
+

Backup Type. datafile or logfile or snapshot. If not set, datafile is the default type.


+
+parentBackupName
+ +string + +
+(Optional) +

if backupType is incremental, parentBackupName is required.


+
+
+status
+ + +BackupStatus + + +
+
+

BackupPolicy +

+
+

BackupPolicy is the Schema for the backuppolicies API (defined by User)


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+dataprotection.kubeblocks.io/v1alpha1 +
+kind
+string +
BackupPolicy
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +BackupPolicySpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+retention
+ + +RetentionSpec + + +
+(Optional) +

retention describe how long the Backup should be retained. if not set, will be retained forever.


+
+schedule
+ + +Schedule + + +
+(Optional) +

schedule policy for backup.


+
+snapshot
+ + +SnapshotPolicy + + +
+(Optional) +

the policy for snapshot backup.


+
+datafile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for datafile backup.


+
+logfile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for logfile backup.


+
+
+status
+ + +BackupPolicyStatus + + +
+
+

BackupTool +

+
+

BackupTool is the Schema for the backuptools API (defined by provider)


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+dataprotection.kubeblocks.io/v1alpha1 +
+kind
+string +
BackupTool
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +BackupToolSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+image
+ +string + +
+

Backup tool Container image name.


+
+deployKind
+ +string + +
+

which kind for run a backup tool.


+
+type
+ +string + +
+

the type of backup tool, file or pitr


+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

Compute Resources required by this container.
Cannot be updated.


+
+env
+ + +[]Kubernetes core/v1.EnvVar + + +
+(Optional) +

List of environment variables to set in the container.


+
+envFrom
+ + +[]Kubernetes core/v1.EnvFromSource + + +
+(Optional) +

List of sources to populate environment variables in the container.
The keys defined within a source must be a C_IDENTIFIER. All invalid keys
will be reported as an event when the container is starting. When a key exists in multiple
sources, the value associated with the last source will take precedence.
Values defined by an Env with a duplicate key will take precedence.
Cannot be updated.


+
+backupCommands
+ +[]string + +
+

Array of command that apps can do database backup.
from invoke args
the order of commands follows the order of array.


+
+incrementalBackupCommands
+ +[]string + +
+(Optional) +

Array of command that apps can do database incremental backup.
like xtrabackup, that can performs an incremental backup file.


+
+physical
+ + +BackupToolRestoreCommand + + +
+

backup tool can support physical restore, in this case, restore must be RESTART database.


+
+logical
+ + +BackupToolRestoreCommand + + +
+(Optional) +

backup tool can support logical restore, in this case, restore NOT RESTART database.


+
+
+status
+ + +BackupToolStatus + + +
+
+

RestoreJob +

+
+

RestoreJob is the Schema for the restorejobs API (defined by User)


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+dataprotection.kubeblocks.io/v1alpha1 +
+kind
+string +
RestoreJob
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +RestoreJobSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + +
+backupJobName
+ +string + +
+

Specified one backupJob to restore.


+
+target
+ + +TargetCluster + + +
+

the target database workload to restore


+
+targetVolumes
+ + +[]Kubernetes core/v1.Volume + + +
+

array of restore volumes .


+
+targetVolumeMounts
+ + +[]Kubernetes core/v1.VolumeMount + + +
+

array of restore volume mounts .


+
+onFailAttempted
+ +int32 + +
+(Optional) +

count of backup stop retries on fail.


+
+
+status
+ + +RestoreJobStatus + + +
+
+

BackupLogStatus +

+

+(Appears on:ManifestsStatus) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+startTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

startTime records the start time of data logging.


+
+stopTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

stopTime records the stop time of data logging.


+
+

BackupPhase +(string alias)

+

+(Appears on:BackupStatus) +

+
+

BackupPhase The current phase. Valid values are New, InProgress, Completed, Failed.


+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Completed"

"Failed"

"InProgress"

"New"

+

BackupPolicyHook +

+

+(Appears on:SnapshotPolicy) +

+
+

BackupPolicyHook defines for the database execute commands before and after backup.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+preCommands
+ +[]string + +
+(Optional) +

pre backup to perform commands


+
+postCommands
+ +[]string + +
+(Optional) +

post backup to perform commands


+
+image
+ +string + +
+(Optional) +

exec command with image


+
+containerName
+ +string + +
+(Optional) +

which container can exec command


+
+

BackupPolicyPhase +(string alias)

+

+(Appears on:BackupPolicyStatus) +

+
+

BackupPolicyPhase defines phases for BackupPolicy CR.


+
+ + + + + + + + + + + + +
ValueDescription

"Available"

"Failed"

+

BackupPolicySecret +

+

+(Appears on:TargetCluster) +

+
+

BackupPolicySecret defines for the target database secret that backup tool can connect.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

the secret name


+
+usernameKey
+ +string + +
+

usernameKey the map key of the user in the connection credential secret


+
+passwordKey
+ +string + +
+

passwordKey the map key of the password in the connection credential secret


+
+

BackupPolicySpec +

+

+(Appears on:BackupPolicy) +

+
+

BackupPolicySpec defines the desired state of BackupPolicy


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+retention
+ + +RetentionSpec + + +
+(Optional) +

retention describe how long the Backup should be retained. if not set, will be retained forever.


+
+schedule
+ + +Schedule + + +
+(Optional) +

schedule policy for backup.


+
+snapshot
+ + +SnapshotPolicy + + +
+(Optional) +

the policy for snapshot backup.


+
+datafile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for datafile backup.


+
+logfile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for logfile backup.


+
+

BackupPolicyStatus +

+

+(Appears on:BackupPolicy) +

+
+

BackupPolicyStatus defines the observed state of BackupPolicy


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the most recent generation observed for this
BackupPolicy. It corresponds to the Cluster’s generation, which is
updated on mutation by the API Server.


+
+phase
+ + +BackupPolicyPhase + + +
+(Optional) +

backup policy phase valid value: Available, Failed.


+
+failureReason
+ +string + +
+(Optional) +

the reason if backup policy check failed.


+
+lastScheduleTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

information when was the last time the job was successfully scheduled.


+
+lastSuccessfulTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

information when was the last time the job successfully completed.


+
+

BackupSnapshotStatus +

+

+(Appears on:ManifestsStatus) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+volumeSnapshotName
+ +string + +
+(Optional) +

volumeSnapshotName records the volumeSnapshot name.


+
+volumeSnapshotContentName
+ +string + +
+(Optional) +

volumeSnapshotContentName specifies the name of a pre-existing VolumeSnapshotContent
object representing an existing volume snapshot.
This field should be set if the snapshot already exists and only needs a representation in Kubernetes.
This field is immutable.


+
+

BackupSpec +

+

+(Appears on:Backup) +

+
+

BackupSpec defines the desired state of Backup.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+backupPolicyName
+ +string + +
+

Which backupPolicy is applied to perform this backup


+
+backupType
+ + +BackupType + + +
+

Backup Type. datafile or logfile or snapshot. If not set, datafile is the default type.


+
+parentBackupName
+ +string + +
+(Optional) +

if backupType is incremental, parentBackupName is required.


+
+

BackupStatus +

+

+(Appears on:Backup) +

+
+

BackupStatus defines the observed state of Backup.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +BackupPhase + + +
+(Optional) +
+parentBackupName
+ +string + +
+(Optional) +

Records parentBackupName if backupType is incremental.


+
+expiration
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

The date and time when the Backup is eligible for garbage collection.
‘null’ means the Backup is NOT be cleaned except delete manual.


+
+startTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

Date/time when the backup started being processed.


+
+completionTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

Date/time when the backup finished being processed.


+
+duration
+ + +Kubernetes meta/v1.Duration + + +
+(Optional) +

The duration time of backup execution.
When converted to a string, the form is “1h2m0.5s”.


+
+totalSize
+ +string + +
+(Optional) +

Backup total size.
A string with capacity units in the form of “1Gi”, “1Mi”, “1Ki”.


+
+failureReason
+ +string + +
+(Optional) +

The reason for a backup failure.


+
+persistentVolumeClaimName
+ +string + +
+(Optional) +

remoteVolume saves the backup data.


+
+backupToolName
+ +string + +
+(Optional) +

backupToolName references the backup tool name.


+
+manifests
+ + +ManifestsStatus + + +
+(Optional) +

manifests determines the backup metadata info.


+
+

BackupStatusUpdate +

+

+(Appears on:BasePolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+(Optional) +

specify the json path of backup object for patch.
example: manifests.backupLog – means patch the backup json path of status.manifests.backupLog.


+
+containerName
+ +string + +
+(Optional) +

which container name that kubectl can execute.


+
+script
+ +string + +
+(Optional) +

the shell Script commands to collect backup status metadata.
The script must exist in the container of ContainerName and the output format must be set to JSON.
Note that outputting to stderr may cause the result format to not be in JSON.


+
+updateStage
+ + +BackupStatusUpdateStage + + +
+(Optional) +

when to update the backup status, pre: before backup, post: after backup


+
+

BackupStatusUpdateStage +(string alias)

+

+(Appears on:BackupStatusUpdate) +

+
+

BackupStatusUpdateStage defines the stage of backup status update.


+
+ + + + + + + + + + + + +
ValueDescription

"post"

"pre"

+

BackupToolManifestsStatus +

+

+(Appears on:ManifestsStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+filePath
+ +string + +
+(Optional) +

filePath records the file path of backup.


+
+uploadTotalSize
+ +string + +
+(Optional) +

Backup upload total size.
A string with capacity units in the form of “1Gi”, “1Mi”, “1Ki”.


+
+checksum
+ +string + +
+(Optional) +

checksum of backup file, generated by md5 or sha1 or sha256.


+
+checkpoint
+ +string + +
+(Optional) +

backup checkpoint, for incremental backup.


+
+

BackupToolRestoreCommand +

+

+(Appears on:BackupToolSpec) +

+
+

BackupToolRestoreCommand defines the restore commands of BackupTool


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+restoreCommands
+ +[]string + +
+(Optional) +

Array of command that apps can perform database restore.
like xtrabackup, that can performs restore mysql from files.


+
+incrementalRestoreCommands
+ +[]string + +
+(Optional) +

Array of incremental restore commands.


+
+

BackupToolSpec +

+

+(Appears on:BackupTool) +

+
+

BackupToolSpec defines the desired state of BackupTool


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+image
+ +string + +
+

Backup tool Container image name.


+
+deployKind
+ +string + +
+

which kind for run a backup tool.


+
+type
+ +string + +
+

the type of backup tool, file or pitr


+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

Compute Resources required by this container.
Cannot be updated.


+
+env
+ + +[]Kubernetes core/v1.EnvVar + + +
+(Optional) +

List of environment variables to set in the container.


+
+envFrom
+ + +[]Kubernetes core/v1.EnvFromSource + + +
+(Optional) +

List of sources to populate environment variables in the container.
The keys defined within a source must be a C_IDENTIFIER. All invalid keys
will be reported as an event when the container is starting. When a key exists in multiple
sources, the value associated with the last source will take precedence.
Values defined by an Env with a duplicate key will take precedence.
Cannot be updated.


+
+backupCommands
+ +[]string + +
+

Array of command that apps can do database backup.
from invoke args
the order of commands follows the order of array.


+
+incrementalBackupCommands
+ +[]string + +
+(Optional) +

Array of command that apps can do database incremental backup.
like xtrabackup, that can performs an incremental backup file.


+
+physical
+ + +BackupToolRestoreCommand + + +
+

backup tool can support physical restore, in this case, restore must be RESTART database.


+
+logical
+ + +BackupToolRestoreCommand + + +
+(Optional) +

backup tool can support logical restore, in this case, restore NOT RESTART database.


+
+

BackupToolStatus +

+

+(Appears on:BackupTool) +

+
+

BackupToolStatus defines the observed state of BackupTool


+
+

BackupType +(string alias)

+

+(Appears on:BackupSpec) +

+
+

BackupType the backup type, marked backup set is datafile or logfile or snapshot.


+
+ + + + + + + + + + + + + + +
ValueDescription

"datafile"

"logfile"

"snapshot"

+

BaseBackupType +(string alias)

+
+

BaseBackupType the base backup type.


+
+

BasePolicy +

+

+(Appears on:CommonBackupPolicy, SnapshotPolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+target
+ + +TargetCluster + + +
+

target database cluster for backup.


+
+backupsHistoryLimit
+ +int32 + +
+(Optional) +

the number of automatic backups to retain. Value must be non-negative integer.
0 means NO limit on the number of backups.


+
+onFailAttempted
+ +int32 + +
+(Optional) +

count of backup stop retries on fail.


+
+backupStatusUpdates
+ + +[]BackupStatusUpdate + + +
+(Optional) +

define how to update metadata for backup status.


+
+

CommonBackupPolicy +

+

+(Appears on:BackupPolicySpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+BasePolicy
+ + +BasePolicy + + +
+

+(Members of BasePolicy are embedded into this type.) +

+
+persistentVolumeClaim
+ + +PersistentVolumeClaim + + +
+

refer to PersistentVolumeClaim and the backup data will be stored in the corresponding persistent volume.


+
+backupToolName
+ +string + +
+

which backup tool to perform database backup, only support one tool.


+
+

CreatePVCPolicy +(string alias)

+

+(Appears on:PersistentVolumeClaim) +

+
+

CreatePVCPolicy the policy how to create the PersistentVolumeClaim for backup.


+
+ + + + + + + + + + + + +
ValueDescription

"IfNotPresent"

"Never"

+

ManifestsStatus +

+

+(Appears on:BackupStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+backupLog
+ + +BackupLogStatus + + +
+(Optional) +

backupLog records startTime and stopTime of data logging.


+
+target
+ +string + +
+(Optional) +

target records the target cluster metadata string, which is in JSON format.


+
+backupSnapshot
+ + +BackupSnapshotStatus + + +
+(Optional) +

snapshot records the volume snapshot metadata.


+
+backupTool
+ + +BackupToolManifestsStatus + + +
+(Optional) +

backupTool records information about backup files generated by the backup tool.


+
+userContext
+ +map[string]string + +
+(Optional) +

userContext stores some loosely structured and extensible information.


+
+

PersistentVolumeClaim +

+

+(Appears on:CommonBackupPolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

the name of the PersistentVolumeClaim.


+
+storageClassName
+ +string + +
+(Optional) +

storageClassName is the name of the StorageClass required by the claim.


+
+initCapacity
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

initCapacity represents the init storage size of the PersistentVolumeClaim which should be created if not exist.
and the default value is 100Gi if it is empty.


+
+createPolicy
+ + +CreatePVCPolicy + + +
+(Optional) +

createPolicy defines the policy for creating the PersistentVolumeClaim, enum values:
- Never: do nothing if the PersistentVolumeClaim not exists.
- IfNotPresent: create the PersistentVolumeClaim if not present and the accessModes only contains ‘ReadWriteMany’.


+
+persistentVolumeConfigMap
+ + +PersistentVolumeConfigMap + + +
+(Optional) +

persistentVolumeConfigMap references the configmap which contains a persistentVolume template.
key must be “persistentVolume” and value is the “PersistentVolume” struct.
support the following built-in Objects:
- $(GENERATE_NAME): generate a specific format “pvcName-pvcNamespace”.
if the PersistentVolumeClaim not exists and CreatePolicy is “IfNotPresent”, the controller
will create it by this template. this is a mutually exclusive setting with “storageClassName”.


+
+

PersistentVolumeConfigMap +

+

+(Appears on:PersistentVolumeClaim) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

the name of the persistentVolume ConfigMap.


+
+namespace
+ +string + +
+

the namespace of the persistentVolume ConfigMap.


+
+

RestoreJobPhase +(string alias)

+

+(Appears on:RestoreJobStatus) +

+
+

RestoreJobPhase The current phase. Valid values are New, InProgressPhy, InProgressLogic, Completed, Failed.


+
+ + + + + + + + + + + + + + + + + + +
ValueDescription

"Completed"

"Failed"

"InProgressLogic"

"InProgressPhy"

"New"

+

RestoreJobSpec +

+

+(Appears on:RestoreJob) +

+
+

RestoreJobSpec defines the desired state of RestoreJob


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+backupJobName
+ +string + +
+

Specified one backupJob to restore.


+
+target
+ + +TargetCluster + + +
+

the target database workload to restore


+
+targetVolumes
+ + +[]Kubernetes core/v1.Volume + + +
+

array of restore volumes .


+
+targetVolumeMounts
+ + +[]Kubernetes core/v1.VolumeMount + + +
+

array of restore volume mounts .


+
+onFailAttempted
+ +int32 + +
+(Optional) +

count of backup stop retries on fail.


+
+

RestoreJobStatus +

+

+(Appears on:RestoreJob) +

+
+

RestoreJobStatus defines the observed state of RestoreJob


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +RestoreJobPhase + + +
+(Optional) +
+expiration
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

The date and time when the Backup is eligible for garbage collection.
‘null’ means the Backup is NOT be cleaned except delete manual.


+
+startTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

Date/time when the backup started being processed.


+
+completionTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

Date/time when the backup finished being processed.


+
+failureReason
+ +string + +
+(Optional) +

Job failed reason.


+
+

RetentionSpec +

+

+(Appears on:BackupPolicySpec) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+ttl
+ +string + +
+(Optional) +

ttl is a time string ending with the ’d’|’D’|‘h’|‘H’ character to describe how long
the Backup should be retained. if not set, will be retained forever.


+
+

Schedule +

+

+(Appears on:BackupPolicySpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+snapshot
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for snapshot backup.


+
+datafile
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for datafile backup.


+
+logfile
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for logfile backup.


+
+

SchedulePolicy +

+

+(Appears on:Schedule) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+cronExpression
+ +string + +
+

the cron expression for schedule, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron.


+
+enable
+ +bool + +
+

enable or disable the schedule.


+
+

SnapshotPolicy +

+

+(Appears on:BackupPolicySpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+BasePolicy
+ + +BasePolicy + + +
+

+(Members of BasePolicy are embedded into this type.) +

+
+hooks
+ + +BackupPolicyHook + + +
+(Optional) +

execute hook commands for backup.


+
+

TargetCluster +

+

+(Appears on:BasePolicy, RestoreJobSpec) +

+
+

TargetCluster TODO (dsj): target cluster need redefined from Cluster API


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+labelsSelector
+ + +Kubernetes meta/v1.LabelSelector + + +
+

labelsSelector is used to find matching pods.
Pods that match this label selector are counted to determine the number of pods
in their corresponding topology domain.


+
+secret
+ + +BackupPolicySecret + + +
+(Optional) +

secret is used to connect to the target database cluster.
If not set, secret will be inherited from backup policy template.
if still not set, the controller will check if any system account for dataprotection has been created.


+
+
+

+Generated with gen-crd-api-reference-docs +

diff --git a/docs/user_docs/api-reference/cluster.md b/docs/user_docs/api-reference/cluster.md new file mode 100644 index 000000000..e2012d64d --- /dev/null +++ b/docs/user_docs/api-reference/cluster.md @@ -0,0 +1,8509 @@ +--- +title: Cluster API Reference +description: Cluster API Reference +keywords: [cluster, api] +sidebar_position: 1 +sidebar_label: Cluster +--- +
+

Packages:

+ +

apps.kubeblocks.io/v1alpha1

+
+
+Resource Types: + +

BackupPolicyTemplate +

+
+

BackupPolicyTemplate is the Schema for the BackupPolicyTemplates API (defined by provider)


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
BackupPolicyTemplate
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +BackupPolicyTemplateSpec + + +
+
+
+ + + + + + + + + + + + + +
+clusterDefinitionRef
+ +string + +
+

clusterDefinitionRef references ClusterDefinition name, this is an immutable attribute.


+
+backupPolicies
+ + +[]BackupPolicy + + +
+

backupPolicies is a list of backup policy template for the specified componentDefinition.


+
+identifier
+ +string + +
+(Optional) +

Identifier is a unique identifier for this BackupPolicyTemplate.
this identifier will be the suffix of the automatically generated backupPolicy name.
and must be added when multiple BackupPolicyTemplates exist,
otherwise the generated backupPolicy override will occur.


+
+
+status
+ + +BackupPolicyTemplateStatus + + +
+
+

Cluster +

+
+

Cluster is the Schema for the clusters API.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
Cluster
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ClusterSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+clusterDefinitionRef
+ +string + +
+

Cluster referencing ClusterDefinition name. This is an immutable attribute.


+
+clusterVersionRef
+ +string + +
+(Optional) +

Cluster referencing ClusterVersion name.


+
+terminationPolicy
+ + +TerminationPolicyType + + +
+

Cluster termination policy. Valid values are DoNotTerminate, Halt, Delete, WipeOut.
DoNotTerminate will block delete operation.
Halt will delete workload resources such as statefulset, deployment workloads but keep PVCs.
Delete is based on Halt and deletes PVCs.
WipeOut is based on Delete and wipe out all volume snapshots and snapshot data from backup storage location.


+
+componentSpecs
+ + +[]ClusterComponentSpec + + +
+

List of componentSpecs you want to replace in ClusterDefinition and ClusterVersion. It will replace the field in ClusterDefinition’s and ClusterVersion’s component if type is matching.


+
+affinity
+ + +Affinity + + +
+(Optional) +

affinity is a group of affinity scheduling rules.


+
+tolerations
+ + +[]Kubernetes core/v1.Toleration + + +
+(Optional) +

tolerations are attached to tolerate any taint that matches the triple key,value,effect using the matching operator operator.


+
+
+status
+ + +ClusterStatus + + +
+
+

ClusterDefinition +

+
+

ClusterDefinition is the Schema for the clusterdefinitions API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
ClusterDefinition
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ClusterDefinitionSpec + + +
+
+
+ + + + + + + + + + + + + +
+type
+ +string + +
+(Optional) +

Cluster definition type defines well known application cluster type, e.g. mysql/redis/mongodb


+
+componentDefs
+ + +[]ClusterComponentDefinition + + +
+

componentDefs provides cluster components definitions.


+
+connectionCredential
+ +map[string]string + +
+(Optional) +

Connection credential template used for creating a connection credential
secret for cluster.apps.kubeblocks.io object. Built-in objects are:
$(RANDOM_PASSWD) - random 8 characters.
$(UUID) - generate a random UUID v4 string.
$(UUID_B64) - generate a random UUID v4 BASE64 encoded string.
$(UUID_STR_B64) - generate a random UUID v4 string then BASE64 encoded.
$(UUID_HEX) - generate a random UUID v4 HEX representation.
$(HEADLESS_SVC_FQDN) - headless service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc,
where 1ST_COMP_NAME is the 1st component that provide ClusterDefinition.spec.componentDefs[].service attribute;
$(SVC_FQDN) - service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc,
where 1ST_COMP_NAME is the 1st component that provide ClusterDefinition.spec.componentDefs[].service attribute;
$(SVC_PORT_{PORT-NAME}) - a ServicePort’s port value with specified port name, i.e, a servicePort JSON struct:
"name": "mysql", "targetPort": "mysqlContainerPort", "port": 3306, and “$(SVC_PORT_mysql)” in the
connection credential value is 3306.


+
+
+status
+ + +ClusterDefinitionStatus + + +
+
+

ClusterVersion +

+
+

ClusterVersion is the Schema for the ClusterVersions API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
ClusterVersion
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ClusterVersionSpec + + +
+
+
+ + + + + + + + + +
+clusterDefinitionRef
+ +string + +
+

ref ClusterDefinition.


+
+componentVersions
+ + +[]ClusterComponentVersion + + +
+

List of components’ containers versioning context, i.e., container image ID, container commands, args., and environments.


+
+
+status
+ + +ClusterVersionStatus + + +
+
+

ComponentClassDefinition +

+
+

ComponentClassDefinition is the Schema for the componentclassdefinitions API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
ComponentClassDefinition
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ComponentClassDefinitionSpec + + +
+
+
+ + + + + +
+groups
+ + +[]ComponentClassGroup + + +
+(Optional) +

group defines a list of class series that conform to the same constraint.


+
+
+status
+ + +ComponentClassDefinitionStatus + + +
+
+

ComponentResourceConstraint +

+
+

ComponentResourceConstraint is the Schema for the componentresourceconstraints API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
ComponentResourceConstraint
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ComponentResourceConstraintSpec + + +
+
+
+ + + + + +
+constraints
+ + +[]ResourceConstraint + + +
+

Component resource constraints


+
+
+

ConfigConstraint +

+
+

ConfigConstraint is the Schema for the configconstraint API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
ConfigConstraint
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +ConfigConstraintSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+reloadOptions
+ + +ReloadOptions + + +
+(Optional) +

reloadOptions indicates whether the process supports reload.
if set, the controller will determine the behavior of the engine instance based on the configuration templates,
restart or reload depending on whether any parameters in the StaticParameters have been modified.


+
+cfgSchemaTopLevelName
+ +string + +
+(Optional) +

cfgSchemaTopLevelName is cue type name, which generates openapi schema.


+
+configurationSchema
+ + +CustomParametersValidation + + +
+(Optional) +

configurationSchema imposes restrictions on database parameter’s rule.


+
+staticParameters
+ +[]string + +
+(Optional) +

staticParameters, list of StaticParameter, modifications of them trigger a process restart.


+
+dynamicParameters
+ +[]string + +
+(Optional) +

dynamicParameters, list of DynamicParameter, modifications of them trigger a config dynamic reload without process restart.


+
+immutableParameters
+ +[]string + +
+(Optional) +

immutableParameters describes parameters that prohibit user from modification.


+
+selector
+ + +Kubernetes meta/v1.LabelSelector + + +
+

selector is used to match the label on the pod,
for example, a pod of the primary is match on the patroni cluster.


+
+formatterConfig
+ + +FormatterConfig + + +
+

formatterConfig describes the format of the configuration file, the controller
1. parses configuration file
2. analyzes the modified parameters
3. applies corresponding policies.


+
+
+status
+ + +ConfigConstraintStatus + + +
+
+

OpsRequest +

+
+

OpsRequest is the Schema for the opsrequests API


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+apiVersion
+string
+apps.kubeblocks.io/v1alpha1 +
+kind
+string +
OpsRequest
+metadata
+ + +Kubernetes meta/v1.ObjectMeta + + +
+Refer to the Kubernetes API documentation for the fields of the +metadata field. +
+spec
+ + +OpsRequestSpec + + +
+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+clusterRef
+ +string + +
+

clusterRef references clusterDefinition.


+
+cancel
+ +bool + +
+(Optional) +

cancel defines the action to cancel the Pending/Creating/Running opsRequest, supported types: [VerticalScaling, HorizontalScaling].
once cancel is set to true, this opsRequest will be canceled and modifying this property again will not take effect.


+
+type
+ + +OpsType + + +
+

type defines the operation type.


+
+ttlSecondsAfterSucceed
+ +int32 + +
+(Optional) +

ttlSecondsAfterSucceed OpsRequest will be deleted after TTLSecondsAfterSucceed second when OpsRequest.status.phase is Succeed.


+
+upgrade
+ + +Upgrade + + +
+(Optional) +

upgrade specifies the cluster version by specifying clusterVersionRef.


+
+horizontalScaling
+ + +[]HorizontalScaling + + +
+(Optional) +

horizontalScaling defines what component need to horizontal scale the specified replicas.


+
+volumeExpansion
+ + +[]VolumeExpansion + + +
+(Optional) +

volumeExpansion defines what component and volumeClaimTemplate need to expand the specified storage.


+
+restart
+ + +[]ComponentOps + + +
+(Optional) +

restart the specified component.


+
+verticalScaling
+ + +[]VerticalScaling + + +
+(Optional) +

verticalScaling defines what component need to vertical scale the specified compute resources.


+
+reconfigure
+ + +Reconfigure + + +
+(Optional) +

reconfigure defines the variables that need to input when updating configuration.


+
+expose
+ + +[]Expose + + +
+(Optional) +

expose defines services the component needs to expose.


+
+restoreFrom
+ + +RestoreFromSpec + + +
+(Optional) +

cluster RestoreFrom backup or point in time


+
+
+status
+ + +OpsRequestStatus + + +
+
+

AccessMode +(string alias)

+

+(Appears on:ConsensusMember, ConsensusMemberStatus) +

+
+

AccessMode defines SVC access mode enums.


+
+ + + + + + + + + + + + + + +
ValueDescription

"None"

"ReadWrite"

"Readonly"

+

AccountName +(string alias)

+

+(Appears on:SystemAccountConfig) +

+
+

AccountName defines system account names.


+
+ + + + + + + + + + + + + + + + + + +
ValueDescription

"kbadmin"

"kbdataprotection"

"kbmonitoring"

"kbprobe"

"kbreplicator"

+

Affinity +

+

+(Appears on:ClusterComponentSpec, ClusterSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+podAntiAffinity
+ + +PodAntiAffinity + + +
+(Optional) +

podAntiAffinity describes the anti-affinity level of pods within a component.
Preferred means try spread pods by TopologyKeys.
Required means must spread pods by TopologyKeys.


+
+topologyKeys
+ +[]string + +
+(Optional) +

topologyKey is the key of node labels.
Nodes that have a label with this key and identical values are considered to be in the same topology.
It’s used as the topology domain for pod anti-affinity and pod spread constraint.
Some well-known label keys, such as “kubernetes.io/hostname” and “topology.kubernetes.io/zone”
are often used as TopologyKey, as well as any other custom label key.


+
+nodeLabels
+ +map[string]string + +
+(Optional) +

nodeLabels describes that pods must be scheduled to the nodes with the specified node labels.


+
+tenancy
+ + +TenancyType + + +
+(Optional) +

tenancy describes how pods are distributed across node.
SharedNode means multiple pods may share the same node.
DedicatedNode means each pod runs on their own dedicated node.


+
+

BackupPolicy +

+

+(Appears on:BackupPolicyTemplateSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+componentDefRef
+ +string + +
+

componentDefRef references componentDef defined in ClusterDefinition spec.


+
+retention
+ + +RetentionSpec + + +
+(Optional) +

retention describe how long the Backup should be retained. if not set, will be retained forever.


+
+schedule
+ + +Schedule + + +
+(Optional) +

schedule policy for backup.


+
+snapshot
+ + +SnapshotPolicy + + +
+(Optional) +

the policy for snapshot backup.


+
+datafile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for datafile backup.


+
+logfile
+ + +CommonBackupPolicy + + +
+(Optional) +

the policy for logfile backup.


+
+

BackupPolicyHook +

+

+(Appears on:SnapshotPolicy) +

+
+

BackupPolicyHook defines for the database execute commands before and after backup.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+preCommands
+ +[]string + +
+(Optional) +

pre backup to perform commands


+
+postCommands
+ +[]string + +
+(Optional) +

post backup to perform commands


+
+image
+ +string + +
+(Optional) +

exec command with image


+
+containerName
+ +string + +
+(Optional) +

which container can exec command


+
+

BackupPolicyTemplateSpec +

+

+(Appears on:BackupPolicyTemplate) +

+
+

BackupPolicyTemplateSpec defines the desired state of BackupPolicyTemplate


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+clusterDefinitionRef
+ +string + +
+

clusterDefinitionRef references ClusterDefinition name, this is an immutable attribute.


+
+backupPolicies
+ + +[]BackupPolicy + + +
+

backupPolicies is a list of backup policy template for the specified componentDefinition.


+
+identifier
+ +string + +
+(Optional) +

Identifier is a unique identifier for this BackupPolicyTemplate.
this identifier will be the suffix of the automatically generated backupPolicy name.
and must be added when multiple BackupPolicyTemplates exist,
otherwise the generated backupPolicy override will occur.


+
+

BackupPolicyTemplateStatus +

+

+(Appears on:BackupPolicyTemplate) +

+
+

BackupPolicyTemplateStatus defines the observed state of BackupPolicyTemplate


+
+

BackupRefSpec +

+

+(Appears on:RestoreFromSpec) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+ref
+ + +RefNamespaceName + + +
+(Optional) +

specify a reference backup to restore


+
+

BackupStatusUpdate +

+

+(Appears on:BasePolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+path
+ +string + +
+(Optional) +

specify the json path of backup object for patch.
example: manifests.backupLog – means patch the backup json path of status.manifests.backupLog.


+
+containerName
+ +string + +
+(Optional) +

which container name that kubectl can execute.


+
+script
+ +string + +
+(Optional) +

the shell Script commands to collect backup status metadata.
The script must exist in the container of ContainerName and the output format must be set to JSON.
Note that outputting to stderr may cause the result format to not be in JSON.


+
+updateStage
+ + +BackupStatusUpdateStage + + +
+(Optional) +

when to update the backup status, pre: before backup, post: after backup


+
+

BackupStatusUpdateStage +(string alias)

+

+(Appears on:BackupStatusUpdate) +

+
+

BackupStatusUpdateStage defines the stage of backup status update.


+
+

BaseBackupType +(string alias)

+
+

BaseBackupType the base backup type, keep synchronized with the BaseBackupType of the data protection API.


+
+

BasePolicy +

+

+(Appears on:CommonBackupPolicy, SnapshotPolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+target
+ + +TargetInstance + + +
+(Optional) +

target instance for backup.


+
+backupsHistoryLimit
+ +int32 + +
+(Optional) +

the number of automatic backups to retain. Value must be non-negative integer.
0 means NO limit on the number of backups.


+
+onFailAttempted
+ +int32 + +
+(Optional) +

count of backup stop retries on fail.


+
+backupStatusUpdates
+ + +[]BackupStatusUpdate + + +
+(Optional) +

define how to update metadata for backup status.


+
+

CPUConstraint +

+

+(Appears on:ResourceConstraint) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+max
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The maximum count of vcpu cores, [Min, Max] defines a range for valid vcpu cores, and the value in this range
must be multiple times of Step. It’s useful to define a large number of valid values without defining them one by
one. Please see the documentation for Step for some examples.
If Slots is specified, Max, Min, and Step are ignored


+
+min
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The minimum count of vcpu cores, [Min, Max] defines a range for valid vcpu cores, and the value in this range
must be multiple times of Step. It’s useful to define a large number of valid values without defining them one by
one. Please see the documentation for Step for some examples.
If Slots is specified, Max, Min, and Step are ignored


+
+step
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The minimum granularity of vcpu cores, [Min, Max] defines a range for valid vcpu cores and the value in this range must be
multiple times of Step.
For example:
1. Min is 2, Max is 8, Step is 2, and the valid vcpu core is {2, 4, 6, 8}.
2. Min is 0.5, Max is 2, Step is 0.5, and the valid vcpu core is {0.5, 1, 1.5, 2}.


+
+slots
+ + +[]Kubernetes resource.Quantity + + +
+(Optional) +

The valid vcpu cores, it’s useful if you want to define valid vcpu cores explicitly.
If Slots is specified, Max, Min, and Step are ignored


+
+

CfgFileFormat +(string alias)

+

+(Appears on:FormatterConfig) +

+
+

CfgFileFormat defines formatter of configuration files.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription

"dotenv"

"hcl"

"ini"

"json"

"properties"

"redis"

"toml"

"xml"

"yaml"

+

CfgReloadType +(string alias)

+
+

CfgReloadType defines reload method.


+
+ + + + + + + + + + + + + + + + + + +
ValueDescription

"http"

"sql"

"exec"

"tpl"

"signal"

+

ClassDefRef +

+

+(Appears on:ClusterComponentSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+(Optional) +

Name refers to the name of the ComponentClassDefinition.


+
+class
+ +string + +
+

Class refers to the name of the class that is defined in the ComponentClassDefinition.


+
+

ClusterComponentDefinition +

+

+(Appears on:ClusterDefinitionSpec) +

+
+

ClusterComponentDefinition provides a workload component specification template,
with attributes that strongly work with stateful workloads and day-2 operations
behaviors.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name of the component, it can be any valid string.


+
+description
+ +string + +
+(Optional) +

The description of component definition.


+
+workloadType
+ + +WorkloadType + + +
+

workloadType defines type of the workload.
Stateless is a stateless workload type used to describe stateless applications.
Stateful is a stateful workload type used to describe common stateful applications.
Consensus is a stateful workload type used to describe applications based on consensus protocols, common consensus protocols such as raft and paxos.
Replication is a stateful workload type used to describe applications based on the primary-secondary data replication protocol.


+
+characterType
+ +string + +
+(Optional) +

characterType defines well-known database component name, such as mongos(mongodb), proxy(redis), mariadb(mysql)
KubeBlocks will generate proper monitor configs for well-known characterType when builtIn is true.


+
+configSpecs
+ + +[]ComponentConfigSpec + + +
+(Optional) +

The configSpec field provided by provider, and
finally this configTemplateRefs will be rendered into the user’s own configuration file according to the user’s cluster.


+
+scriptSpecs
+ + +[]ComponentTemplateSpec + + +
+(Optional) +

The scriptSpec field provided by provider, and
finally this configTemplateRefs will be rendered into the user’s own configuration file according to the user’s cluster.


+
+probes
+ + +ClusterDefinitionProbes + + +
+(Optional) +

probes setting for healthy checks.


+
+monitor
+ + +MonitorConfig + + +
+(Optional) +

monitor is monitoring config which provided by provider.


+
+logConfigs
+ + +[]LogConfig + + +
+(Optional) +

logConfigs is detail log file config which provided by provider.


+
+podSpec
+ + +Kubernetes core/v1.PodSpec + + +
+(Optional) +

podSpec define pod spec template of the cluster component.


+
+service
+ + +ServiceSpec + + +
+(Optional) +

service defines the behavior of a service spec.
provide read-write service when WorkloadType is Consensus.


+
+statelessSpec
+ + +StatelessSetSpec + + +
+(Optional) +

statelessSpec defines stateless related spec if workloadType is Stateless.


+
+statefulSpec
+ + +StatefulSetSpec + + +
+(Optional) +

statefulSpec defines stateful related spec if workloadType is Stateful.


+
+consensusSpec
+ + +ConsensusSetSpec + + +
+(Optional) +

consensusSpec defines consensus related spec if workloadType is Consensus, required if workloadType is Consensus.


+
+replicationSpec
+ + +ReplicationSetSpec + + +
+(Optional) +

replicationSpec defines replication related spec if workloadType is Replication.


+
+horizontalScalePolicy
+ + +HorizontalScalePolicy + + +
+(Optional) +

horizontalScalePolicy controls the behavior of horizontal scale.


+
+systemAccounts
+ + +SystemAccountSpec + + +
+(Optional) +

Statement to create system account.


+
+volumeTypes
+ + +[]VolumeTypeSpec + + +
+(Optional) +

volumeTypes is used to describe the purpose of the volumes
mapping the name of the VolumeMounts in the PodSpec.Container field,
such as data volume, log volume, etc.
When backing up the volume, the volume can be correctly backed up
according to the volumeType.



For example:
name: data, type: data means that the volume named data is used to store data.
name: binlog, type: log means that the volume named binlog is used to store log.



NOTE:
When volumeTypes is not defined, the backup function will not be supported,
even if a persistent volume has been specified.


+
+customLabelSpecs
+ + +[]CustomLabelSpec + + +
+(Optional) +

customLabelSpecs is used for custom label tags which you want to add to the component resources.


+
+

ClusterComponentPhase +(string alias)

+

+(Appears on:ClusterComponentStatus, OpsRequestComponentStatus) +

+
+

ClusterComponentPhase defines the Cluster CR .status.components.phase


+
+ + + + + + + + + + + + + + + + + + + + +
ValueDescription

"Abnormal"

"Creating"

"Failed"

"Running"

"Updating"

Abnormal is a sub-state of failed, where one or more workload pods is not in “Running” phase.


"Stopped"

+

ClusterComponentService +

+

+(Appears on:ClusterComponentSpec, Expose, LastComponentConfiguration) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Service name


+
+serviceType
+ + +Kubernetes core/v1.ServiceType + + +
+(Optional) +

serviceType determines how the Service is exposed. Valid
options are ClusterIP, NodePort, and LoadBalancer.
“ClusterIP” allocates a cluster-internal IP address for load-balancing
to endpoints. Endpoints are determined by the selector or if that is not
specified, they are determined by manual construction of an Endpoints object or
EndpointSlice objects. If clusterIP is “None”, no virtual IP is
allocated and the endpoints are published as a set of endpoints rather
than a virtual IP.
“NodePort” builds on ClusterIP and allocates a port on every node which
routes to the same endpoints as the clusterIP.
“LoadBalancer” builds on NodePort and creates an external load-balancer
(if supported in the current cloud) which routes to the same endpoints
as the clusterIP.
More info: https://kubernetes.io/docs/concepts/services-networking/service/#publishing-services-service-types.


+
+annotations
+ +map[string]string + +
+(Optional) +

If ServiceType is LoadBalancer, cloud provider related parameters can be put here
More info: https://kubernetes.io/docs/concepts/services-networking/service/#loadbalancer.


+
+

ClusterComponentSpec +

+

+(Appears on:ClusterSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name defines cluster’s component name.


+
+componentDefRef
+ +string + +
+

componentDefRef references the componentDef defined in ClusterDefinition spec.


+
+classDefRef
+ + +ClassDefRef + + +
+(Optional) +

classDefRef references the class defined in ComponentClassDefinition.


+
+monitor
+ +bool + +
+(Optional) +

monitor is a switch to enable monitoring and is set as false by default.
KubeBlocks provides an extension mechanism to support component level monitoring,
which will scrape metrics auto or manually from servers in component and export
metrics to Time Series Database.


+
+enabledLogs
+ +[]string + +
+(Optional) +

enabledLogs indicates which log file takes effect in the database cluster.
element is the log type which is defined in cluster definition logConfig.name,
and will set relative variables about this log type in database kernel.


+
+replicas
+ +int32 + +
+

Component replicas. The default value is used in ClusterDefinition spec if not specified.


+
+affinity
+ + +Affinity + + +
+(Optional) +

affinity describes affinities specified by users.


+
+tolerations
+ + +[]Kubernetes core/v1.Toleration + + +
+(Optional) +

Component tolerations will override ClusterSpec.Tolerations if specified.


+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

Resources requests and limits of workload.


+
+volumeClaimTemplates
+ + +[]ClusterComponentVolumeClaimTemplate + + +
+(Optional) +

volumeClaimTemplates information for statefulset.spec.volumeClaimTemplates.


+
+services
+ + +[]ClusterComponentService + + +
+(Optional) +

Services expose endpoints that can be accessed by clients.


+
+primaryIndex
+ +int32 + +
+(Optional) +

primaryIndex determines which index is primary when workloadType is Replication. Index number starts from zero.


+
+switchPolicy
+ + +ClusterSwitchPolicy + + +
+(Optional) +

switchPolicy defines the strategy for switchover and failover when workloadType is Replication.


+
+tls
+ +bool + +
+(Optional) +

Enables or disables TLS certs.


+
+issuer
+ + +Issuer + + +
+(Optional) +

issuer defines provider context for TLS certs.
required when TLS enabled


+
+serviceAccountName
+ +string + +
+(Optional) +

serviceAccountName is the name of the ServiceAccount that running component depends on.


+
+noCreatePDB
+ +bool + +
+(Optional) +

noCreatePDB defines the PodDistruptionBudget creation behavior and is set to true if creation of PodDistruptionBudget
for this component is not needed. It defaults to false.


+
+

ClusterComponentStatus +

+

+(Appears on:ClusterStatus) +

+
+

ClusterComponentStatus records components status.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +ClusterComponentPhase + + +
+

phase describes the phase of the component and the detail information of the phases are as following:
Running: the component is running. [terminal state]
Stopped: the component is stopped, as no running pod. [terminal state]
Failed: the component is unavailable, i.e. all pods are not ready for Stateless/Stateful component and
Leader/Primary pod is not ready for Consensus/Replication component. [terminal state]
Abnormal: the component is running but part of its pods are not ready.
Leader/Primary pod is ready for Consensus/Replication component. [terminal state]
Creating: the component has entered creating process.
Updating: the component has entered updating process, triggered by Spec. updated.


+
+message
+ + +ComponentMessageMap + + +
+(Optional) +

message records the component details message in current phase.
Keys are podName or deployName or statefulSetName. The format is ObjectKind/Name.


+
+podsReady
+ +bool + +
+(Optional) +

podsReady checks if all pods of the component are ready.


+
+podsReadyTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

podsReadyTime what time point of all component pods are ready,
this time is the ready time of the last component pod.


+
+consensusSetStatus
+ + +ConsensusSetStatus + + +
+(Optional) +

consensusSetStatus specifies the mapping of role and pod name.


+
+replicationSetStatus
+ + +ReplicationSetStatus + + +
+(Optional) +

replicationSetStatus specifies the mapping of role and pod name.


+
+

ClusterComponentVersion +

+

+(Appears on:ClusterVersionSpec) +

+
+

ClusterComponentVersion is an application version component spec.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+componentDefRef
+ +string + +
+

componentDefRef reference one of the cluster component definition names in ClusterDefinition API (spec.componentDefs.name).


+
+configSpecs
+ + +[]ComponentConfigSpec + + +
+(Optional) +

configSpecs defines a configuration extension mechanism to handle configuration differences between versions,
the configTemplateRefs field, together with configTemplateRefs in the ClusterDefinition,
determines the final configuration file.


+
+systemAccountSpec
+ + +SystemAccountShortSpec + + +
+(Optional) +

systemAccountSpec define image for the component to connect database or engines.
It overrides image and env attributes defined in ClusterDefinition.spec.componentDefs.systemAccountSpec.cmdExecutorConfig.
To clean default envs settings, set SystemAccountSpec.CmdExecutorConfig.Env to empty list.


+
+versionsContext
+ + +VersionsContext + + +
+

versionContext defines containers images’ context for component versions,
this value replaces ClusterDefinition.spec.componentDefs.podSpec.[initContainers | containers]


+
+

ClusterComponentVolumeClaimTemplate +

+

+(Appears on:ClusterComponentSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Reference ClusterDefinition.spec.componentDefs.containers.volumeMounts.name.


+
+spec
+ + +PersistentVolumeClaimSpec + + +
+(Optional) +

spec defines the desired characteristics of a volume requested by a pod author.


+
+
+ + + + + + + + + + + + + +
+accessModes
+ + +[]Kubernetes core/v1.PersistentVolumeAccessMode + + +
+(Optional) +

accessModes contains the desired access modes the volume should have.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1.


+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

resources represents the minimum resources the volume should have.
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
that are lower than previous value but must still be higher than capacity recorded in the
status field of the claim.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources.


+
+storageClassName
+ +string + +
+(Optional) +

storageClassName is the name of the StorageClass required by the claim.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1.


+
+
+

ClusterDefinitionProbe +

+

+(Appears on:ClusterDefinitionProbes) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+periodSeconds
+ +int32 + +
+

How often (in seconds) to perform the probe.


+
+timeoutSeconds
+ +int32 + +
+

Number of seconds after which the probe times out. Defaults to 1 second.


+
+failureThreshold
+ +int32 + +
+

Minimum consecutive failures for the probe to be considered failed after having succeeded.


+
+commands
+ + +ClusterDefinitionProbeCMDs + + +
+(Optional) +

commands used to execute for probe.


+
+

ClusterDefinitionProbeCMDs +

+

+(Appears on:ClusterDefinitionProbe) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+writes
+ +[]string + +
+(Optional) +

Write check executed on probe sidecar, used to check workload’s allow write access.


+
+queries
+ +[]string + +
+(Optional) +

Read check executed on probe sidecar, used to check workload’s readonly access.


+
+

ClusterDefinitionProbes +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+runningProbe
+ + +ClusterDefinitionProbe + + +
+(Optional) +

Probe for DB running check.


+
+statusProbe
+ + +ClusterDefinitionProbe + + +
+(Optional) +

Probe for DB status check.


+
+roleProbe
+ + +ClusterDefinitionProbe + + +
+(Optional) +

Probe for DB role changed check.


+
+roleProbeTimeoutAfterPodsReady
+ +int32 + +
+(Optional) +

roleProbeTimeoutAfterPodsReady(in seconds), when all pods of the component are ready,
it will detect whether the application is available in the pod.
if pods exceed the InitializationTimeoutSeconds time without a role label,
this component will enter the Failed/Abnormal phase.
Note that this configuration will only take effect if the component supports RoleProbe
and will not affect the life cycle of the pod. default values are 60 seconds.


+
+

ClusterDefinitionSpec +

+

+(Appears on:ClusterDefinition) +

+
+

ClusterDefinitionSpec defines the desired state of ClusterDefinition


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ +string + +
+(Optional) +

Cluster definition type defines well known application cluster type, e.g. mysql/redis/mongodb


+
+componentDefs
+ + +[]ClusterComponentDefinition + + +
+

componentDefs provides cluster components definitions.


+
+connectionCredential
+ +map[string]string + +
+(Optional) +

Connection credential template used for creating a connection credential
secret for cluster.apps.kubeblocks.io object. Built-in objects are:
$(RANDOM_PASSWD) - random 8 characters.
$(UUID) - generate a random UUID v4 string.
$(UUID_B64) - generate a random UUID v4 BASE64 encoded string.
$(UUID_STR_B64) - generate a random UUID v4 string then BASE64 encoded.
$(UUID_HEX) - generate a random UUID v4 HEX representation.
$(HEADLESS_SVC_FQDN) - headless service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME)-headless.$(NAMESPACE).svc,
where 1ST_COMP_NAME is the 1st component that provide ClusterDefinition.spec.componentDefs[].service attribute;
$(SVC_FQDN) - service FQDN placeholder, value pattern - $(CLUSTER_NAME)-$(1ST_COMP_NAME).$(NAMESPACE).svc,
where 1ST_COMP_NAME is the 1st component that provide ClusterDefinition.spec.componentDefs[].service attribute;
$(SVC_PORT_{PORT-NAME}) - a ServicePort’s port value with specified port name, i.e, a servicePort JSON struct:
"name": "mysql", "targetPort": "mysqlContainerPort", "port": 3306, and “$(SVC_PORT_mysql)” in the
connection credential value is 3306.


+
+

ClusterDefinitionStatus +

+

+(Appears on:ClusterDefinition) +

+
+

ClusterDefinitionStatus defines the observed state of ClusterDefinition


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +Phase + + +
+

ClusterDefinition phase, valid values are empty, Available, ‘Unavailable`.
Available is ClusterDefinition become available, and can be referenced for co-related objects.


+
+message
+ +string + +
+(Optional) +

Extra message in current phase


+
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the most recent generation observed for this
ClusterDefinition. It corresponds to the ClusterDefinition’s generation, which is
updated on mutation by the API Server.


+
+

ClusterPhase +(string alias)

+

+(Appears on:ClusterStatus, OpsRequestBehaviour) +

+
+

ClusterPhase defines the Cluster CR .status.phase


+
+ + + + + + + + + + + + + + + + + + + + +
ValueDescription

"Abnormal"

"Creating"

Abnormal is a sub-state of failed, where one of the cluster components has “Failed” or “Abnormal” status phase.


"Failed"

"Running"

REVIEW/TODO: AbnormalClusterPhase provides hybrid, consider remove it if possible


"Updating"

"Stopped"

+

ClusterSpec +

+

+(Appears on:Cluster) +

+
+

ClusterSpec defines the desired state of Cluster.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+clusterDefinitionRef
+ +string + +
+

Cluster referencing ClusterDefinition name. This is an immutable attribute.


+
+clusterVersionRef
+ +string + +
+(Optional) +

Cluster referencing ClusterVersion name.


+
+terminationPolicy
+ + +TerminationPolicyType + + +
+

Cluster termination policy. Valid values are DoNotTerminate, Halt, Delete, WipeOut.
DoNotTerminate will block delete operation.
Halt will delete workload resources such as statefulset, deployment workloads but keep PVCs.
Delete is based on Halt and deletes PVCs.
WipeOut is based on Delete and wipe out all volume snapshots and snapshot data from backup storage location.


+
+componentSpecs
+ + +[]ClusterComponentSpec + + +
+

List of componentSpecs you want to replace in ClusterDefinition and ClusterVersion. It will replace the field in ClusterDefinition’s and ClusterVersion’s component if type is matching.


+
+affinity
+ + +Affinity + + +
+(Optional) +

affinity is a group of affinity scheduling rules.


+
+tolerations
+ + +[]Kubernetes core/v1.Toleration + + +
+(Optional) +

tolerations are attached to tolerate any taint that matches the triple key,value,effect using the matching operator operator.


+
+

ClusterStatus +

+

+(Appears on:Cluster) +

+
+

ClusterStatus defines the observed state of Cluster.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the most recent generation observed for this
Cluster. It corresponds to the Cluster’s generation, which is
updated on mutation by the API Server.


+
+phase
+ + +ClusterPhase + + +
+(Optional) +

phase describes the phase of the Cluster, the detail information of the phases are as following:
Running: cluster is running, all its components are available. [terminal state]
Stopped: cluster has stopped, all its components are stopped. [terminal state]
Failed: cluster is unavailable. [terminal state]
Abnormal: Cluster is still running, but part of its components are Abnormal/Failed. [terminal state]
Creating: Cluster has entered creating process.
Updating: Cluster has entered updating process, triggered by Spec. updated.


+
+message
+ +string + +
+(Optional) +

message describes cluster details message in current phase.


+
+components
+ + +map[string]..ClusterComponentStatus + + +
+(Optional) +

components record the current status information of all components of the cluster.


+
+clusterDefGeneration
+ +int64 + +
+(Optional) +

clusterDefGeneration represents the generation number of ClusterDefinition referenced.


+
+conditions
+ + +[]Kubernetes meta/v1.Condition + + +
+(Optional) +

Describe current state of cluster API Resource, like warning.


+
+

ClusterSwitchPolicy +

+

+(Appears on:ClusterComponentSpec) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+type
+ + +SwitchPolicyType + + +
+(Optional) +

clusterSwitchPolicy type defined by Provider in ClusterDefinition, refer components[i].replicationSpec.switchPolicies[x].type


+
+

ClusterVersionSpec +

+

+(Appears on:ClusterVersion) +

+
+

ClusterVersionSpec defines the desired state of ClusterVersion


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+clusterDefinitionRef
+ +string + +
+

ref ClusterDefinition.


+
+componentVersions
+ + +[]ClusterComponentVersion + + +
+

List of components’ containers versioning context, i.e., container image ID, container commands, args., and environments.


+
+

ClusterVersionStatus +

+

+(Appears on:ClusterVersion) +

+
+

ClusterVersionStatus defines the observed state of ClusterVersion


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +Phase + + +
+(Optional) +

phase - in list of [Available,Unavailable]


+
+message
+ +string + +
+(Optional) +

A human readable message indicating details about why the ClusterVersion is in this phase.


+
+observedGeneration
+ +int64 + +
+(Optional) +

generation number


+
+clusterDefGeneration
+ +int64 + +
+(Optional) +

clusterDefGeneration represents the generation number of ClusterDefinition referenced.


+
+

CmdExecutorConfig +

+

+(Appears on:SystemAccountSpec) +

+
+

CmdExecutorConfig specifies how to perform creation and deletion statements.


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+CommandExecutorEnvItem
+ + +CommandExecutorEnvItem + + +
+

+(Members of CommandExecutorEnvItem are embedded into this type.) +

+
+CommandExecutorItem
+ + +CommandExecutorItem + + +
+

+(Members of CommandExecutorItem are embedded into this type.) +

+
+

CommandExecutorEnvItem +

+

+(Appears on:CmdExecutorConfig, SwitchCmdExecutorConfig, SystemAccountShortSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+image
+ +string + +
+

image for Connector when executing the command.


+
+env
+ + +[]Kubernetes core/v1.EnvVar + + +
+(Optional) +

envs is a list of environment variables.


+
+

CommandExecutorItem +

+

+(Appears on:CmdExecutorConfig, SwitchStep) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+command
+ +[]string + +
+

command to perform statements.


+
+args
+ +[]string + +
+(Optional) +

args is used to perform statements.


+
+

CommonBackupPolicy +

+

+(Appears on:BackupPolicy) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+BasePolicy
+ + +BasePolicy + + +
+

+(Members of BasePolicy are embedded into this type.) +

+
+backupToolName
+ +string + +
+

which backup tool to perform database backup, only support one tool.


+
+

ComponentClass +

+

+(Appears on:ComponentClassInstance, ComponentClassSeries) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+(Optional) +

name is the class name


+
+args
+ +[]string + +
+(Optional) +

args are variable’s value


+
+cpu
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

the CPU of the class


+
+memory
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

the memory of the class


+
+

ComponentClassDefinitionSpec +

+

+(Appears on:ComponentClassDefinition) +

+
+

ComponentClassDefinitionSpec defines the desired state of ComponentClassDefinition


+
+ + + + + + + + + + + + + +
FieldDescription
+groups
+ + +[]ComponentClassGroup + + +
+(Optional) +

group defines a list of class series that conform to the same constraint.


+
+

ComponentClassDefinitionStatus +

+

+(Appears on:ComponentClassDefinition) +

+
+

ComponentClassDefinitionStatus defines the observed state of ComponentClassDefinition


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the most recent generation observed for this
ComponentClassDefinition. It corresponds to the ComponentClassDefinition’s generation, which is
updated on mutation by the API Server.


+
+classes
+ + +[]ComponentClassInstance + + +
+

classes is the list of classes that have been observed for this ComponentClassDefinition


+
+

ComponentClassGroup +

+

+(Appears on:ComponentClassDefinitionSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+resourceConstraintRef
+ +string + +
+

resourceConstraintRef reference to the resource constraint object name, indicates that the series
defined below all conform to the constraint.


+
+template
+ +string + +
+(Optional) +

template is a class definition template that uses the Go template syntax and allows for variable declaration.
When defining a class in Series, specifying the variable’s value is sufficient, as the complete class
definition will be generated through rendering the template.



For example:
template: |
cpu: “{{ or .cpu 1 }}”
memory: “{{ or .memory 4 }}Gi”


+
+vars
+ +[]string + +
+(Optional) +

vars defines the variables declared in the template and will be used to generating the complete class definition by
render the template.


+
+series
+ + +[]ComponentClassSeries + + +
+(Optional) +

series is a series of class definitions.


+
+

ComponentClassInstance +

+

+(Appears on:ComponentClassDefinitionStatus) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentClass
+ + +ComponentClass + + +
+

+(Members of ComponentClass are embedded into this type.) +

+
+resourceConstraintRef
+ +string + +
+

resourceConstraintRef reference to the resource constraint object name.


+
+

ComponentClassSeries +

+

+(Appears on:ComponentClassGroup) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+namingTemplate
+ +string + +
+(Optional) +

namingTemplate is a template that uses the Go template syntax and allows for referencing variables defined
in ComponentClassGroup.Template. This enables dynamic generation of class names.
For example:
name: “general-{{ .cpu }}c{{ .memory }}g”


+
+classes
+ + +[]ComponentClass + + +
+(Optional) +

classes are definitions of classes that come in two forms. In the first form, only ComponentClass.Args
need to be defined, and the complete class definition is generated by rendering the ComponentClassGroup.Template
and Name. In the second form, the Name, CPU and Memory must be defined.


+
+

ComponentConfigSpec +

+

+(Appears on:ClusterComponentDefinition, ClusterComponentVersion) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentTemplateSpec
+ + +ComponentTemplateSpec + + +
+

+(Members of ComponentTemplateSpec are embedded into this type.) +

+
+keys
+ +[]string + +
+(Optional) +

Specify a list of keys.
If empty, ConfigConstraint takes effect for all keys in configmap.


+
+constraintRef
+ +string + +
+(Optional) +

Specify the name of the referenced the configuration constraints object.


+
+

ComponentMessageMap +(map[string]string alias)

+

+(Appears on:ClusterComponentStatus) +

+
+
+

ComponentNameSet +(map[string]struct{} alias)

+
+
+

ComponentOps +

+

+(Appears on:Expose, HorizontalScaling, OpsRequestSpec, Reconfigure, VerticalScaling, VolumeExpansion) +

+
+

ComponentOps defines the common variables of component scope operations.


+
+ + + + + + + + + + + + + +
FieldDescription
+componentName
+ +string + +
+

componentName cluster component name.


+
+

ComponentResourceConstraintSpec +

+

+(Appears on:ComponentResourceConstraint) +

+
+

ComponentResourceConstraintSpec defines the desired state of ComponentResourceConstraint


+
+ + + + + + + + + + + + + +
FieldDescription
+constraints
+ + +[]ResourceConstraint + + +
+

Component resource constraints


+
+

ComponentResourceKey +(string alias)

+
+

ComponentResourceKey defines the resource key of component, such as pod/pvc.


+
+ + + + + + + + + + +
ValueDescription

"pods"

+

ComponentTemplateSpec +

+

+(Appears on:ClusterComponentDefinition, ComponentConfigSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Specify the name of configuration template.


+
+templateRef
+ +string + +
+

Specify the name of the referenced the configuration template ConfigMap object.


+
+namespace
+ +string + +
+(Optional) +

Specify the namespace of the referenced the configuration template ConfigMap object.
An empty namespace is equivalent to the “default” namespace.


+
+volumeName
+ +string + +
+

volumeName is the volume name of PodTemplate, which the configuration file produced through the configuration template will be mounted to the corresponding volume.
The volume name must be defined in podSpec.containers[*].volumeMounts.


+
+defaultMode
+ +int32 + +
+(Optional) +

defaultMode is optional: mode bits used to set permissions on created files by default.
Must be an octal value between 0000 and 0777 or a decimal value between 0 and 511.
YAML accepts both octal and decimal values, JSON requires decimal values for mode bits.
Defaults to 0644.
Directories within the path are not affected by this setting.
This might be in conflict with other options that affect the file
mode, like fsGroup, and the result can be other mode bits set.


+
+

ConfigConstraintPhase +(string alias)

+

+(Appears on:ConfigConstraintStatus) +

+
+

ConfigConstraintPhase defines the ConfigConstraint CR .status.phase


+
+ + + + + + + + + + + + + + +
ValueDescription

"Available"

"Deleting"

"Unavailable"

+

ConfigConstraintSpec +

+

+(Appears on:ConfigConstraint) +

+
+

ConfigConstraintSpec defines the desired state of ConfigConstraint


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+reloadOptions
+ + +ReloadOptions + + +
+(Optional) +

reloadOptions indicates whether the process supports reload.
if set, the controller will determine the behavior of the engine instance based on the configuration templates,
restart or reload depending on whether any parameters in the StaticParameters have been modified.


+
+cfgSchemaTopLevelName
+ +string + +
+(Optional) +

cfgSchemaTopLevelName is cue type name, which generates openapi schema.


+
+configurationSchema
+ + +CustomParametersValidation + + +
+(Optional) +

configurationSchema imposes restrictions on database parameter’s rule.


+
+staticParameters
+ +[]string + +
+(Optional) +

staticParameters, list of StaticParameter, modifications of them trigger a process restart.


+
+dynamicParameters
+ +[]string + +
+(Optional) +

dynamicParameters, list of DynamicParameter, modifications of them trigger a config dynamic reload without process restart.


+
+immutableParameters
+ +[]string + +
+(Optional) +

immutableParameters describes parameters that prohibit user from modification.


+
+selector
+ + +Kubernetes meta/v1.LabelSelector + + +
+

selector is used to match the label on the pod,
for example, a pod of the primary is match on the patroni cluster.


+
+formatterConfig
+ + +FormatterConfig + + +
+

formatterConfig describes the format of the configuration file, the controller
1. parses configuration file
2. analyzes the modified parameters
3. applies corresponding policies.


+
+

ConfigConstraintStatus +

+

+(Appears on:ConfigConstraint) +

+
+

ConfigConstraintStatus defines the observed state of ConfigConstraint.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +ConfigConstraintPhase + + +
+(Optional) +

phase is status of configuration template, when set to CCAvailablePhase, it can be referenced by ClusterDefinition or ClusterVersion.


+
+message
+ +string + +
+(Optional) +

message field describes the reasons of abnormal status.


+
+observedGeneration
+ +int64 + +
+(Optional) +

observedGeneration is the latest generation observed for this
ClusterDefinition. It refers to the ConfigConstraint’s generation, which is
updated by the API Server.


+
+

Configuration +

+

+(Appears on:Reconfigure) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name is a config template name.


+
+keys
+ + +[]ParameterConfig + + +
+

keys is used to set the parameters to be updated.


+
+

ConfigurationStatus +

+

+(Appears on:ReconfiguringStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name is a config template name.


+
+updatePolicy
+ + +UpgradePolicy + + +
+(Optional) +

updatePolicy describes the policy of reconfiguring.


+
+status
+ +string + +
+(Optional) +

status describes the current state of the reconfiguring state machine.


+
+succeedCount
+ +int32 + +
+(Optional) +

succeedCount describes the number of successful reconfiguring.


+
+expectedCount
+ +int32 + +
+(Optional) +

expectedCount describes the number of expected reconfiguring.


+
+lastStatus
+ +string + +
+(Optional) +

lastStatus describes the last status for the reconfiguring controller.


+
+lastAppliedConfiguration
+ +map[string]string + +
+(Optional) +

LastAppliedConfiguration describes the last configuration.


+
+updatedParameters
+ + +UpdatedParameters + + +
+(Optional) +

updatedParameters describes the updated parameters.


+
+

ConnectionCredentialKey +

+

+(Appears on:TargetInstance) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+passwordKey
+ +string + +
+(Optional) +

the key of password in the ConnectionCredential secret.
if not set, the default key is “password”.


+
+usernameKey
+ +string + +
+(Optional) +

the key of username in the ConnectionCredential secret.
if not set, the default key is “username”.


+
+

ConsensusMember +

+

+(Appears on:ConsensusSetSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name, role name.


+
+accessMode
+ + +AccessMode + + +
+

accessMode, what service this member capable.


+
+replicas
+ +int32 + +
+(Optional) +

replicas, number of Pods of this role.
default 1 for Leader
default 0 for Learner
default Cluster.spec.componentSpec[*].Replicas - Leader.Replicas - Learner.Replicas for Followers


+
+

ConsensusMemberStatus +

+

+(Appears on:ConsensusSetStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Defines the role name.


+
+accessMode
+ + +AccessMode + + +
+

accessMode defines what service this pod provides.


+
+pod
+ +string + +
+

Pod name.


+
+

ConsensusSetSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+StatefulSetSpec
+ + +StatefulSetSpec + + +
+

+(Members of StatefulSetSpec are embedded into this type.) +

+
+leader
+ + +ConsensusMember + + +
+

leader, one single leader.


+
+followers
+ + +[]ConsensusMember + + +
+(Optional) +

followers, has voting right but not Leader.


+
+learner
+ + +ConsensusMember + + +
+(Optional) +

learner, no voting right.


+
+

ConsensusSetStatus +

+

+(Appears on:ClusterComponentStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+leader
+ + +ConsensusMemberStatus + + +
+

Leader status.


+
+followers
+ + +[]ConsensusMemberStatus + + +
+(Optional) +

Followers status.


+
+learner
+ + +ConsensusMemberStatus + + +
+(Optional) +

Learner status.


+
+

CustomLabelSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+key
+ +string + +
+

key name of label


+
+value
+ +string + +
+

value of label


+
+resources
+ + +[]GVKResource + + +
+

resources defines the resources to be labeled.


+
+

CustomParametersValidation +

+

+(Appears on:ConfigConstraintSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+schema
+ + +Kubernetes api extensions v1.JSONSchemaProps + + +
+

schema provides a way for providers to validate the changed parameters through json.


+
+cue
+ +string + +
+(Optional) +

cue that to let provider verify user configuration through cue language.


+
+

ExporterConfig +

+

+(Appears on:MonitorConfig) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+scrapePort
+ + +Kubernetes api utils intstr.IntOrString + + +
+

scrapePort is exporter port for Time Series Database to scrape metrics.


+
+scrapePath
+ +string + +
+(Optional) +

scrapePath is exporter url path for Time Series Database to scrape metrics.


+
+

Expose +

+

+(Appears on:OpsRequestSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentOps
+ + +ComponentOps + + +
+

+(Members of ComponentOps are embedded into this type.) +

+
+services
+ + +[]ClusterComponentService + + +
+

Setting the list of services to be exposed.


+
+

FormatterConfig +

+

+(Appears on:ConfigConstraintSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+FormatterOptions
+ + +FormatterOptions + + +
+

+(Members of FormatterOptions are embedded into this type.) +

+(Optional) +

The FormatterOptions represents the special options of configuration file.
This is optional for now. If not specified.


+
+format
+ + +CfgFileFormat + + +
+

The configuration file format. Valid values are ini, xml, yaml, json,
hcl, dotenv, properties and toml.



ini: a configuration file that consists of a text-based content with a structure and syntax comprising key–value pairs for properties, reference wiki: https://en.wikipedia.org/wiki/INI_file
xml: reference wiki: https://en.wikipedia.org/wiki/XML
yaml: a configuration file support for complex data types and structures.
json: reference wiki: https://en.wikipedia.org/wiki/JSON
hcl: : The HashiCorp Configuration Language (HCL) is a configuration language authored by HashiCorp, reference url: https://www.linode.com/docs/guides/introduction-to-hcl/
dotenv: this was a plain text file with simple key–value pairs, reference wiki: https://en.wikipedia.org/wiki/Configuration_file#MS-DOS
properties: a file extension mainly used in Java, reference wiki: https://en.wikipedia.org/wiki/.properties
toml: reference wiki: https://en.wikipedia.org/wiki/TOML


+
+

FormatterOptions +

+

+(Appears on:FormatterConfig) +

+
+

FormatterOptions represents the special options of configuration file.
Only one of its members may be specified.


+
+ + + + + + + + + + + + + +
FieldDescription
+iniConfig
+ + +IniConfig + + +
+(Optional) +

iniConfig represents the ini options.


+
+

GVKResource +

+

+(Appears on:CustomLabelSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+gvk
+ +string + +
+

gvk is Group/Version/Kind, for example “v1/Pod”, “apps/v1/StatefulSet”, etc.
when the gvk resource filtered by the selector already exists, if there is no corresponding custom label, it will be added, and if label already exists, it will be updated.


+
+selector
+ +map[string]string + +
+(Optional) +

selector is a label query over a set of resources.


+
+

HScaleDataClonePolicyType +(string alias)

+

+(Appears on:HorizontalScalePolicy) +

+
+

HScaleDataClonePolicyType defines data clone policy when horizontal scaling.


+
+ + + + + + + + + + + + + + +
ValueDescription

"Backup"

"Snapshot"

"None"

+

HorizontalScalePolicy +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ + +HScaleDataClonePolicyType + + +
+(Optional) +

type controls what kind of data synchronization do when component scale out.
Policy is in enum of {None, Snapshot}. The default policy is None.
None: Default policy, do nothing.
Snapshot: Do native volume snapshot before scaling and restore to newly scaled pods.
Prefer backup job to create snapshot if can find a backupPolicy from ‘BackupPolicyTemplateName’.
Notice that ‘Snapshot’ policy will only take snapshot on one volumeMount, default is
the first volumeMount of first container (i.e. clusterdefinition.spec.components.podSpec.containers[0].volumeMounts[0]),
since take multiple snapshots at one time might cause consistency problem.


+
+backupPolicyTemplateName
+ +string + +
+(Optional) +

BackupPolicyTemplateName reference the backup policy template.


+
+volumeMountsName
+ +string + +
+(Optional) +

volumeMountsName defines which volumeMount of the container to do backup,
only work if Type is not None
if not specified, the 1st volumeMount will be chosen


+
+

HorizontalScaling +

+

+(Appears on:OpsRequestSpec) +

+
+

HorizontalScaling defines the variables of horizontal scaling operation


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentOps
+ + +ComponentOps + + +
+

+(Members of ComponentOps are embedded into this type.) +

+
+replicas
+ +int32 + +
+

replicas for the workloads.


+
+

IniConfig +

+

+(Appears on:FormatterOptions) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+sectionName
+ +string + +
+(Optional) +

sectionName describes ini section.


+
+

Issuer +

+

+(Appears on:ClusterComponentSpec) +

+
+

Issuer defines Tls certs issuer


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ + +IssuerName + + +
+

Name of issuer.
Options supported:
- KubeBlocks - Certificates signed by KubeBlocks Operator.
- UserProvided - User provided own CA-signed certificates.


+
+secretRef
+ + +TLSSecretRef + + +
+(Optional) +

secretRef. TLS certs Secret reference
required when from is UserProvided


+
+

IssuerName +(string alias)

+

+(Appears on:Issuer) +

+
+

IssuerName defines Tls certs issuer name


+
+ + + + + + + + + + + + +
ValueDescription

"KubeBlocks"

IssuerKubeBlocks Certificates signed by KubeBlocks Operator.


"UserProvided"

IssuerUserProvided User provided own CA-signed certificates.


+

KBAccountType +(byte alias)

+
+

KBAccountType is used for bitwise operation.


+
+ + + + + + + + + + +
ValueDescription

0

+

LastComponentConfiguration +

+

+(Appears on:LastConfiguration) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+replicas
+ +int32 + +
+(Optional) +

replicas are the last replicas of the component.


+
+ResourceRequirements
+ + +Kubernetes core/v1.ResourceRequirements + + +
+

+(Members of ResourceRequirements are embedded into this type.) +

+(Optional) +

the last resources of the component.


+
+class
+ +string + +
+(Optional) +

the last class name of the component.


+
+volumeClaimTemplates
+ + +[]OpsRequestVolumeClaimTemplate + + +
+(Optional) +

volumeClaimTemplates records the last volumeClaimTemplates of the component.


+
+services
+ + +[]ClusterComponentService + + +
+(Optional) +

services records the last services of the component.


+
+targetResources
+ +map[..ComponentResourceKey][]string + +
+(Optional) +

targetResources records the affecting target resources information for the component.
resource key is in list of [pods].


+
+

LastConfiguration +

+

+(Appears on:OpsRequestStatus) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+clusterVersionRef
+ +string + +
+(Optional) +

clusterVersionRef references ClusterVersion name.


+
+components
+ + +map[string]..LastComponentConfiguration + + +
+(Optional) +

components records last configuration of the component.


+
+

LetterCase +(string alias)

+

+(Appears on:PasswordConfig) +

+
+

LetterCase defines cases to use in password generation.


+
+ + + + + + + + + + + + + + +
ValueDescription

"LowerCases"

"MixedCases"

"UpperCases"

+

LogConfig +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name log type name, such as slow for MySQL slow log file.


+
+filePathPattern
+ +string + +
+

filePathPattern log file path pattern which indicate how to find this file
corresponding to variable (log path) in database kernel. please don’t set this casually.


+
+

MemoryConstraint +

+

+(Appears on:ResourceConstraint) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+sizePerCPU
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The size of memory per vcpu core.
For example: 1Gi, 200Mi.
If SizePerCPU is specified, MinPerCPU and MaxPerCPU are ignore.


+
+maxPerCPU
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The maximum size of memory per vcpu core, [MinPerCPU, MaxPerCPU] defines a range for valid memory size per vcpu core.
It is useful on GCP as the ratio between the CPU and memory may be a range.
If SizePerCPU is specified, MinPerCPU and MaxPerCPU are ignored.
Reference: https://cloud.google.com/compute/docs/general-purpose-machines#custom_machine_types


+
+minPerCPU
+ + +Kubernetes resource.Quantity + + +
+(Optional) +

The minimum size of memory per vcpu core, [MinPerCPU, MaxPerCPU] defines a range for valid memory size per vcpu core.
It is useful on GCP as the ratio between the CPU and memory may be a range.
If SizePerCPU is specified, MinPerCPU and MaxPerCPU are ignored.
Reference: https://cloud.google.com/compute/docs/general-purpose-machines#custom_machine_types


+
+

MonitorConfig +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+builtIn
+ +bool + +
+(Optional) +

builtIn is a switch to enable KubeBlocks builtIn monitoring.
If BuiltIn is set to true, monitor metrics will be scraped automatically.
If BuiltIn is set to false, the provider should set ExporterConfig and Sidecar container own.


+
+exporterConfig
+ + +ExporterConfig + + +
+(Optional) +

exporterConfig provided by provider, which specify necessary information to Time Series Database.
exporterConfig is valid when builtIn is false.


+
+

OpsPhase +(string alias)

+

+(Appears on:OpsRequestStatus) +

+
+

OpsPhase defines opsRequest phase.


+
+ + + + + + + + + + + + + + + + + + + + + + +
ValueDescription

"Cancelled"

"Cancelling"

"Creating"

"Failed"

"Pending"

"Running"

"Succeed"

+

OpsRecorder +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name OpsRequest name


+
+type
+ + +OpsType + + +
+

clusterPhase the cluster phase when the OpsRequest is running


+
+

OpsRequestBehaviour +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+FromClusterPhases
+ + +[]ClusterPhase + + +
+
+ToClusterPhase
+ + +ClusterPhase + + +
+
+ProcessingReasonInClusterCondition
+ +string + +
+
+

OpsRequestComponentStatus +

+

+(Appears on:OpsRequestStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+phase
+ + +ClusterComponentPhase + + +
+(Optional) +

phase describes the component phase, reference Cluster.status.component.phase.


+
+progressDetails
+ + +[]ProgressStatusDetail + + +
+(Optional) +

progressDetails describes the progress details of the component for this operation.


+
+workloadType
+ + +WorkloadType + + +
+(Optional) +

workloadType references workload type of component in ClusterDefinition.


+
+

OpsRequestSpec +

+

+(Appears on:OpsRequest) +

+
+

OpsRequestSpec defines the desired state of OpsRequest


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+clusterRef
+ +string + +
+

clusterRef references clusterDefinition.


+
+cancel
+ +bool + +
+(Optional) +

cancel defines the action to cancel the Pending/Creating/Running opsRequest, supported types: [VerticalScaling, HorizontalScaling].
once cancel is set to true, this opsRequest will be canceled and modifying this property again will not take effect.


+
+type
+ + +OpsType + + +
+

type defines the operation type.


+
+ttlSecondsAfterSucceed
+ +int32 + +
+(Optional) +

ttlSecondsAfterSucceed OpsRequest will be deleted after TTLSecondsAfterSucceed second when OpsRequest.status.phase is Succeed.


+
+upgrade
+ + +Upgrade + + +
+(Optional) +

upgrade specifies the cluster version by specifying clusterVersionRef.


+
+horizontalScaling
+ + +[]HorizontalScaling + + +
+(Optional) +

horizontalScaling defines what component need to horizontal scale the specified replicas.


+
+volumeExpansion
+ + +[]VolumeExpansion + + +
+(Optional) +

volumeExpansion defines what component and volumeClaimTemplate need to expand the specified storage.


+
+restart
+ + +[]ComponentOps + + +
+(Optional) +

restart the specified component.


+
+verticalScaling
+ + +[]VerticalScaling + + +
+(Optional) +

verticalScaling defines what component need to vertical scale the specified compute resources.


+
+reconfigure
+ + +Reconfigure + + +
+(Optional) +

reconfigure defines the variables that need to input when updating configuration.


+
+expose
+ + +[]Expose + + +
+(Optional) +

expose defines services the component needs to expose.


+
+restoreFrom
+ + +RestoreFromSpec + + +
+(Optional) +

cluster RestoreFrom backup or point in time


+
+

OpsRequestStatus +

+

+(Appears on:OpsRequest) +

+
+

OpsRequestStatus defines the observed state of OpsRequest


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+clusterGeneration
+ +int64 + +
+(Optional) +

ClusterGeneration records the cluster generation after handling the opsRequest action.


+
+phase
+ + +OpsPhase + + +
+

phase describes OpsRequest phase.


+
+progress
+ +string + +
+
+lastConfiguration
+ + +LastConfiguration + + +
+(Optional) +

lastConfiguration records the last configuration before this operation take effected.


+
+components
+ + +map[string]..OpsRequestComponentStatus + + +
+(Optional) +

components defines the recorded the status information of changed components for operation request.


+
+startTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

startTimestamp The time when the OpsRequest started processing.


+
+completionTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

completionTimestamp defines the OpsRequest completion time.


+
+cancelTimestamp
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

CancelTimestamp defines cancel time.


+
+reconfiguringStatus
+ + +ReconfiguringStatus + + +
+(Optional) +

reconfiguringStatus defines the status information of reconfiguring.


+
+conditions
+ + +[]Kubernetes meta/v1.Condition + + +
+(Optional) +

conditions describes opsRequest detail status.


+
+

OpsRequestVolumeClaimTemplate +

+

+(Appears on:LastComponentConfiguration, VolumeExpansion) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+storage
+ + +Kubernetes resource.Quantity + + +
+

Request storage size.


+
+name
+ +string + +
+

name references volumeClaimTemplate name from cluster components.


+
+

OpsType +(string alias)

+

+(Appears on:OpsRecorder, OpsRequestSpec) +

+
+

OpsType defines operation types.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription

"Expose"

StartType the start operation will start the pods which is deleted in stop operation.


"HorizontalScaling"

"Reconfiguring"

"Restart"

"Start"

StopType the stop operation will delete all pods in a cluster concurrently.


"Stop"

RestartType the restart operation is a special case of the rolling update operation.


"Upgrade"

"VerticalScaling"

"VolumeExpansion"

+

ParameterConfig +

+

+(Appears on:Configuration) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+key
+ +string + +
+

key indicates the key name of ConfigMap.


+
+parameters
+ + +[]ParameterPair + + +
+

Setting the list of parameters for a single configuration file.


+
+

ParameterPair +

+

+(Appears on:ParameterConfig) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+key
+ +string + +
+

key is name of the parameter to be updated.


+
+value
+ +string + +
+

parameter values to be updated.
if set nil, the parameter defined by the key field will be deleted from the configuration file.


+
+

PasswordConfig +

+

+(Appears on:SystemAccountSpec) +

+
+

PasswordConfig helps provide to customize complexity of password generation pattern.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+length
+ +int32 + +
+(Optional) +

length defines the length of password.


+
+numDigits
+ +int32 + +
+(Optional) +

numDigits defines number of digits.


+
+numSymbols
+ +int32 + +
+(Optional) +

numSymbols defines number of symbols.


+
+letterCase
+ + +LetterCase + + +
+(Optional) +

letterCase defines to use lower-cases, upper-cases or mixed-cases of letters.


+
+

PersistentVolumeClaimSpec +

+

+(Appears on:ClusterComponentVolumeClaimTemplate) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+accessModes
+ + +[]Kubernetes core/v1.PersistentVolumeAccessMode + + +
+(Optional) +

accessModes contains the desired access modes the volume should have.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#access-modes-1.


+
+resources
+ + +Kubernetes core/v1.ResourceRequirements + + +
+(Optional) +

resources represents the minimum resources the volume should have.
If RecoverVolumeExpansionFailure feature is enabled users are allowed to specify resource requirements
that are lower than previous value but must still be higher than capacity recorded in the
status field of the claim.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources.


+
+storageClassName
+ +string + +
+(Optional) +

storageClassName is the name of the StorageClass required by the claim.
More info: https://kubernetes.io/docs/concepts/storage/persistent-volumes#class-1.


+
+

Phase +(string alias)

+

+(Appears on:ClusterDefinitionStatus, ClusterVersionStatus) +

+
+

Phase defines the ClusterDefinition and ClusterVersion CR .status.phase


+
+ + + + + + + + + + + + +
ValueDescription

"Available"

"Unavailable"

+

PodAntiAffinity +(string alias)

+

+(Appears on:Affinity) +

+
+

PodAntiAffinity defines pod anti-affinity strategy.


+
+ + + + + + + + + + + + +
ValueDescription

"Preferred"

"Required"

+

PointInTimeRefSpec +

+

+(Appears on:RestoreFromSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+time
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

specify the time point to restore, with UTC as the time zone.


+
+ref
+ + +RefNamespaceName + + +
+(Optional) +

specify a reference source cluster to restore


+
+

ProgressStatus +(string alias)

+

+(Appears on:ProgressStatusDetail) +

+
+

ProgressStatus defines the status of the opsRequest progress.


+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Failed"

"Pending"

"Processing"

"Succeed"

+

ProgressStatusDetail +

+

+(Appears on:OpsRequestComponentStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+group
+ +string + +
+(Optional) +

group describes which group the current object belongs to.
if the objects of a component belong to the same group, we can ignore it.


+
+objectKey
+ +string + +
+

objectKey is the unique key of the object.


+
+status
+ + +ProgressStatus + + +
+

status describes the state of processing the object.


+
+message
+ +string + +
+(Optional) +

message is a human readable message indicating details about the object condition.


+
+startTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

startTime is the start time of object processing.


+
+endTime
+ + +Kubernetes meta/v1.Time + + +
+(Optional) +

endTime is the completion time of object processing.


+
+

ProvisionPolicy +

+

+(Appears on:SystemAccountConfig) +

+
+

ProvisionPolicy defines the policy details for creating accounts.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ + +ProvisionPolicyType + + +
+

type defines the way to provision an account, either CreateByStmt or ReferToExisting.


+
+scope
+ + +ProvisionScope + + +
+

scope is the scope to provision account, and the scope could be anyPod or allPods.


+
+statements
+ + +ProvisionStatements + + +
+(Optional) +

statements will be used when Type is CreateByStmt.


+
+secretRef
+ + +ProvisionSecretRef + + +
+(Optional) +

secretRef will be used when Type is ReferToExisting.


+
+

ProvisionPolicyType +(string alias)

+

+(Appears on:ProvisionPolicy) +

+
+

ProvisionPolicyType defines the policy for creating accounts.


+
+ + + + + + + + + + + + +
ValueDescription

"CreateByStmt"

CreateByStmt will create account w.r.t. deletion and creation statement given by provider.


"ReferToExisting"

ReferToExisting will not create account, but create a secret by copying data from referred secret file.


+

ProvisionScope +(string alias)

+

+(Appears on:ProvisionPolicy) +

+
+

ProvisionScope defines the scope (within component) of provision.


+
+ + + + + + + + + + + + +
ValueDescription

"AllPods"

AllPods will create accounts for all pods belong to the component.


"AnyPods"

AndyPods will only create accounts on one pod.


+

ProvisionSecretRef +

+

+(Appears on:ProvisionPolicy) +

+
+

ProvisionSecretRef defines the information of secret referred to.


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name refers to the name of the secret.


+
+namespace
+ +string + +
+

namespace refers to the namespace of the secret.


+
+

ProvisionStatements +

+

+(Appears on:ProvisionPolicy) +

+
+

ProvisionStatements defines the statements used to create accounts.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+creation
+ +string + +
+

creation specifies statement how to create this account with required privileges.


+
+update
+ +string + +
+

update specifies statement how to update account’s password.


+
+deletion
+ +string + +
+(Optional) +

deletion specifies statement how to delete this account.
Used in combination with CreateionStatement to delete the account before create it.
For instance, one usually uses drop user if exists statement followed by create user statement to create an account.


+
+

Reconfigure +

+

+(Appears on:OpsRequestSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentOps
+ + +ComponentOps + + +
+

+(Members of ComponentOps are embedded into this type.) +

+
+configurations
+ + +[]Configuration + + +
+

configurations defines which components perform the operation.


+
+

ReconfiguringStatus +

+

+(Appears on:OpsRequestStatus) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+configurationStatus
+ + +[]ConfigurationStatus + + +
+

configurationStatus describes the status of the component reconfiguring.


+
+

RefNamespaceName +

+

+(Appears on:BackupRefSpec, PointInTimeRefSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+(Optional) +

specified the name


+
+namespace
+ +string + +
+(Optional) +

specified the namespace


+
+

ReloadOptions +

+

+(Appears on:ConfigConstraintSpec) +

+
+

ReloadOptions defines reload options
Only one of its members may be specified.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+unixSignalTrigger
+ + +UnixSignalTrigger + + +
+(Optional) +

unixSignalTrigger used to reload by sending a signal.


+
+shellTrigger
+ + +ShellTrigger + + +
+(Optional) +

shellTrigger performs the reload command.


+
+tplScriptTrigger
+ + +TPLScriptTrigger + + +
+(Optional) +

goTplTrigger performs the reload command.


+
+

ReplicationMemberStatus +

+

+(Appears on:ReplicationSetStatus) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+pod
+ +string + +
+

Pod name.


+
+

ReplicationSetSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+StatefulSetSpec
+ + +StatefulSetSpec + + +
+

+(Members of StatefulSetSpec are embedded into this type.) +

+
+switchPolicies
+ + +[]SwitchPolicy + + +
+

switchPolicies defines a collection of different types of switchPolicy, and each type of switchPolicy is limited to one.


+
+switchCmdExecutorConfig
+ + +SwitchCmdExecutorConfig + + +
+

switchCmdExecutorConfig configs how to get client SDK and perform switch statements.


+
+

ReplicationSetStatus +

+

+(Appears on:ClusterComponentStatus) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+primary
+ + +ReplicationMemberStatus + + +
+

Primary status.


+
+secondaries
+ + +[]ReplicationMemberStatus + + +
+(Optional) +

Secondaries status.


+
+

ResourceConstraint +

+

+(Appears on:ComponentResourceConstraintSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+cpu
+ + +CPUConstraint + + +
+

The constraint for vcpu cores.


+
+memory
+ + +MemoryConstraint + + +
+

The constraint for memory size.


+
+

RestoreFromSpec +

+

+(Appears on:OpsRequestSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+backup
+ + +[]BackupRefSpec + + +
+(Optional) +

use the backup name and component name for restore, support for multiple components’ recovery.


+
+pointInTime
+ + +PointInTimeRefSpec + + +
+(Optional) +

specified the point in time to recovery


+
+

RetentionSpec +

+

+(Appears on:BackupPolicy) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+ttl
+ +string + +
+(Optional) +

ttl is a time string ending with the ’d’|’D’|‘h’|‘H’ character to describe how long
the Backup should be retained. if not set, will be retained forever.


+
+

Schedule +

+

+(Appears on:BackupPolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+snapshot
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for snapshot backup.


+
+datafile
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for datafile backup.


+
+logfile
+ + +SchedulePolicy + + +
+(Optional) +

schedule policy for logfile backup.


+
+

SchedulePolicy +

+

+(Appears on:Schedule) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+cronExpression
+ +string + +
+

the cron expression for schedule, the timezone is in UTC. see https://en.wikipedia.org/wiki/Cron.


+
+enable
+ +bool + +
+

enable or disable the schedule.


+
+

ServicePort +

+

+(Appears on:ServiceSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

The name of this port within the service. This must be a DNS_LABEL.
All ports within a ServiceSpec must have unique names. When considering
the endpoints for a Service, this must match the ‘name’ field in the
EndpointPort.


+
+protocol
+ + +Kubernetes core/v1.Protocol + + +
+(Optional) +

The IP protocol for this port. Supports “TCP”, “UDP”, and “SCTP”.
Default is TCP.


+
+appProtocol
+ +string + +
+(Optional) +

The application protocol for this port.
This field follows standard Kubernetes label syntax.
Un-prefixed names are reserved for IANA standard service names (as per
RFC-6335 and https://www.iana.org/assignments/service-names).
Non-standard protocols should use prefixed names such as
mycompany.com/my-custom-protocol.


+
+port
+ +int32 + +
+

The port that will be exposed by this service.


+
+targetPort
+ + +Kubernetes api utils intstr.IntOrString + + +
+(Optional) +

Number or name of the port to access on the pods targeted by the service.
Number must be in the range 1 to 65535. Name must be an IANA_SVC_NAME.
If this is a string, it will be looked up as a named port in the
target Pod’s container ports. If this is not specified, the value
of the ‘port’ field is used (an identity map).
This field is ignored for services with clusterIP=None, and should be
omitted or set equal to the ‘port’ field.
More info: https://kubernetes.io/docs/concepts/services-networking/service/#defining-a-service


+
+

ServiceSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+ports
+ + +[]ServicePort + + +
+(Optional) +

The list of ports that are exposed by this service.
More info: https://kubernetes.io/docs/concepts/services-networking/service/#virtual-ips-and-service-proxies


+
+

ShellTrigger +

+

+(Appears on:ReloadOptions) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+exec
+ +string + +
+

exec used to execute for reload.


+
+scriptConfigMapRef
+ +string + +
+

scriptConfigMapRef used to execute for reload.


+
+namespace
+ +string + +
+(Optional) +

Specify the namespace of the referenced the tpl script ConfigMap object.
An empty namespace is equivalent to the “default” namespace.


+
+

SignalType +(string alias)

+

+(Appears on:UnixSignalTrigger) +

+
+

SignalType defines which signals are valid.


+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ValueDescription

"SIGABRT"

"SIGALRM"

"SIGBUS"

"SIGCHLD"

"SIGCONT"

"SIGFPE"

"SIGHUP"

"SIGILL"

"SIGINT"

"SIGIO"

"SIGKILL"

"SIGPIPE"

"SIGPROF"

"SIGPWR"

"SIGQUIT"

"SIGSEGV"

"SIGSTKFLT"

"SIGSTOP"

"SIGSYS"

"SIGTERM"

"SIGTRAP"

"SIGTSTP"

"SIGTTIN"

"SIGTTOU"

"SIGURG"

"SIGUSR1"

"SIGUSR2"

"SIGVTALRM"

"SIGWINCH"

"SIGXCPU"

"SIGXFSZ"

+

SnapshotPolicy +

+

+(Appears on:BackupPolicy) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+BasePolicy
+ + +BasePolicy + + +
+

+(Members of BasePolicy are embedded into this type.) +

+
+hooks
+ + +BackupPolicyHook + + +
+(Optional) +

execute hook commands for backup.


+
+

StatefulSetSpec +

+

+(Appears on:ClusterComponentDefinition, ConsensusSetSpec, ReplicationSetSpec) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+updateStrategy
+ + +UpdateStrategy + + +
+(Optional) +

updateStrategy, Pods update strategy.
In case of workloadType=Consensus the update strategy will be following:



serial: update Pods one by one that guarantee minimum component unavailable time.
Learner -> Follower(with AccessMode=none) -> Follower(with AccessMode=readonly) -> Follower(with AccessMode=readWrite) -> Leader
bestEffortParallel: update Pods in parallel that guarantee minimum component un-writable time.
Learner, Follower(minority) in parallel -> Follower(majority) -> Leader, keep majority online all the time.
parallel: force parallel


+
+llPodManagementPolicy
+ + +Kubernetes apps/v1.PodManagementPolicyType + + +
+(Optional) +

llPodManagementPolicy is the low-level controls how pods are created during initial scale up,
when replacing pods on nodes, or when scaling down.
OrderedReady policy specify where pods are created in increasing order (pod-0, then
pod-1, etc) and the controller will wait until each pod is ready before
continuing. When scaling down, the pods are removed in the opposite order.
Parallel policy specify create pods in parallel
to match the desired scale without waiting, and on scale down will delete
all pods at once.


+
+llUpdateStrategy
+ + +Kubernetes apps/v1.StatefulSetUpdateStrategy + + +
+(Optional) +

llUpdateStrategy indicates the low-level StatefulSetUpdateStrategy that will be
employed to update Pods in the StatefulSet when a revision is made to
Template. Will ignore updateStrategy attribute if provided.


+
+

StatefulSetWorkload +

+
+

StatefulSetWorkload interface


+
+

StatelessSetSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + +
FieldDescription
+updateStrategy
+ + +Kubernetes apps/v1.DeploymentStrategy + + +
+(Optional) +

updateStrategy defines the underlying deployment strategy to use to replace existing pods with new ones.


+
+

SwitchCmdExecutorConfig +

+

+(Appears on:ReplicationSetSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+CommandExecutorEnvItem
+ + +CommandExecutorEnvItem + + +
+

+(Members of CommandExecutorEnvItem are embedded into this type.) +

+
+switchSteps
+ + +[]SwitchStep + + +
+(Optional) +

switchSteps definition, users can customize the switching steps on the provided three roles - NewPrimary, OldPrimary, and Secondaries.
the same role can customize multiple steps in the order of the list, and KubeBlocks will perform switching operations in the defined order.
if switchStep is not set, we will try to use the built-in switchStep for the database engine with built-in support.


+
+

SwitchPolicy +

+

+(Appears on:ReplicationSetSpec) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ + +SwitchPolicyType + + +
+

switchPolicyType defines type of the switchPolicy.
MaximumAvailability: when the primary is active, do switch if the synchronization delay = 0 in the user-defined lagProbe data delay detection logic, otherwise do not switch. The primary is down, switch immediately.
MaximumDataProtection: when the primary is active, do switch if synchronization delay = 0 in the user-defined lagProbe data lag detection logic, otherwise do not switch. If the primary is down, if it can be judged that the primary and secondary data are consistent, then do the switch, otherwise do not switch.
Noop: KubeBlocks will not perform high-availability switching on components. Users need to implement HA by themselves.


+
+switchStatements
+ + +SwitchStatements + + +
+(Optional) +

switchStatements defines switching actions according to their respective roles, We divide all pods into three switchStatement role={Promote,Demote,Follow}.
Promote: candidate primary after elected, which to be promoted
Demote: primary before switch, which to be demoted
Follow: the other secondaries that are not selected as the primary, which to follow the new primary
if switchStatements is not set,we will try to use the built-in switchStatements for the database engine with built-in support.


+
+

SwitchPolicyType +(string alias)

+

+(Appears on:ClusterSwitchPolicy, SwitchPolicy) +

+
+

SwitchPolicyType defines switchPolicy type.


+
+ + + + + + + + + + + + + + +
ValueDescription

"MaximumAvailability"

"MaximumDataProtection"

"Noop"

+

SwitchStatements +

+

+(Appears on:SwitchPolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+promote
+ +[]string + +
+(Optional) +

promote defines the switching actions for the candidate primary which to be promoted.


+
+demote
+ +[]string + +
+(Optional) +

demote defines the switching actions for the old primary which to be demoted.


+
+follow
+ +[]string + +
+(Optional) +

follow defines the switching actions for the other secondaries which are not selected as the primary.


+
+

SwitchStep +

+

+(Appears on:SwitchCmdExecutorConfig) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+CommandExecutorItem
+ + +CommandExecutorItem + + +
+

+(Members of CommandExecutorItem are embedded into this type.) +

+
+role
+ + +SwitchStepRole + + +
+

role determines which role to execute the command on, role is divided into three roles NewPrimary, OldPrimary, and Secondaries.


+
+

SwitchStepRole +(string alias)

+

+(Appears on:SwitchStep) +

+
+

SwitchStepRole defines the role to execute the switch command.


+
+ + + + + + + + + + + + + + +
ValueDescription

"NewPrimary"

"OldPrimary"

"Secondaries"

+

SystemAccountConfig +

+

+(Appears on:SystemAccountSpec) +

+
+

SystemAccountConfig specifies how to create and delete system accounts.


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ + +AccountName + + +
+

name is the name of a system account.


+
+provisionPolicy
+ + +ProvisionPolicy + + +
+

provisionPolicy defines how to create account.


+
+

SystemAccountShortSpec +

+

+(Appears on:ClusterComponentVersion) +

+
+

SystemAccountShortSpec is a short version of SystemAccountSpec, with only CmdExecutorConfig field.


+
+ + + + + + + + + + + + + +
FieldDescription
+cmdExecutorConfig
+ + +CommandExecutorEnvItem + + +
+

cmdExecutorConfig configs how to get client SDK and perform statements.


+
+

SystemAccountSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+

SystemAccountSpec specifies information to create system accounts.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+cmdExecutorConfig
+ + +CmdExecutorConfig + + +
+

cmdExecutorConfig configs how to get client SDK and perform statements.


+
+passwordConfig
+ + +PasswordConfig + + +
+

passwordConfig defines the pattern to generate password.


+
+accounts
+ + +[]SystemAccountConfig + + +
+

accounts defines system account config settings.


+
+

TLSSecretRef +

+

+(Appears on:Issuer) +

+
+

TLSSecretRef defines Secret contains Tls certs


+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

Name of the Secret


+
+ca
+ +string + +
+

CA cert key in Secret


+
+cert
+ +string + +
+

Cert key in Secret


+
+key
+ +string + +
+

Key of TLS private key in Secret


+
+

TPLScriptTrigger +

+

+(Appears on:ReloadOptions) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+scriptConfigMapRef
+ +string + +
+

scriptConfigMapRef used to execute for reload.


+
+namespace
+ +string + +
+(Optional) +

Specify the namespace of the referenced the tpl script ConfigMap object.
An empty namespace is equivalent to the “default” namespace.


+
+sync
+ +bool + +
+(Optional) +

Specify synchronize updates parameters to the config manager.


+
+

TargetInstance +

+

+(Appears on:BasePolicy) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+role
+ +string + +
+(Optional) +

select instance of corresponding role for backup, role are:
- the name of Leader/Follower/Leaner for Consensus component.
- primary or secondary for Replication component.
finally, invalid role of the component will be ignored.
such as if workload type is Replication and component’s replicas is 1,
the secondary role is invalid. and it also will be ignored when component is Stateful/Stateless.
the role will be transformed to a role LabelSelector for BackupPolicy’s target attribute.


+
+account
+ +string + +
+(Optional) +

refer to spec.componentDef.systemAccounts.accounts[*].name in ClusterDefinition.
the secret created by this account will be used to connect the database.
if not set, the secret created by spec.ConnectionCredential of the ClusterDefinition will be used.
it will be transformed to a secret for BackupPolicy’s target secret.


+
+connectionCredentialKey
+ + +ConnectionCredentialKey + + +
+

connectionCredentialKey defines connection credential key in secret
which created by spec.ConnectionCredential of the ClusterDefinition.
it will be ignored when “account” is set.


+
+

TenancyType +(string alias)

+

+(Appears on:Affinity) +

+
+

TenancyType for cluster tenant resources.


+
+ + + + + + + + + + + + +
ValueDescription

"DedicatedNode"

"SharedNode"

+

TerminationPolicyType +(string alias)

+

+(Appears on:ClusterSpec) +

+
+

TerminationPolicyType defines termination policy types.


+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Delete"

"DoNotTerminate"

"Halt"

"WipeOut"

+

UnixSignalTrigger +

+

+(Appears on:ReloadOptions) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+signal
+ + +SignalType + + +
+

signal is valid for unix signal.
e.g: SIGHUP
url: ../../internal/configuration/configmap/handler.go:allUnixSignals


+
+processName
+ +string + +
+

processName is process name, sends unix signal to proc.


+
+

UpdateStrategy +(string alias)

+

+(Appears on:StatefulSetSpec) +

+
+

UpdateStrategy defines Cluster Component update strategy.


+
+ + + + + + + + + + + + + + +
ValueDescription

"BestEffortParallel"

"Parallel"

"Serial"

+

UpdatedParameters +

+

+(Appears on:ConfigurationStatus) +

+
+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+addedKeys
+ +map[string]string + +
+(Optional) +

addedKeys describes the key added.


+
+deletedKeys
+ +map[string]string + +
+(Optional) +

deletedKeys describes the key deleted.


+
+updatedKeys
+ +map[string]string + +
+(Optional) +

updatedKeys describes the key updated.


+
+

Upgrade +

+

+(Appears on:OpsRequestSpec) +

+
+

Upgrade defines the variables of upgrade operation.


+
+ + + + + + + + + + + + + +
FieldDescription
+clusterVersionRef
+ +string + +
+

clusterVersionRef references ClusterVersion name.


+
+

UpgradePolicy +(string alias)

+

+(Appears on:ConfigurationStatus) +

+
+

UpgradePolicy defines the policy of reconfiguring.


+
+ + + + + + + + + + + + + + + + + + + + +
ValueDescription

"autoReload"

"none"

"simple"

"operatorSyncUpdate"

"parallel"

"rolling"

+

VersionsContext +

+

+(Appears on:ClusterComponentVersion) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+initContainers
+ + +[]Kubernetes core/v1.Container + + +
+(Optional) +

Provide ClusterDefinition.spec.componentDefs.podSpec.initContainers override
values, typical scenarios are application container image updates.


+
+containers
+ + +[]Kubernetes core/v1.Container + + +
+(Optional) +

Provide ClusterDefinition.spec.componentDefs.podSpec.containers override
values, typical scenarios are application container image updates.


+
+

VerticalScaling +

+

+(Appears on:OpsRequestSpec) +

+
+

VerticalScaling defines the variables that need to input when scaling compute resources.


+
+ + + + + + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentOps
+ + +ComponentOps + + +
+

+(Members of ComponentOps are embedded into this type.) +

+
+ResourceRequirements
+ + +Kubernetes core/v1.ResourceRequirements + + +
+

+(Members of ResourceRequirements are embedded into this type.) +

+

resources specifies the computing resource size of verticalScaling.


+
+class
+ +string + +
+(Optional) +

class specifies the class name of the component


+
+

VolumeExpansion +

+

+(Appears on:OpsRequestSpec) +

+
+

VolumeExpansion defines the variables of volume expansion operation.


+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+ComponentOps
+ + +ComponentOps + + +
+

+(Members of ComponentOps are embedded into this type.) +

+
+volumeClaimTemplates
+ + +[]OpsRequestVolumeClaimTemplate + + +
+

volumeClaimTemplates specifies the storage size and volumeClaimTemplate name.


+
+

VolumeType +(string alias)

+

+(Appears on:VolumeTypeSpec) +

+
+

VolumeType defines volume type for backup data or log.


+
+ + + + + + + + + + + + +
ValueDescription

"data"

"log"

+

VolumeTypeSpec +

+

+(Appears on:ClusterComponentDefinition) +

+
+
+ + + + + + + + + + + + + + + + + +
FieldDescription
+name
+ +string + +
+

name definition is the same as the name of the VolumeMounts field in PodSpec.Container,
similar to the relations of Volumes[].name and VolumesMounts[].name in Pod.Spec.


+
+type
+ + +VolumeType + + +
+(Optional) +

type is in enum of {data, log}.
VolumeTypeData: the volume is for the persistent data storage.
VolumeTypeLog: the volume is for the persistent log storage.


+
+

WorkloadType +(string alias)

+

+(Appears on:ClusterComponentDefinition, OpsRequestComponentStatus) +

+
+

WorkloadType defines ClusterDefinition’s component workload type.


+
+ + + + + + + + + + + + + + + + +
ValueDescription

"Consensus"

"Replication"

"Stateful"

"Stateless"

+
+

+Generated with gen-crd-api-reference-docs +

diff --git a/docs/user_docs/api/_category_.yml b/docs/user_docs/api/_category_.yml deleted file mode 100644 index 2841f6b76..000000000 --- a/docs/user_docs/api/_category_.yml +++ /dev/null @@ -1,4 +0,0 @@ -position: 11 -label: KubeBlocks API -collapsible: true -collapsed: true \ No newline at end of file diff --git a/docs/user_docs/api/lifecycle-management/_category_.yml b/docs/user_docs/api/lifecycle-management/_category_.yml deleted file mode 100644 index 17af7cff4..000000000 --- a/docs/user_docs/api/lifecycle-management/_category_.yml +++ /dev/null @@ -1,4 +0,0 @@ -position: 1 -label: Lifecycle management -collapsible: true -collapsed: true \ No newline at end of file diff --git a/docs/user_docs/api/lifecycle-management/lifecycle-management-api.md b/docs/user_docs/api/lifecycle-management/lifecycle-management-api.md deleted file mode 100644 index 4572d907c..000000000 --- a/docs/user_docs/api/lifecycle-management/lifecycle-management-api.md +++ /dev/null @@ -1,307 +0,0 @@ ---- -title: Lifecycle -description: The API of KubeBlocks lifecycle management -sidebar_position: 1 -sidebar_label: Lifecycle ---- - -# Lifecycle - -This guide describes the details of KubeBlocks lifecycle API. KubeBlocks API is declarative and enables providers to describe the database cluster typology and lifecycle by YAML files, thus dynamically generating a management and control task flow to provide users with a consistent database operation experience. KubeBlocks has three APIs, namely `ClusterDefinition`, `ClusterVersion`, and `Cluster`. `ClusterDefinition` and `AppVersion` are designed for providers and `Cluster` is for end users. - -## ClusterDefinition (for providers) - -`ClusterDefinition` is a Kubernetes custom resource definition and enables providers to describe the cluster typology and the dependencies among roles in operation tasks. - -### ClusterDefinition spec - -#### spec.workloadType - -`spec.workloadType` stands for the component workload type. KubeBlocks supports `stateless`, `stateful`, and `consensus`. `stateless` is set as default. - -#### spec.consensusSpec - -When the `spec.workloadType` is set as `consensus`, `spec.consensusSpec` is required. - -- `leader` - - `leader` stands for the leader node and provides write capability. - - - `name` - - `name` stands for the role name and comes from the result of `roleObserveQuery`. - - - `accessMode` - - `accessMode` stands for the service capability. There are three types available, namely `readWrite`, `readonly`, and `none`. `readWrite` provides read and write services. `readonly` provides write service. `none` does not provide any service. - -- `followers` - - `followers` participates in the election. Its name and access mode are defined by default. - -- `learner` - - `learner` does not participate in the election. Its name and access mode are defined by default. Its `replicas` stands for the pod amount and it is non-overridable in the cluster. - -- `updateStrategy` - - `updateStrategy` stands for the updating strategy. `serial`, `bestEffortParallel` and `parallel` are selectable. `serial` is set as the default. - - - `serial` stands for the serial executor. For example, when MySQL three-node cluster is upgrading, this process will be executed following this order, `learner1 -> learner2 -> logger -> follower -> leader`. - - `bestEffortParallel` means the controller tries to execute in parallel. Under the same scene in `serial`, the process will be executed following this order, `learner1, learner2, logger in parallel way -> follower -> leader`. The majority with election rights will be kept online during the operation process. - - `parallel` will force a parallel executor. - -#### spec.defaultTerminationPolicy - -`spec.defaultTerminatingPolicy` can be set as `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. - -#### spec.connectionCredential - -`spec.connectionCredential` is used to create a connection credential secret. - -Requirements for `.spec.connectionCredential`: - - 8 random characters `$(RANDOM_PASSWD)` placeholder. - - self reference map object `$(CONN_CREDENTIAL)[.])` - - Connection credential secret name place holder should be `$(CONN_CREDENTIAL_SECRET_NAME)`. - - Usage example: - ``` - spec: - connectionCredential: - username: "admin-password" - password: "$(RANDOM_PASSWD)" - "$(CONN_CREDENTIAL).username": "$(CONN_CREDENTIAL).password" - # output: - spec: - connectionCredential: - username: "admin-password" - password: "" - "admin-password": "" - ``` - -### Example - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterDefinition -metadata: - name: wesql -spec: - componentDefs: - - name: mysql-a - workloadType: consensus - consensusSpec: - leader: - name: "leader" - accessMode: readWrite - followers: - - name: "follower" - accessMode: readonly - service: - ports: - - protocol: TCP - port: 3306 - targetPort: 3306 - type: LoadBalancer - readonlyService: - ports: - - protocol: TCP - port: 3306 - targetPort: 3306 - type: LoadBalancer - podSpec: - containers: - - name: mysql - imagePullPolicy: IfNotPresent - volumeMounts: - - mountPath: /data - name: data - ports: - - containerPort: 3306 - protocol: TCP - name: mysql - - containerPort: 13306 - protocol: TCP - name: paxos - env: - - name: MYSQL_ROOT_HOST - value: "%" - - name: MYSQL_ROOT_USER - value: "root" - - name: MYSQL_ROOT_PASSWORD - value: - - name: MYSQL_ALLOW_EMPTY_PASSWORD - value: "yes" - - name: MYSQL_DATABASE - value: "mydb" - - name: MYSQL_USER - value: "u1" - - name: MYSQL_PASSWORD - value: "u1" - - name: CLUSTER_ID - value: 1 - - name: CLUSTER_START_INDEX - value: 1 - - name: REPLICATIONUSER - value: "replicator" - - name: REPLICATION_PASSWORD - value: - - name: MYSQL_TEMPLATE_CONFIG - values: - - name: MYSQL_CUSTOM_CONFIG - values: - - name: MYSQL_DYNAMIC_CONFIG - values: - command: [ "/bin/bash", "-c" ] - args: - - > - cluster_info=""; - for (( i=0; i< $KB_REPLICASETS_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_REPLICASETS_"$i"_HOSTNAME) - cluster_info="$cluster_info$host:13306"; - done; - idx=0; - while IFS='-' read -ra ADDR; do - for i in "${ADDR[@]}"; do - idx=$i; - done; - done <<< "$KB_MY_POD_NAME"; - echo $idx; - cluster_info="$cluster_info@$(($idx+1))"; - echo $cluster_info; echo {{ .Values.cluster.replicaSetCount }}; - docker-entrypoint.sh mysqld --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$cluster_info" --cluster-id=$CLUSTER_ID -``` - -:::note - -`envs` automatically injected by KubeBlocks can be used in the above `env` and `args` fields. - -- KB_POD_NAME - Pod Name -- KB_NAMESPACE - Namespace -- KB_SA_NAME - Service Account Name -- KB_NODENAME - Node Name -- KB_HOST_IP - Host IP address -- KB_POD_IP - Pod IP address -- KB_POD_IPS - Pod IP addresses -- KB_CLUSTER_NAME - KubeBlock Cluster API object name -- KB_COMP_NAME - Running pod's KubeBlock Cluster API object's .spec.components.name -- KB_CLUSTER_COMP_NAME - Running pod's KubeBlock Cluster API object's <.metadata.name>-<.spec.components..name>, same name is used for Deployment or StatefulSet workload name, and Service object name - -::: - -## ClusterVersion (for providers) - -`ClusterVersion` enables providers to describe the image versions and condition variables of the corresponding database versions. - -### ClusterVersion spec - -#### spec.clusterDefinitionRef - -`spec.clusterDefinitionRef` refers to `ClusterDefinition` and its value should be the same as `ClusterDefinition`. - -#### spec.component - -`type` should be the same component name as `ClusterDefinition`. - -### ClusterVersion status - -You can check `phase` and `message` to view the executing status and result. - -### Example - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterVersion -metadata: - name: ac-mysql-8.0.30 -spec: - clusterDefinitionRef: apecloud-mysql - components: - - type: wesql - versionsContext: - containers: - - name: mysql - image: apecloud/apecloud-mysql-server:8.0.30-4.alpha1.20221031.g1aa54a3 - imagePullPolicy: IfNotPresent -``` - -## Cluster (for end users) - -`Cluster` enables end users to describe the database cluster they want to create. - -### Cluster spec - -#### spec.clusterDefinitionRef - -`spec.clusterDefinitionRef` refers to `ClusterDefinition` and its value should be the same as `ClusterDefinition`. - -#### spec.clusterVersionRef - -It refers to ClusterVersion and its value should be the same as `ClusterVersion`. - -#### spec.components - -`type` points to the component name in ClusterDefinition. - -`replicas`: If you edit `replicas`, horizontal scaling will be triggered. If the amount of `replicas` does not meet the limits of `definition`, an error occurs. - -`resources`: If you edit the `requests` and `limits` of `resources`, vertical scaling will be triggered. - -#### spec.volumeClaimTemplates - -`volumeClaimTemplates` is a list of claims that pods are allowed to refer to. The StatefulSet controller is responsible for mapping network identities to claims in a way that maintains the identity of a pod. Every claim in this list must have at least one matching (by name) `volumeMount` in one container in the template. A claim in this list takes precedence over any volumes in the template with the same name. -`PersistentVolumeClaim` is a user's request for and claim to a persistent volume. - -### Cluster status - -`status` describes the current state and progress of the `Cluster`. - -#### cluster.phase - -`cluster.phase` includes `Running`, `Failed`, `Creating`, `Upgrading`, `Scaling`, `Deleting`, and `Abnormal`. You can observe the executing status by `phase` changes. - -### Example - -The following are examples of ApeCloud MySQL three-node clusters. - -- Standard version: - - ``` - apiVersion: apps.kubeblocks.io/v1alpha1 - kind: Cluster - metadata: - name: mysql-a-series-standard - spec: - clusterDefinitionRef: wesql - clusterVersionRef: ac-mysql-8.0.30 - components: - - name: "mysql-a-1" - type: mysql-a - terminationPolicy: Halt - ``` - -- Enterprise version: - - ``` - apiVersion: apps.kubeblocks.io/v1alpha1 - kind: Cluster - metadata: - name: mysql-a-series-enterprise - spec: - clusterDefinitionRef: wesql - clusterVersionRef: ac-mysql-8.0.30 - components: - - name: "mysql-a-2" - type: mysql-a - replicas: 3 - resources: - requests: - cpu: 32 - memory: 128Gi - limits: - cpu: 32 - memory: 128Gi - terminationPolicy: Halt - ``` diff --git a/docs/user_docs/api/lifecycle-management/ops-request-api.md b/docs/user_docs/api/lifecycle-management/ops-request-api.md deleted file mode 100644 index 4b6d2b213..000000000 --- a/docs/user_docs/api/lifecycle-management/ops-request-api.md +++ /dev/null @@ -1,263 +0,0 @@ ---- -title: OpsRequest -description: The API of KubeBlocks OpsRequest -sidebar_position: 2 -sidebar_label: OpsRequest ---- - -# OpsRequest - -## What is OpsRequest - -`OpsRequest` is a Kubernetes Custom Resource Definitions (CRD). You can initiate an operation request via `OpsRequest` to operate database clusters. KubeBlocks supports the following operation tasks: database restarting, database version upgrading, vertical scaling, horizontal scaling, and volume expansion. - -## OpsRequest CRD Specifications - -The following are examples of `OpsRequest` CRs for different operations: - -### Example for restarting a KubeBlocks cluster - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: OpsRequest -metadata: - name: mysql-restart - namespace: default -spec: - clusterRef: mysql-cluster-01 - ttlSecondsAfterSucceed: 3600 - type: Restart - restart: - - componentName: replicasets -status: - StartTimestamp: "2022-09-27T06:01:31Z" - completionTimestamp: "2022-09-27T06:02:30Z" - components: - replicasets: - phase: Running - conditions: - - lastTransitionTime: "2022-09-27T06:01:31Z" - message: 'Controller has started to progress the OpsRequest: mysql-restart in - Cluster: mysql-cluster-01' - reason: OpsRequestProgressingStarted - status: "True" - type: Progressing - - lastTransitionTime: "2022-09-27T06:01:31Z" - message: 'OpsRequest: mysql-restart is validated' - reason: ValidateOpsRequestPassed - status: "True" - type: Validated - - lastTransitionTime: "2022-09-27T06:01:31Z" - message: 'start restarting database in Cluster: mysql-cluster-01' - reason: RestartingStarted - status: "True" - type: Restarting - - lastTransitionTime: "2022-09-27T06:02:30Z" - message: 'Controller has successfully processed the OpsRequest: mysql-restart - in Cluster: mysql-cluster-01' - reason: OpsRequestProcessedSuccessfully - status: "True" - type: Succeed - observedGeneration: 1 - phase: Succeed -``` - -### Example for vertical scaling - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: OpsRequest -metadata: - generate-name: verticalscaling- -spec: - # cluster ref - clusterRef: myMongoscluster - type: VerticalScaling - verticalScaling: - - componentName: shard1 - requests: - memory: "150Mi" - cpu: "0.1" - limits: - memory: "250Mi" - cpu: "0.2" -``` - -### Example for horizontal scaling - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: OpsRequest -metadata: - name: ops-xxxx -spec: - # cluster ref - clusterRef: myMongoscluster - type: HorizontalScaling - componentOps: - - componentNames: [shard1] - horizontalScaling: - replicas: 3 -``` - -### Example for upgrading a KubeBlocks cluster - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: OpsRequest -metadata: - name: ops-xxxx -spec: - # cluster ref - clusterRef: myMongoscluster - type: Upgrade - upgrade: - # Upgrade to the specidief clusterversion - clusterVersionRef: 5.0.1 -``` - -### Example for volume expansion - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: OpsRequest -metadata: - name: ops-xxxx -spec: - # cluster ref - clusterRef: myMongoscluster - type: VolumeExpansion - volumeExpansion: - - componentName: shard1 - volumeClaimTemplates: - - name: data - storage: "2Gi" -``` - -## OpsRequest spec - -An `OpsRequest` object has the following fields in the `spec` section. - -### spec.clusterRef - -`spec.clusterRef` is a required field and points to the cluster to which the current OpsRequest is applied. Its value should be filled as `cluster.metadata.name` - -### spec.type - -`spec.type` is a required field. It points to the operation OpsRequest uses and decides the operation OpsRequest performs. - -The following types of operations are allowed in `OpsRequest`. - -- `Upgrade` -- `VerticalScaling` -- `VolumeExpansion` -- `HorizontalScaling` -- `Restart` - -### spec.clusterOps - -It indicates the cluster-level operation. Its attribute is as follows: - -- Upgrade - - It specifies the information for upgrading clusterversion and `spec.type` should be `Upgrade` to make it effective. Its attribute is as follows: - - `clusterVersion` specifies the clusterVersion object used in the current upgrading operation. - Value: `ClusterVersion.metadata.name` - -### spec.componentOps - -It indicates the component-level operation and is an array that supports operations of different parameters. Its attribute is as follows: - -- componentNames - - It is a required field and specifies the component to which the operation is applied and is a `cluster.component.name` array. - -- verticalScaling - - `verticalScaling` scales up and down the computing resources of a component. Its value is an object of Kubernetes container resources. For example, - - ``` - verticalScaling: - requests: - memory: "200Mi" - cpu: "0.1" - limits: - memory: "300Mi" - cpu: "0.2" - ``` - -- volumeExpansion - - `volumeExpansion`, a volume array, indicates the storage resources that apply to each component database engine. `spec.type` should be `VolumeExpansion` to make it effective. Its attributes are as follows: - - - storage: the storage space - - name: the name of volumeClaimTemplates. - -- horizontalScaling - - `horizontalScaling` is the replicas amount of the current component. `spec.type` should be `HorizontalScaling` to make it effective. Its value includes `componentName.replicas`. For example: - - ``` - horizontalScaling: - replicas: 3 - ``` - -## OpsRequest status - -`status` describes the current state and progress of the `OpsRequest` operation. It has the following fields: - -### status.observedGeneration - -It corresponds to `metadata.generation`. - -### status.phase - -`OpsRequest` task is one-time and is deleted after the operation succeeds. - -`status.phase` indicates the overall phase of the operation for this OpsRequest. It can have the following four values: - -| **Phase** | **Meaning** | -|:--- | :--- | -| Succeed | OpsRequest is performed successfully and cannot be edited. | -| Running | OpsRequest is running and cannot be edited. | -| Pending | OpsRequest is waiting for processing. | -| Failed | OpsRequest failed. | - -### status.conditions - -`status.conditions` is the general data structure provided by Kubernetes, indicating the resource state. It can provide more detailed information (such as state switch time and upgrading time) than `status` does and functions as an extension mechanism. - -`condition.type` indicates the type of the last OpsRequest status. - -| **Type** | **Meaning** | -|:--- | :--- | -| Progressing | OpsRequest is under controller processing. | -| Validated | OpsRequest is validated. | -| Restarting | Start processing restart ops. | -| VerticalScaling | Start scaling resources vertically. | -| HorizontalScaling | Start scaling nodes horizontally. | -| VolumeExpanding | Start process volume expansion. | -| Upgrading | Start upgrading. | -| Succeed | Operation is proceeded successfully. | -| Failed | Operation failed. | - -`condition.status` indicates whether this condition is applicable. The results of `condition.status` include `True`, `False`, and `Unknown`, respectively standing for success, failure, and unknown error. - -`condition.Reason` indicates why the current condition changes. Each reason is only one word and exclusive. - -| **Reason** | **Meanings** | -| :--- | :--- | -| OpsRequestProgressingStarted | Controller is processing operations. | -| Starting | Controller starts running operations. | -| ValidateOpsRequestPassed | OpsRequest is validated. | -| OpsRequestProcessedSuccessfully | OpsRequest is processed. | -| RestartingStarted | Restarting started. | -| VerticalScalingStarted | VerticalScaling started. | -| HorizontalScalingStarted | HorizontalScaling started. | -| VolumeExpandingStarted | VolumeExpanding started. | -| UpgradingStarted | Upgrade started. | -| ClusterPhaseMisMatch | The cluster status mismatches. | -| OpsTypeNotSupported | The cluster does not support this operation. | -| ClusterExistOtherOperation | Another mutually exclusive operation is running.| -| ClusterNotFound | The specified cluster is not found. | -| VolumeExpansionValidateError | Volume expansion validation failed. | diff --git a/docs/user_docs/api/observability/access-logs.md b/docs/user_docs/api/observability/access-logs.md deleted file mode 100644 index d6493c449..000000000 --- a/docs/user_docs/api/observability/access-logs.md +++ /dev/null @@ -1,264 +0,0 @@ ---- -title: Access logs -description: The API of accessing logs -sidebar_position: 1 ---- - -# Access logs - -## API definition - -Add the log-related specification to the API file to enable this function for a cluster. - -### Cluster (for users) - -The `enabledLogs` string is added in `spec.components` to mark whether to enable the log-related function of a cluster. - -***Example*** - -Add the `enabledLogs` key and fill its value with a log type defined by the provider to enable the log function. - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - name: mysql-cluster-01 - namespace: default -spec: - clusterDefinitionRef: mysql-cluster-definition - clusterVersionRef: clusterversion-mysql-latest - components: - - name: replicasets - type: replicasets - enabledLogs: - - slow - volumeClaimTemplates: - - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi - - name: log - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi -``` - -### ClusterDefinition (for providers) - -The `logsConfigs` string is used to search log files. Fill the `name` with the custom log type and `filePathPattern` with the path of the log file. `name` can be defined by providers and is the only identifier. -Fill the value of `configTemplateRefs` with the kernel parameters. - -***Example*** - -Here is an example of configuring the error log and slow log. - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterDefinition -metadata: - name: mysql-cluster-definition -spec: - componentDefs: - - name: replicasets - characterType: mysql - monitor: - builtIn: true - logConfigs: - - name: error - filePathPattern: /log/mysql/log/mysqld.err - - name: slow - filePathPattern: /log/mysql/mysqld-slow.log - configTemplateRefs: - - name: mysql-tree-node-template-8.0 - volumeName: mysql-config - workloadType: Consensus - consensusSpec: - leader: - name: leader - accessMode: ReadWrite - followers: - - name: follower - accessMode: Readonly - podSpec: - containers: - - name: mysql - imagePullPolicy: IfNotPresent - ports: - - containerPort: 3306 - protocol: TCP - name: mysql - - containerPort: 13306 - protocol: TCP - name: paxos - volumeMounts: - - mountPath: /data - name: data - - mountPath: /log - name: log - - mountPath: /data/config/mysql - name: mysql-config - env: - - name: "MYSQL_ROOT_PASSWORD" - valueFrom: - secretKeyRef: - name: $(KB_SECRET_NAME) - key: password - command: ["/usr/bin/bash", "-c"] - args: - - > - cluster_info=""; - for (( i=0; i<$KB_REPLICASETS_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_REPLICASETS_"$i"_HOSTNAME) - cluster_info="$cluster_info$host:13306"; - done; - idx=0; - while IFS='-' read -ra ADDR; do - for i in "${ADDR[@]}"; do - idx=$i; - done; - done <<< "$KB_POD_NAME"; - echo $idx; - cluster_info="$cluster_info@$(($idx+1))"; - echo $cluster_info; - mkdir -p /data/mysql/log; - mkdir -p /data/mysql/data; - mkdir -p /data/mysql/std_data; - mkdir -p /data/mysql/tmp; - mkdir -p /data/mysql/run; - chmod +777 -R /data/mysql; - docker-entrypoint.sh mysqld --defaults-file=/data/config/mysql/my.cnf --cluster-start-index=1 --cluster-info="$cluster_info" --cluster-id=1 -``` - -#### How to configure `name` and `filePath` under different conditions - -- Multiple files under one path - - Here is an example of how to write three files at the same time in the internal PostgreSQL audit file of Alibaba Cloud. - - ``` - logsConfig: - - # `name` is customized by the provider and is the only identifier. - - name: audit - # The path information of the log file. - filePath: /postgresql/log/postgresql_[0-2]_audit.log - ``` - -- Multiple paths (including a path under which there are single or multiple files) - - For the log which is sent to multiple paths and is separated into multiple types, the configurations are as follows: - - ``` - logsConfig: - # The following is the audit log of configuring multiple paths. - # `name` is customized by the provider and is the only identifier. - - name: audit1 - # The path information of the log file. - filePath: /var/log1/postgresql_*_audit.log - - name: audit2 - # The path information of the log file. - filePath: /var/log2/postgresql_*_audit.log - ``` - -### ConfigTemplate (for providers) - -When opening a certain log of a certain engine, write the related kernel configuration in `ConfigTemplate` to make sure the log file can be output correctly. - -***Example*** - -Here is an example. - -``` -apiVersion: v1 -kind: ConfigMap -metadata: - name: mysql-tree-node-template-8.0 -data: - my.cnf: |- - [mysqld] - loose_query_cache_type = OFF - loose_query_cache_size = 0 - loose_innodb_thread_concurrency = 0 - loose_concurrent_insert = 0 - loose_gts_lease = 2000 - loose_log_bin_use_v1_row_events = off - loose_binlog_checksum = crc32 - - {{- if mustHas "error" $.Component.EnabledLogs }} - # Mysql error log - log_error={{ $log_root }}/mysqld.err - {{- end }} - - {{- if mustHas "slow" $.Component.EnabledLogs }} - # MySQL Slow log - slow_query_log=ON - long_query_time=5 - log_output=FILE - slow_query_log_file={{ $log_root }}/mysqld-slow.log - {{- end }} -... -``` - -### Status - -The log-related function, similar to a warning, neither affects the main flow of control and management nor changes `Phase` or `Generation`. It adds a `conditions` field in `cluster API status` to store the warning of a cluster. - -``` -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - ... -spec: - ... -status: - # metav1.Condition[] - conditions: - - key: spec.components[replicasets].logs - reason: 'xxx' is invalid - - components: - # component name - replicasets: - phase: Failed - message: Volume snapshot not support -``` - -Run `kbcli describe cluster ` and its output information is as follows: - -``` -Status: - Cluster Def Generation: 3 - Components: - Replicasets: - Phase: Running - Conditions: - Last Transition Time: 2022-11-11T03:57:42Z - Message: EnabledLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Reason: EnabledLogsListValidateFail - Status: False - Type: ValidateEnabledLogs - Observed Generation: 2 - Operations: - Horizontal Scalable: - Name: replicasets - Restartable: - replicasets - Vertical Scalable: - replicasets - Phase: Running -Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Creating 49s cluster-controller Start Creating in Cluster: release-name-error - Warning EnabledLogsListValidateFail 49s cluster-controller EnabledLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Normal Running 36s cluster-controller Cluster: release-name-error is ready, current phase is Running -``` \ No newline at end of file diff --git a/docs/user_docs/cli/_category_.yml b/docs/user_docs/cli/_category_.yml index ddf878812..268abe220 100644 --- a/docs/user_docs/cli/_category_.yml +++ b/docs/user_docs/cli/_category_.yml @@ -1,4 +1,4 @@ -position: 10 +position: 12 label: Command Line collapsible: true collapsed: true diff --git a/docs/user_docs/cli/cli.md b/docs/user_docs/cli/cli.md index cbbf95f9e..e961e8108 100644 --- a/docs/user_docs/cli/cli.md +++ b/docs/user_docs/cli/cli.md @@ -23,17 +23,18 @@ Manage alert receiver, include add, list and delete receiver. * [kbcli alert list-receivers](kbcli_alert_list-receivers.md) - List all alert receivers. -## [backup-config](kbcli_backup-config.md) +## [bench](kbcli_bench.md) -KubeBlocks backup config. +Run a benchmark. +* [kbcli bench sysbench](kbcli_bench_sysbench.md) - run a SysBench benchmark -## [bench](kbcli_bench.md) +## [builder](kbcli_builder.md) -Run a benchmark. +builder command. -* [kbcli bench tpcc](kbcli_bench_tpcc.md) - Run a TPCC benchmark. +* [kbcli builder template](kbcli_builder_template.md) - tpl - a developer tool integrated with KubeBlocks that can help developers quickly generate rendered configurations or scripts based on Helm templates, and discover errors in the template before creating the database cluster. ## [class](kbcli_class.md) @@ -49,8 +50,9 @@ Manage classes Cluster command. -* [kbcli cluster backup](kbcli_cluster_backup.md) - Create a backup. -* [kbcli cluster configure](kbcli_cluster_configure.md) - Reconfigure parameters with the specified components in the cluster. +* [kbcli cluster backup](kbcli_cluster_backup.md) - Create a backup for the cluster. +* [kbcli cluster cancel-ops](kbcli_cluster_cancel-ops.md) - cancel the pending/creating/running OpsRequest which type is vscale or hscale. +* [kbcli cluster configure](kbcli_cluster_configure.md) - Configure parameters with the specified components in the cluster. * [kbcli cluster connect](kbcli_cluster_connect.md) - Connect to a cluster or instance. * [kbcli cluster create](kbcli_cluster_create.md) - Create a cluster. * [kbcli cluster create-account](kbcli_cluster_create-account.md) - Create account for a cluster @@ -58,26 +60,27 @@ Cluster command. * [kbcli cluster delete-account](kbcli_cluster_delete-account.md) - Delete account for a cluster * [kbcli cluster delete-backup](kbcli_cluster_delete-backup.md) - Delete a backup. * [kbcli cluster delete-ops](kbcli_cluster_delete-ops.md) - Delete an OpsRequest. -* [kbcli cluster delete-restore](kbcli_cluster_delete-restore.md) - Delete a restore job. * [kbcli cluster describe](kbcli_cluster_describe.md) - Show details of a specific cluster. * [kbcli cluster describe-account](kbcli_cluster_describe-account.md) - Describe account roles and related information * [kbcli cluster describe-config](kbcli_cluster_describe-config.md) - Show details of a specific reconfiguring. * [kbcli cluster describe-ops](kbcli_cluster_describe-ops.md) - Show details of a specific OpsRequest. * [kbcli cluster diff-config](kbcli_cluster_diff-config.md) - Show the difference in parameters between the two submitted OpsRequest. +* [kbcli cluster edit-backup-policy](kbcli_cluster_edit-backup-policy.md) - Edit backup policy * [kbcli cluster edit-config](kbcli_cluster_edit-config.md) - Edit the config file of the component. * [kbcli cluster explain-config](kbcli_cluster_explain-config.md) - List the constraint for supported configuration params. -* [kbcli cluster expose](kbcli_cluster_expose.md) - Expose a cluster. +* [kbcli cluster expose](kbcli_cluster_expose.md) - Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'. * [kbcli cluster grant-role](kbcli_cluster_grant-role.md) - Grant role to account * [kbcli cluster hscale](kbcli_cluster_hscale.md) - Horizontally scale the specified components in the cluster. +* [kbcli cluster label](kbcli_cluster_label.md) - Update the labels on cluster * [kbcli cluster list](kbcli_cluster_list.md) - List clusters. * [kbcli cluster list-accounts](kbcli_cluster_list-accounts.md) - List accounts for a cluster +* [kbcli cluster list-backup-policy](kbcli_cluster_list-backup-policy.md) - List backups policies. * [kbcli cluster list-backups](kbcli_cluster_list-backups.md) - List backups. * [kbcli cluster list-components](kbcli_cluster_list-components.md) - List cluster components. * [kbcli cluster list-events](kbcli_cluster_list-events.md) - List cluster events. * [kbcli cluster list-instances](kbcli_cluster_list-instances.md) - List cluster instances. * [kbcli cluster list-logs](kbcli_cluster_list-logs.md) - List supported log files in cluster. * [kbcli cluster list-ops](kbcli_cluster_list-ops.md) - List all opsRequests. -* [kbcli cluster list-restores](kbcli_cluster_list-restores.md) - List all restore jobs. * [kbcli cluster logs](kbcli_cluster_logs.md) - Access cluster log file. * [kbcli cluster restart](kbcli_cluster_restart.md) - Restart the specified components in the cluster. * [kbcli cluster restore](kbcli_cluster_restore.md) - Restore a new cluster from backup. @@ -95,6 +98,7 @@ Cluster command. ClusterDefinition command. * [kbcli clusterdefinition list](kbcli_clusterdefinition_list.md) - List ClusterDefinitions. +* [kbcli clusterdefinition list-components](kbcli_clusterdefinition_list-components.md) - List cluster definition components. ## [clusterversion](kbcli_clusterversion.md) @@ -102,6 +106,8 @@ ClusterDefinition command. ClusterVersion command. * [kbcli clusterversion list](kbcli_clusterversion_list.md) - List ClusterVersions. +* [kbcli clusterversion set-default](kbcli_clusterversion_set-default.md) - Set the clusterversion to the default clusterversion for its clusterdefinition. +* [kbcli clusterversion unset-default](kbcli_clusterversion_unset-default.md) - Unset the clusterversion if it's default. ## [dashboard](kbcli_dashboard.md) @@ -112,10 +118,24 @@ List and open the KubeBlocks dashboards. * [kbcli dashboard open](kbcli_dashboard_open.md) - Open one dashboard. +## [fault](kbcli_fault.md) + +Inject faults to pod. + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. +* [kbcli fault node](kbcli_fault_node.md) - Node chaos. +* [kbcli fault pod](kbcli_fault_pod.md) - Pod chaos. +* [kbcli fault stress](kbcli_fault_stress.md) - Add memory pressure or CPU load to the system. +* [kbcli fault time](kbcli_fault_time.md) - Clock skew failure. + + ## [kubeblocks](kbcli_kubeblocks.md) KubeBlocks operation commands. +* [kbcli kubeblocks config](kbcli_kubeblocks_config.md) - KubeBlocks config. +* [kbcli kubeblocks describe-config](kbcli_kubeblocks_describe-config.md) - describe KubeBlocks config. * [kbcli kubeblocks install](kbcli_kubeblocks_install.md) - Install KubeBlocks. * [kbcli kubeblocks list-versions](kbcli_kubeblocks_list-versions.md) - List KubeBlocks versions. * [kbcli kubeblocks preflight](kbcli_kubeblocks_preflight.md) - Run and retrieve preflight checks for KubeBlocks. @@ -124,6 +144,18 @@ KubeBlocks operation commands. * [kbcli kubeblocks upgrade](kbcli_kubeblocks_upgrade.md) - Upgrade KubeBlocks. +## [migration](kbcli_migration.md) + +Data migration between two data sources. + +* [kbcli migration create](kbcli_migration_create.md) - Create a migration task. +* [kbcli migration describe](kbcli_migration_describe.md) - Show details of a specific migration task. +* [kbcli migration list](kbcli_migration_list.md) - List migration tasks. +* [kbcli migration logs](kbcli_migration_logs.md) - Access migration task log file. +* [kbcli migration templates](kbcli_migration_templates.md) - List migration templates. +* [kbcli migration terminate](kbcli_migration_terminate.md) - Delete migration task. + + ## [options](kbcli_options.md) Print the list of flags inherited by all commands. @@ -132,13 +164,27 @@ Print the list of flags inherited by all commands. ## [playground](kbcli_playground.md) -Bootstrap a playground KubeBlocks in local host or cloud. +Bootstrap or destroy a playground KubeBlocks in local host or cloud. -* [kbcli playground destroy](kbcli_playground_destroy.md) - Destroy the playground kubernetes cluster. -* [kbcli playground guide](kbcli_playground_guide.md) - Display playground cluster user guide. +* [kbcli playground destroy](kbcli_playground_destroy.md) - Destroy the playground KubeBlocks and kubernetes cluster. * [kbcli playground init](kbcli_playground_init.md) - Bootstrap a kubernetes cluster and install KubeBlocks for playground. +## [plugin](kbcli_plugin.md) + +Provides utilities for interacting with plugins. + + Plugins provide extended functionality that is not part of the major command-line distribution. + +* [kbcli plugin describe](kbcli_plugin_describe.md) - Describe a plugin +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes +* [kbcli plugin install](kbcli_plugin_install.md) - Install kbcli or kubectl plugins +* [kbcli plugin list](kbcli_plugin_list.md) - List all visible plugin executables on a user's PATH +* [kbcli plugin search](kbcli_plugin_search.md) - Search kbcli or kubectl plugins +* [kbcli plugin uninstall](kbcli_plugin_uninstall.md) - Uninstall kbcli or kubectl plugins +* [kbcli plugin upgrade](kbcli_plugin_upgrade.md) - Upgrade kbcli or kubectl plugins + + ## [version](kbcli_version.md) Print the version information, include kubernetes, KubeBlocks and kbcli version. diff --git a/docs/user_docs/cli/kbcli.md b/docs/user_docs/cli/kbcli.md index 8935f3900..7d220e563 100644 --- a/docs/user_docs/cli/kbcli.md +++ b/docs/user_docs/cli/kbcli.md @@ -56,16 +56,19 @@ kbcli [flags] * [kbcli addon](kbcli_addon.md) - Addon command. * [kbcli alert](kbcli_alert.md) - Manage alert receiver, include add, list and delete receiver. -* [kbcli backup-config](kbcli_backup-config.md) - KubeBlocks backup config. * [kbcli bench](kbcli_bench.md) - Run a benchmark. +* [kbcli builder](kbcli_builder.md) - builder command. * [kbcli class](kbcli_class.md) - Manage classes * [kbcli cluster](kbcli_cluster.md) - Cluster command. * [kbcli clusterdefinition](kbcli_clusterdefinition.md) - ClusterDefinition command. * [kbcli clusterversion](kbcli_clusterversion.md) - ClusterVersion command. * [kbcli dashboard](kbcli_dashboard.md) - List and open the KubeBlocks dashboards. +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. * [kbcli kubeblocks](kbcli_kubeblocks.md) - KubeBlocks operation commands. +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. * [kbcli options](kbcli_options.md) - Print the list of flags inherited by all commands. -* [kbcli playground](kbcli_playground.md) - Bootstrap a playground KubeBlocks in local host or cloud. +* [kbcli playground](kbcli_playground.md) - Bootstrap or destroy a playground KubeBlocks in local host or cloud. +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. * [kbcli version](kbcli_version.md) - Print the version information, include kubernetes, KubeBlocks and kbcli version. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_addon_disable.md b/docs/user_docs/cli/kbcli_addon_disable.md index b25637de4..0380dbdec 100644 --- a/docs/user_docs/cli/kbcli_addon_disable.md +++ b/docs/user_docs/cli/kbcli_addon_disable.md @@ -13,6 +13,7 @@ kbcli addon disable ADDON_NAME [flags] ``` --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) --dry-run string[="unchanged"] Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource. (default "none") + --edit Edit the API resource -h, --help help for disable -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file). --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. diff --git a/docs/user_docs/cli/kbcli_addon_enable.md b/docs/user_docs/cli/kbcli_addon_enable.md index 01559bc56..50ffebf60 100644 --- a/docs/user_docs/cli/kbcli_addon_enable.md +++ b/docs/user_docs/cli/kbcli_addon_enable.md @@ -19,11 +19,12 @@ kbcli addon enable ADDON_NAME [flags] # Enabled "prometheus" addon and its extra alertmanager component with custom resources settings kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2 \ - --memory alertmanager:16Mi/256Mi --storage: alertmanager:1Gi --replicas alertmanager:2 + --memory alertmanager:16Mi/256Mi --storage alertmanager:1Gi --replicas alertmanager:2 # Enabled "prometheus" addon with tolerations - kbcli addon enable prometheus --tolerations '[[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]]' \ - --tolerations 'alertmanager:[[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]]' + kbcli addon enable prometheus \ + --tolerations '[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' \ + --tolerations 'alertmanager:[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' # Enabled "prometheus" addon with helm like custom settings kbcli addon enable prometheus --set prometheus.alertmanager.image.tag=v0.24.0 @@ -38,6 +39,7 @@ kbcli addon enable ADDON_NAME [flags] --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) --cpu stringArray Sets addon CPU resource values (--cpu [extraName:]/) (can specify multiple if has extra items)) --dry-run string[="unchanged"] Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource. (default "none") + --edit Edit the API resource --force ignoring the installable restrictions and forcefully enabling. -h, --help help for enable --memory stringArray Sets addon memory resource values (--memory [extraName:]/) (can specify multiple if has extra items)) @@ -46,7 +48,9 @@ kbcli addon enable ADDON_NAME [flags] --set stringArray set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2), it's only being processed if addon's type is helm. --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. --storage stringArray Sets addon storage size (--storage [extraName:]) (can specify multiple if has extra items)). - Additional notes for Helm type Addon, that resizing storage will fail if modified value is a storage request size + Additional notes: + 1. Specify '0' value will remove storage values settings and explicitly disable 'persistentVolumeEnabled' attribute. + 2. For Helm type Addon, that resizing storage will fail if modified value is a storage request size that belongs to StatefulSet's volume claim template, to resolve 'Failed' Addon status possible action is disable and re-enable the addon (More info on how-to resize a PVC: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources). diff --git a/docs/user_docs/cli/kbcli_alert_add-receiver.md b/docs/user_docs/cli/kbcli_alert_add-receiver.md index d5a1cac5e..ab7bc166c 100644 --- a/docs/user_docs/cli/kbcli_alert_add-receiver.md +++ b/docs/user_docs/cli/kbcli_alert_add-receiver.md @@ -18,13 +18,13 @@ kbcli alert add-receiver [flags] kbcli alert add-receiver --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=XXX' # add email receiver - kbcli alter add-receiver --email='a@foo.com,b@foo.com' + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' # add email receiver, and only receive alert from cluster mycluster - kbcli alter add-receiver --email='a@foo.com,b@foo.com' --cluster=mycluster + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' --cluster=mycluster # add email receiver, and only receive alert from cluster mycluster and alert severity is warning - kbcli alter add-receiver --email='a@foo.com,b@foo.com' --cluster=mycluster --severity=warning + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' --cluster=mycluster --severity=warning # add slack receiver kbcli alert add-receiver --slack api_url=https://hooks.slackConfig.com/services/foo,channel=monitor,username=kubeblocks-alert-bot @@ -33,10 +33,10 @@ kbcli alert add-receiver [flags] ### Options ``` - --cluster stringArray Cluster name, such as mycluster, more than one cluster can be specified, such as mycluster,mycluster2 - --email stringArray Add email address, such as bar@foo.com, more than one emailConfig can be specified separated by comma + --cluster stringArray Cluster name, such as mycluster, more than one cluster can be specified, such as mycluster1,mycluster2 + --email stringArray Add email address, such as user@kubeblocks.io, more than one emailConfig can be specified separated by comma -h, --help help for add-receiver - --severity stringArray Alert severity, critical, warning or info, more than one severity can be specified, such as critical,warning + --severity stringArray Alert severity level, critical, warning or info, more than one severity level can be specified, such as critical,warning --slack stringArray Add slack receiver, such as api_url=https://hooks.slackConfig.com/services/foo,channel=monitor,username=kubeblocks-alert-bot --webhook stringArray Add webhook receiver, such as url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=xxxxx ``` diff --git a/docs/user_docs/cli/kbcli_bench.md b/docs/user_docs/cli/kbcli_bench.md index 955faffe2..57c50b960 100644 --- a/docs/user_docs/cli/kbcli_bench.md +++ b/docs/user_docs/cli/kbcli_bench.md @@ -7,21 +7,7 @@ Run a benchmark. ### Options ``` - --count int Total execution count, 0 means infinite - -D, --db string Database name (default "kb_test") - -d, --driver string Database driver: mysql (default "mysql") - --dropdata Cleanup data before prepare - -h, --help help for bench - -H, --host string Database host (default "127.0.0.1") - --ignore-error Ignore error when running workload - --interval duration Output interval time (default 5s) - --max-procs int runtime.GOMAXPROCS - -p, --password string Database password (default "sakila") - -P, --port int Database port (default 3306) - --silence Don't print error when running workload - -T, --threads int Thread concurrency (default 1) - --time duration Total execution time (default 2562047h47m16.854775807s) - -U, --user string Database user (default "root") + -h, --help help for bench ``` ### Options inherited from parent commands @@ -45,12 +31,13 @@ Run a benchmark. -s, --server string The address and port of the Kubernetes API server --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use ``` ### SEE ALSO -* [kbcli bench tpcc](kbcli_bench_tpcc.md) - Run a TPCC benchmark. +* [kbcli bench sysbench](kbcli_bench_sysbench.md) - run a SysBench benchmark #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_bench_sysbench.md b/docs/user_docs/cli/kbcli_bench_sysbench.md new file mode 100644 index 000000000..af392787d --- /dev/null +++ b/docs/user_docs/cli/kbcli_bench_sysbench.md @@ -0,0 +1,54 @@ +--- +title: kbcli bench sysbench +--- + +run a SysBench benchmark + +### Options + +``` + --database string database name + --driver string database driver + -h, --help help for sysbench + --host string the host of database + --password string the password of database + --port int the port of database + --size int the data size of per table (default 20000) + --tables int the number of tables (default 10) + --times int the number of test times (default 100) + --type string sysbench type (default "oltp_read_write_pct") + --user string the user of database +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server +``` + +### SEE ALSO + +* [kbcli bench](kbcli_bench.md) - Run a benchmark. +* [kbcli bench sysbench cleanup](kbcli_bench_sysbench_cleanup.md) - Cleanup the data of SysBench for cluster +* [kbcli bench sysbench prepare](kbcli_bench_sysbench_prepare.md) - Prepare the data of SysBench for a cluster +* [kbcli bench sysbench run](kbcli_bench_sysbench_run.md) - Run SysBench on cluster + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_bench_sysbench_cleanup.md b/docs/user_docs/cli/kbcli_bench_sysbench_cleanup.md new file mode 100644 index 000000000..8cd82b058 --- /dev/null +++ b/docs/user_docs/cli/kbcli_bench_sysbench_cleanup.md @@ -0,0 +1,55 @@ +--- +title: kbcli bench sysbench cleanup +--- + +Cleanup the data of SysBench for cluster + +``` +kbcli bench sysbench cleanup [NAME] [flags] +``` + +### Options + +``` + -h, --help help for cleanup +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --database string database name + --disable-compression If true, opt-out of response compression for all requests to the server + --driver string database driver + --host string the host of database + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --password string the password of database + --port int the port of database + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --size int the data size of per table (default 20000) + --tables int the number of tables (default 10) + --times int the number of test times (default 100) + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --type string sysbench type (default "oltp_read_write_pct") + --user string the user of database +``` + +### SEE ALSO + +* [kbcli bench sysbench](kbcli_bench_sysbench.md) - run a SysBench benchmark + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_bench_sysbench_prepare.md b/docs/user_docs/cli/kbcli_bench_sysbench_prepare.md new file mode 100644 index 000000000..782227e47 --- /dev/null +++ b/docs/user_docs/cli/kbcli_bench_sysbench_prepare.md @@ -0,0 +1,55 @@ +--- +title: kbcli bench sysbench prepare +--- + +Prepare the data of SysBench for a cluster + +``` +kbcli bench sysbench prepare [NAME] [flags] +``` + +### Options + +``` + -h, --help help for prepare +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --database string database name + --disable-compression If true, opt-out of response compression for all requests to the server + --driver string database driver + --host string the host of database + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --password string the password of database + --port int the port of database + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --size int the data size of per table (default 20000) + --tables int the number of tables (default 10) + --times int the number of test times (default 100) + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --type string sysbench type (default "oltp_read_write_pct") + --user string the user of database +``` + +### SEE ALSO + +* [kbcli bench sysbench](kbcli_bench_sysbench.md) - run a SysBench benchmark + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_bench_sysbench_run.md b/docs/user_docs/cli/kbcli_bench_sysbench_run.md new file mode 100644 index 000000000..7f33e755d --- /dev/null +++ b/docs/user_docs/cli/kbcli_bench_sysbench_run.md @@ -0,0 +1,55 @@ +--- +title: kbcli bench sysbench run +--- + +Run SysBench on cluster + +``` +kbcli bench sysbench run [NAME] [flags] +``` + +### Options + +``` + -h, --help help for run +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --database string database name + --disable-compression If true, opt-out of response compression for all requests to the server + --driver string database driver + --host string the host of database + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --password string the password of database + --port int the port of database + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --size int the data size of per table (default 20000) + --tables int the number of tables (default 10) + --times int the number of test times (default 100) + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --type string sysbench type (default "oltp_read_write_pct") + --user string the user of database +``` + +### SEE ALSO + +* [kbcli bench sysbench](kbcli_bench_sysbench.md) - run a SysBench benchmark + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_bench_tpcc_prepare.md b/docs/user_docs/cli/kbcli_bench_tpcc_prepare.md index ebf0e9d2f..0aa5b756f 100644 --- a/docs/user_docs/cli/kbcli_bench_tpcc_prepare.md +++ b/docs/user_docs/cli/kbcli_bench_tpcc_prepare.md @@ -14,10 +14,10 @@ kbcli bench tpcc prepare [flags] -h, --help help for prepare --no-check TPCC prepare check, default false --output-dir string Output directory for generating file if specified - --output-type string Output file type. If empty, then load data to db. Current only support csv + --output-type string Output file type. If not set, database generates the data itself. Current only support csv --retry-count int Retry count when errors occur (default 50) --retry-interval duration The interval for each retry (default 5s) - --tables string Specified tables for generating file, separated by ','. Valid only if output is set. If this flag is not set, generate all tables by default + --tables string Specified tables for generating file, separated by ','. Valid only if output is set. If not set, generate all tables by default ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_bench_tpcc_run.md b/docs/user_docs/cli/kbcli_bench_tpcc_run.md index 60055c434..19f2302f1 100644 --- a/docs/user_docs/cli/kbcli_bench_tpcc_run.md +++ b/docs/user_docs/cli/kbcli_bench_tpcc_run.md @@ -12,7 +12,7 @@ kbcli bench tpcc run [flags] ``` -h, --help help for run - --max-measure-latency duration max measure latency in millisecond (default 16s) + --max-measure-latency duration max measure latency in milliseconds (default 16s) --wait including keying & thinking time described on TPC-C Standard Specification --weight ints Weight for NewOrder, Payment, OrderStatus, Delivery, StockLevel (default [45,43,4,4,4]) ``` diff --git a/docs/user_docs/cli/kbcli_builder.md b/docs/user_docs/cli/kbcli_builder.md new file mode 100644 index 000000000..fe5ea937b --- /dev/null +++ b/docs/user_docs/cli/kbcli_builder.md @@ -0,0 +1,43 @@ +--- +title: kbcli builder +--- + +builder command. + +### Options + +``` + -h, --help help for builder +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + + +* [kbcli builder template](kbcli_builder_template.md) - tpl - a developer tool integrated with KubeBlocks that can help developers quickly generate rendered configurations or scripts based on Helm templates, and discover errors in the template before creating the database cluster. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_builder_template.md b/docs/user_docs/cli/kbcli_builder_template.md new file mode 100644 index 000000000..ce129743f --- /dev/null +++ b/docs/user_docs/cli/kbcli_builder_template.md @@ -0,0 +1,68 @@ +--- +title: kbcli builder template +--- + +tpl - a developer tool integrated with KubeBlocks that can help developers quickly generate rendered configurations or scripts based on Helm templates, and discover errors in the template before creating the database cluster. + +``` +kbcli builder template [flags] +``` + +### Examples + +``` + # builder template: Provides a mechanism to rendered template for ComponentConfigSpec and ComponentScriptSpec in the ClusterComponentDefinition. + # builder template --helm deploy/redis --memory=64Gi --cpu=16 --replicas=3 --component-name=redis --config-spec=redis-replication-config + + # build all configspec + kbcli builder template --helm deploy/redis -a +``` + +### Options + +``` + -a, --all template all config specs + --clean specify whether to clear the output dir + --cluster string the cluster yaml file + --cluster-definition string the cluster definition yaml file + --component-name string specify the component name of the clusterdefinition + --config-spec string specify the config spec to be rendered + --cpu string specify the cpu of the component + --helm string specify the helm template dir + --helm-output string specify the helm template output dir + -h, --help help for template + --memory string specify the memory of the component + -o, --output-dir string specify the output directory + -r, --replicas int32 specify the replicas of the component (default 1) + --volume-name string specify the data volume name of the component +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli builder](kbcli_builder.md) - builder command. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_class_create.md b/docs/user_docs/cli/kbcli_class_create.md index 46dcf7849..311f367e3 100644 --- a/docs/user_docs/cli/kbcli_class_create.md +++ b/docs/user_docs/cli/kbcli_class_create.md @@ -11,23 +11,22 @@ kbcli class create [NAME] [flags] ### Examples ``` - # Create a class following class family kubeblocks-general-classes for component mysql in cluster definition apecloud-mysql, which have 1 cpu core, 2Gi memory and storage is 10Gi - kbcli class create custom-1c2g --cluster-definition apecloud-mysql --type mysql --class-family kubeblocks-general-classes --cpu 1 --memory 2Gi --storage name=data,size=10Gi + # Create a class with constraint kb-resource-constraint-general for component mysql in cluster definition apecloud-mysql, which has 1 CPU core and 1Gi memory + kbcli class create custom-1c1g --cluster-definition apecloud-mysql --type mysql --constraint kb-resource-constraint-general --cpu 1 --memory 1Gi - # Create classes for component mysql in cluster definition apecloud-mysql, where classes is defined in file + # Create classes for component mysql in cluster definition apecloud-mysql, with classes defined in file kbcli class create --cluster-definition apecloud-mysql --type mysql --file ./classes.yaml ``` ### Options ``` - --class-family string Specify class family - --cluster-definition string Specify cluster definition, run "kbcli clusterdefinition list" to show all available cluster definition - --cpu string Specify component cpu cores - --file string Specify file path which contains YAML definition of class + --cluster-definition string Specify cluster definition, run "kbcli clusterdefinition list" to show all available cluster definitions + --constraint string Specify resource constraint + --cpu string Specify component CPU cores + --file string Specify file path of class definition YAML -h, --help help for create --memory string Specify component memory size - --storage stringArray Specify component storage disks --type string Specify component type ``` diff --git a/docs/user_docs/cli/kbcli_class_list.md b/docs/user_docs/cli/kbcli_class_list.md index 798d52e85..c44de63f0 100644 --- a/docs/user_docs/cli/kbcli_class_list.md +++ b/docs/user_docs/cli/kbcli_class_list.md @@ -18,7 +18,7 @@ kbcli class list [flags] ### Options ``` - --cluster-definition string Specify cluster definition, run "kbcli cluster-definition list" to show all available cluster definition + --cluster-definition string Specify cluster definition, run "kbcli clusterdefinition list" to show all available cluster definition -h, --help help for list ``` diff --git a/docs/user_docs/cli/kbcli_cluster.md b/docs/user_docs/cli/kbcli_cluster.md index 359bc280f..6e23ddf3e 100644 --- a/docs/user_docs/cli/kbcli_cluster.md +++ b/docs/user_docs/cli/kbcli_cluster.md @@ -37,8 +37,9 @@ Cluster command. ### SEE ALSO -* [kbcli cluster backup](kbcli_cluster_backup.md) - Create a backup. -* [kbcli cluster configure](kbcli_cluster_configure.md) - Reconfigure parameters with the specified components in the cluster. +* [kbcli cluster backup](kbcli_cluster_backup.md) - Create a backup for the cluster. +* [kbcli cluster cancel-ops](kbcli_cluster_cancel-ops.md) - cancel the pending/creating/running OpsRequest which type is vscale or hscale. +* [kbcli cluster configure](kbcli_cluster_configure.md) - Configure parameters with the specified components in the cluster. * [kbcli cluster connect](kbcli_cluster_connect.md) - Connect to a cluster or instance. * [kbcli cluster create](kbcli_cluster_create.md) - Create a cluster. * [kbcli cluster create-account](kbcli_cluster_create-account.md) - Create account for a cluster @@ -46,26 +47,27 @@ Cluster command. * [kbcli cluster delete-account](kbcli_cluster_delete-account.md) - Delete account for a cluster * [kbcli cluster delete-backup](kbcli_cluster_delete-backup.md) - Delete a backup. * [kbcli cluster delete-ops](kbcli_cluster_delete-ops.md) - Delete an OpsRequest. -* [kbcli cluster delete-restore](kbcli_cluster_delete-restore.md) - Delete a restore job. * [kbcli cluster describe](kbcli_cluster_describe.md) - Show details of a specific cluster. * [kbcli cluster describe-account](kbcli_cluster_describe-account.md) - Describe account roles and related information * [kbcli cluster describe-config](kbcli_cluster_describe-config.md) - Show details of a specific reconfiguring. * [kbcli cluster describe-ops](kbcli_cluster_describe-ops.md) - Show details of a specific OpsRequest. * [kbcli cluster diff-config](kbcli_cluster_diff-config.md) - Show the difference in parameters between the two submitted OpsRequest. +* [kbcli cluster edit-backup-policy](kbcli_cluster_edit-backup-policy.md) - Edit backup policy * [kbcli cluster edit-config](kbcli_cluster_edit-config.md) - Edit the config file of the component. * [kbcli cluster explain-config](kbcli_cluster_explain-config.md) - List the constraint for supported configuration params. -* [kbcli cluster expose](kbcli_cluster_expose.md) - Expose a cluster. +* [kbcli cluster expose](kbcli_cluster_expose.md) - Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'. * [kbcli cluster grant-role](kbcli_cluster_grant-role.md) - Grant role to account * [kbcli cluster hscale](kbcli_cluster_hscale.md) - Horizontally scale the specified components in the cluster. +* [kbcli cluster label](kbcli_cluster_label.md) - Update the labels on cluster * [kbcli cluster list](kbcli_cluster_list.md) - List clusters. * [kbcli cluster list-accounts](kbcli_cluster_list-accounts.md) - List accounts for a cluster +* [kbcli cluster list-backup-policy](kbcli_cluster_list-backup-policy.md) - List backups policies. * [kbcli cluster list-backups](kbcli_cluster_list-backups.md) - List backups. * [kbcli cluster list-components](kbcli_cluster_list-components.md) - List cluster components. * [kbcli cluster list-events](kbcli_cluster_list-events.md) - List cluster events. * [kbcli cluster list-instances](kbcli_cluster_list-instances.md) - List cluster instances. * [kbcli cluster list-logs](kbcli_cluster_list-logs.md) - List supported log files in cluster. * [kbcli cluster list-ops](kbcli_cluster_list-ops.md) - List all opsRequests. -* [kbcli cluster list-restores](kbcli_cluster_list-restores.md) - List all restore jobs. * [kbcli cluster logs](kbcli_cluster_logs.md) - Access cluster log file. * [kbcli cluster restart](kbcli_cluster_restart.md) - Restart the specified components in the cluster. * [kbcli cluster restore](kbcli_cluster_restore.md) - Restore a new cluster from backup. diff --git a/docs/user_docs/cli/kbcli_cluster_backup.md b/docs/user_docs/cli/kbcli_cluster_backup.md index b6fece97e..483f87729 100644 --- a/docs/user_docs/cli/kbcli_cluster_backup.md +++ b/docs/user_docs/cli/kbcli_cluster_backup.md @@ -2,27 +2,37 @@ title: kbcli cluster backup --- -Create a backup. +Create a backup for the cluster. ``` -kbcli cluster backup [flags] +kbcli cluster backup NAME [flags] ``` ### Examples ``` - # create a backup - kbcli cluster backup cluster-name + # create a backup, the default type is snapshot. + kbcli cluster backup mycluster + + # create a snapshot backup + # create a snapshot of the cluster's persistent volume for backup + kbcli cluster backup mycluster --type snapshot + + # create a datafile backup + # backup all files under the data directory and save them to the specified storage, only full backup is supported now. + kbcli cluster backup mycluster --type datafile + + # create a backup with specified backup policy + kbcli cluster backup mycluster --policy ``` ### Options ``` - --backup-name string Backup name - --backup-type string Backup type (default "snapshot") - -h, --help help for backup - --role string backup on cluster role - --ttl string Time to live (default "168h0m0s") + -h, --help help for backup + --name string Backup name + --policy string Backup policy name, this flag will be ignored when backup-type is snapshot + --type string Backup type (default "snapshot") ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_delete-user.md b/docs/user_docs/cli/kbcli_cluster_cancel-ops.md similarity index 79% rename from docs/user_docs/cli/kbcli_cluster_delete-user.md rename to docs/user_docs/cli/kbcli_cluster_cancel-ops.md index b7ca04a75..cab2f8c6a 100644 --- a/docs/user_docs/cli/kbcli_cluster_delete-user.md +++ b/docs/user_docs/cli/kbcli_cluster_cancel-ops.md @@ -1,28 +1,25 @@ --- -title: kbcli cluster delete-user +title: kbcli cluster cancel-ops --- -Delete user for a cluster +cancel the pending/creating/running OpsRequest which type is vscale or hscale. ``` -kbcli cluster delete-user [flags] +kbcli cluster cancel-ops NAME [flags] ``` ### Examples ``` - # delete user by name - kbcli cluster delete-user NAME --component-name COMPNAME --username NAME + # cancel the opsRequest which is not completed. + kbcli cluster cancel-ops ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for delete-user - -i, --instance string Specify the name of instance to be connected. - -u, --username string Required. Specify the name of user - --verbose Print verbose information. + --auto-approve Skip interactive approval before cancel the opsRequest + -h, --help help for cancel-ops ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_configure.md b/docs/user_docs/cli/kbcli_cluster_configure.md index 009e132d1..96d641721 100644 --- a/docs/user_docs/cli/kbcli_cluster_configure.md +++ b/docs/user_docs/cli/kbcli_cluster_configure.md @@ -2,32 +2,36 @@ title: kbcli cluster configure --- -Reconfigure parameters with the specified components in the cluster. +Configure parameters with the specified components in the cluster. ``` -kbcli cluster configure [flags] +kbcli cluster configure NAME --set key=value[,key=value] [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file] [flags] ``` ### Examples ``` # update component params - kbcli cluster configure --component= --config-spec= --config-file= --set max_connections=1000,general_log=OFF + kbcli cluster configure mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf --set max_connections=1000,general_log=OFF + # if only one component, and one config spec, and one config file, simplify the searching process of configure. e.g: # update mysql max_connections, cluster name is mycluster - kbcli cluster configure mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf --set max_connections=2000 + kbcli cluster configure mycluster --set max_connections=2000 ``` ### Options ``` - --component string Specify the name of Component to be updated. If the cluster has only one component, unset the parameter. - --config-file string Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'. - --config-spec string Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'. - -h, --help help for configure - --name string OpsRequest name. if not specified, it will be randomly generated - --set strings Specify updated parameter list. For details about the parameters, refer to kbcli sub command: 'kbcli cluster describe-config'. - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before reconfiguring the cluster + --component string Specify the name of Component to be updated. If the cluster has only one component, unset the parameter. + --config-file string Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). For available templates and configs, refer to: 'kbcli cluster describe-config'. + --config-spec string Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). For available templates and configs, refer to: 'kbcli cluster describe-config'. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for configure + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --set strings Specify parameters list to be updated. For more details, refer to 'kbcli cluster describe-config'. + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_connect.md b/docs/user_docs/cli/kbcli_cluster_connect.md index 2565e2ab5..72521a91d 100644 --- a/docs/user_docs/cli/kbcli_cluster_connect.md +++ b/docs/user_docs/cli/kbcli_cluster_connect.md @@ -11,12 +11,18 @@ kbcli cluster connect (NAME | -i INSTANCE-NAME) [flags] ### Examples ``` - # connect to a specified cluster, default connect to the leader or primary instance + # connect to a specified cluster, default connect to the leader/primary instance kbcli cluster connect mycluster + # connect to cluster as user + kbcli cluster connect mycluster --as-user myuser + # connect to a specified instance kbcli cluster connect -i mycluster-instance-0 + # connect to a specified component + kbcli cluster connect mycluster --component mycomponent + # show cli connection example kbcli cluster connect mycluster --show-example --client=cli @@ -30,10 +36,12 @@ kbcli cluster connect (NAME | -i INSTANCE-NAME) [flags] ### Options ``` - --client string Which client connection example should be output, only valid if --show-example is true. - -h, --help help for connect - -i, --instance string The instance name to connect. - --show-example Show how to connect to cluster or instance from different client. + --as-user string Connect to cluster as user + --client string Which client connection example should be output, only valid if --show-example is true. + --component string The component to connect. If not specified, pick up the first one. + -h, --help help for connect + -i, --instance string The instance name to connect. + --show-example Show how to connect to cluster/instance from different clients. ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_create-account.md b/docs/user_docs/cli/kbcli_cluster_create-account.md index 59e4e935b..da94e0de4 100644 --- a/docs/user_docs/cli/kbcli_cluster_create-account.md +++ b/docs/user_docs/cli/kbcli_cluster_create-account.md @@ -11,22 +11,24 @@ kbcli cluster create-account [flags] ### Examples ``` - # create account - kbcli cluster create-account NAME --component-name COMPNAME --username NAME --password PASSWD + # create account with password + kbcli cluster create-account CLUSTERNAME --component COMPNAME --name USERNAME --password PASSWD # create account without password - kbcli cluster create-account NAME --component-name COMPNAME --username NAME - # create account with expired interval - kbcli cluster create-account NAME --component-name COMPNAME --username NAME --password PASSWD --expiredAt 2046-01-02T15:04:05Z + kbcli cluster create-account CLUSTERNAME --component COMPNAME --name USERNAME + # create account with default component + kbcli cluster create-account CLUSTERNAME --name USERNAME + # create account for instance + kbcli cluster create-account --instance INSTANCE --name USERNAME ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for create-account - -i, --instance string Specify the name of instance to be connected. - -p, --password string Optional. Specify the password of user. The default value is empty, which means a random password will be generated. - -u, --username string Required. Specify the name of user, which must be unique. + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for create-account + -i, --instance string Specify the name of instance to be connected. + --name string Required. Specify the name of user, which must be unique. + -p, --password string Optional. Specify the password of user. The default value is empty, which means a random password will be generated. ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_create.md b/docs/user_docs/cli/kbcli_cluster_create.md index 424296190..c42808c8a 100644 --- a/docs/user_docs/cli/kbcli_cluster_create.md +++ b/docs/user_docs/cli/kbcli_cluster_create.md @@ -5,7 +5,7 @@ title: kbcli cluster create Create a cluster. ``` -kbcli cluster create [CLUSTER_NAME] [flags] +kbcli cluster create [NAME] [flags] ``` ### Examples @@ -14,67 +14,93 @@ kbcli cluster create [CLUSTER_NAME] [flags] # Create a cluster with cluster definition apecloud-mysql and cluster version ac-mysql-8.0.30 kbcli cluster create mycluster --cluster-definition apecloud-mysql --cluster-version ac-mysql-8.0.30 - # --cluster-definition is required, if --cluster-version is not specified, will use the most recently created version + # --cluster-definition is required, if --cluster-version is not specified, pick the most recently created version kbcli cluster create mycluster --cluster-definition apecloud-mysql - # Create a cluster and set termination policy DoNotTerminate that will prevent the cluster from being deleted + # Output resource information in YAML format, without creation of resources. + kbcli cluster create mycluster --cluster-definition apecloud-mysql --dry-run -o yaml + + # Output resource information in YAML format, the information will be sent to the server + # but the resources will not be actually created. + kbcli cluster create mycluster --cluster-definition apecloud-mysql --dry-run=server -o yaml + + # Create a cluster and set termination policy DoNotTerminate that prevents the cluster from being deleted kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy DoNotTerminate - # In scenarios where you want to delete resources such as statements, deployments, services, pdb, but keep PVCs + # Delete resources such as statefulsets, deployments, services, pdb, but keep PVCs # when deleting the cluster, use termination policy Halt kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy Halt - # In scenarios where you want to delete resource such as statements, deployments, services, pdb, and including + # Delete resource such as statefulsets, deployments, services, pdb, and including # PVCs when deleting the cluster, use termination policy Delete kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy Delete - # In scenarios where you want to delete all resources including all snapshots and snapshot data when deleting + # Delete all resources including all snapshots and snapshot data when deleting # the cluster, use termination policy WipeOut kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy WipeOut # Create a cluster and set cpu to 1 core, memory to 1Gi, storage size to 20Gi and replicas to 3 kbcli cluster create mycluster --cluster-definition apecloud-mysql --set cpu=1,memory=1Gi,storage=20Gi,replicas=3 - # Create a cluster and set class to general-1c4g - kbcli cluster create myclsuter --cluster-definition apecloud-mysql --set class=general-1c4g + # Create a cluster and set storageClass to csi-hostpath-sc, if storageClass is not specified, + # the default storage class will be used + kbcli cluster create mycluster --cluster-definition apecloud-mysql --set storageClass=csi-hostpath-sc + + # Create a cluster and set the class to general-1c1g + # run "kbcli class list --cluster-definition=cluster-definition-name" to get the class list + kbcli cluster create mycluster --cluster-definition apecloud-mysql --set class=general-1c1g + + # Create a cluster with replicationSet workloadType and set switchPolicy to Noop + kbcli cluster create mycluster --cluster-definition postgresql --set switchPolicy=Noop + + # Create a cluster with more than one component, use "--set type=component-name" to specify the component, + # if not specified, the main component will be used, run "kbcli cd list-components CLUSTER-DEFINITION-NAME" + # to show the components in the cluster definition + kbcli cluster create mycluster --cluster-definition redis --set type=redis,cpu=1 --set type=redis-sentinel,cpu=200m # Create a cluster and use a URL to set cluster resource - kbcli cluster create mycluster --cluster-definition apecloud-mysql --set-file https://kubeblocks.io/yamls/my.yaml + kbcli cluster create mycluster --cluster-definition apecloud-mysql \ + --set-file https://kubeblocks.io/yamls/apecloud-mysql.yaml # Create a cluster and load cluster resource set from stdin cat << EOF | kbcli cluster create mycluster --cluster-definition apecloud-mysql --set-file - - name: my-test ... - # Create a cluster forced to scatter by node - kbcli cluster create --cluster-definition apecloud-mysql --topology-keys kubernetes.io/hostname --pod-anti-affinity Required + # Create a cluster scattered by nodes + kbcli cluster create --cluster-definition apecloud-mysql --topology-keys kubernetes.io/hostname \ + --pod-anti-affinity Required # Create a cluster in specific labels nodes - kbcli cluster create --cluster-definition apecloud-mysql --node-labels '"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"' + kbcli cluster create --cluster-definition apecloud-mysql \ + --node-labels '"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"' # Create a Cluster with two tolerations - kbcli cluster create --cluster-definition apecloud-mysql --tolerations '"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + kbcli cluster create --cluster-definition apecloud-mysql --tolerations \ '"engineType=mongo:NoSchedule","diskType=ssd:NoSchedule"' # Create a cluster, with each pod runs on their own dedicated node - kbcli cluster create --tenancy=DedicatedNode + kbcli cluster create --cluster-definition apecloud-mysql --tenancy=DedicatedNode ``` ### Options ``` - --backup string Set a source backup to restore data - --cluster-definition string Specify cluster definition, run "kbcli cd list" to show all available cluster definitions - --cluster-version string Specify cluster version, run "kbcli cv list" to show all available cluster versions, use the latest version if not specified - --enable-all-logs Enable advanced application all log extraction, and true will ignore enabledLogs of component level (default true) - -h, --help help for create - --monitor Set monitor enabled and inject metrics exporter (default true) - --node-labels stringToString Node label selector (default []) - --pod-anti-affinity string Pod anti-affinity type, one of: (Preferred, Required) (default "Preferred") - --set stringArray Set the cluster resource including cpu, memory, replicas and storage, each set corresponds to a component.(e.g. --set cpu=1,memory=1Gi,replicas=3,storage=20Gi) - -f, --set-file string Use yaml file, URL, or stdin to set the cluster resource - --tenancy string Tenancy options, one of: (SharedNode, DedicatedNode) (default "SharedNode") - --termination-policy string Termination policy, one of: (DoNotTerminate, Halt, Delete, WipeOut) (default "Delete") - --tolerations strings Tolerations for cluster, such as '"key=engineType,value=mongo,operator=Equal,effect=NoSchedule"' - --topology-keys stringArray Topology keys for affinity + --backup string Set a source backup to restore data + --cluster-definition string Specify cluster definition, run "kbcli cd list" to show all available cluster definitions + --cluster-version string Specify cluster version, run "kbcli cv list" to show all available cluster versions, use the latest version if not specified + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --edit Edit the API resource before creating + --enable-all-logs Enable advanced application all log extraction, set to true will ignore enabledLogs of component level, default is false + -h, --help help for create + --monitor Set monitor enabled and inject metrics exporter (default true) + --node-labels stringToString Node label selector (default []) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --pod-anti-affinity string Pod anti-affinity type, one of: (Preferred, Required) (default "Preferred") + --set stringArray Set the cluster resource including cpu, memory, replicas and storage, or just specify the class, each set corresponds to a component.(e.g. --set cpu=1,memory=1Gi,replicas=3,storage=20Gi or --set class=general-1c1g) + -f, --set-file string Use yaml file, URL, or stdin to set the cluster resource + --tenancy string Tenancy options, one of: (SharedNode, DedicatedNode) (default "SharedNode") + --termination-policy string Termination policy, one of: (DoNotTerminate, Halt, Delete, WipeOut) (default "Delete") + --tolerations strings Tolerations for cluster, such as "key=value:effect, key:effect", for example '"engineType=mongo:NoSchedule", "diskType:NoSchedule"' + --topology-keys stringArray Topology keys for affinity ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_delete-account.md b/docs/user_docs/cli/kbcli_cluster_delete-account.md index 171243fb0..869dc038e 100644 --- a/docs/user_docs/cli/kbcli_cluster_delete-account.md +++ b/docs/user_docs/cli/kbcli_cluster_delete-account.md @@ -12,16 +12,21 @@ kbcli cluster delete-account [flags] ``` # delete account by name - kbcli cluster delete-account NAME --component-name COMPNAME --username NAME + kbcli cluster delete-account CLUSTERNAME --component COMPNAME --name USERNAME + # delete account with default component + kbcli cluster delete-account CLUSTERNAME --name USERNAME + # delete account for instance + kbcli cluster delete-account --instance INSTANCE --name USERNAME ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for delete-account - -i, --instance string Specify the name of instance to be connected. - -u, --username string Required. Specify the name of user + --auto-approve Skip interactive approval before deleting account + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for delete-account + -i, --instance string Specify the name of instance to be connected. + --name string Required user name, please specify it ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_describe-account.md b/docs/user_docs/cli/kbcli_cluster_describe-account.md index 527ba7ecd..1d8c74174 100644 --- a/docs/user_docs/cli/kbcli_cluster_describe-account.md +++ b/docs/user_docs/cli/kbcli_cluster_describe-account.md @@ -12,16 +12,20 @@ kbcli cluster describe-account [flags] ``` # describe account and show role information - kbcli cluster describe-account NAME --component-name COMPNAME--username NAME + kbcli cluster describe-account CLUSTERNAME --component COMPNAME --name USERNAME + # describe account with default component + kbcli cluster describe-account CLUSTERNAME --name USERNAME + # describe account for instance + kbcli cluster describe-account --instance INSTANCE --name USERNAME ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for describe-account - -i, --instance string Specify the name of instance to be connected. - -u, --username string Required. Specify the name of user + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for describe-account + -i, --instance string Specify the name of instance to be connected. + --name string Required user name, please specify it ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_describe-config.md b/docs/user_docs/cli/kbcli_cluster_describe-config.md index f7127fd60..ed2d5ede7 100644 --- a/docs/user_docs/cli/kbcli_cluster_describe-config.md +++ b/docs/user_docs/cli/kbcli_cluster_describe-config.md @@ -15,23 +15,23 @@ kbcli cluster describe-config [flags] kbcli cluster describe-config mycluster # describe a component, e.g. cluster name is mycluster, component name is mysql - kbcli cluster describe-config mycluster --component-name=mysql + kbcli cluster describe-config mycluster --component=mysql # describe all configuration files. - kbcli cluster describe-config mycluster --component-name=mysql --show-detail + kbcli cluster describe-config mycluster --component=mysql --show-detail # describe a content of configuration file. - kbcli cluster describe-config mycluster --component-name=mysql --config-file=my.cnf --show-detail + kbcli cluster describe-config mycluster --component=mysql --config-file=my.cnf --show-detail ``` ### Options ``` - --component-name string Specify the name of Component to be describe (e.g. for apecloud-mysql: --component-name=mysql). If the cluster has only one component, unset the parameter." - --config-file strings Specify the name of the configuration file to be describe (e.g. for mysql: --config-file=my.cnf). If unset, all files. - --config-specs strings Specify the name of the configuration template to be describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl) - -h, --help help for describe-config - --show-detail If true, the content of the files specified by config-file will be printed. + --component string Specify the name of Component to describe (e.g. for apecloud-mysql: --component=mysql). If the cluster has only one component, unset the parameter." + --config-file strings Specify the name of the configuration file to be describe (e.g. for mysql: --config-file=my.cnf). If unset, all files. + --config-specs strings Specify the name of the configuration template to describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl) + -h, --help help for describe-config + --show-detail If true, the content of the files specified by config-file will be printed. ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_edit-backup-policy.md b/docs/user_docs/cli/kbcli_cluster_edit-backup-policy.md new file mode 100644 index 000000000..96c31087b --- /dev/null +++ b/docs/user_docs/cli/kbcli_cluster_edit-backup-policy.md @@ -0,0 +1,72 @@ +--- +title: kbcli cluster edit-backup-policy +--- + +Edit backup policy + +``` +kbcli cluster edit-backup-policy +``` + +### Examples + +``` + # edit backup policy + kbcli cluster edit-backup-policy + + # using short cmd to edit backup policy + kbcli cluster edit-bp +``` + +### Options + +``` + --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) + --field-manager string Name of the manager used to track field ownership. (default "kubectl-edit") + -f, --filename strings Filename, directory, or URL to files to use to edit the resource + -h, --help help for edit-backup-policy + -k, --kustomize string Process the kustomization directory. This flag can't be used together with -f or -R. + -o, --output string Output format. One of: (json, yaml, name, go-template, go-template-file, template, templatefile, jsonpath, jsonpath-as-json, jsonpath-file). + --output-patch Output the patch if the resource is edited. + -R, --recursive Process the directory used in -f, --filename recursively. Useful when you want to manage related manifests organized within the same directory. + --save-config If true, the configuration of current object will be saved in its annotation. Otherwise, the annotation will be unchanged. This flag is useful when you want to perform kubectl apply on this object in the future. + --show-managed-fields If true, keep the managedFields when printing objects in JSON or YAML format. + --subresource string If specified, edit will operate on the subresource of the requested object. Must be one of [status]. This flag is alpha and may change in the future. + --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. + --validate string[="strict"] Must be one of: strict (or true), warn, ignore (or false). + "true" or "strict" will use a schema to validate the input and fail the request if invalid. It will perform server side validation if ServerSideFieldValidation is enabled on the api-server, but will fall back to less reliable client-side validation if not. + "warn" will warn about unknown or duplicate fields without blocking the request if server-side field validation is enabled on the API server, and behave as "ignore" otherwise. + "false" or "ignore" will not perform any schema validation, silently dropping any unknown or duplicate fields. (default "strict") + --windows-line-endings Defaults to the line ending native to your platform. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli cluster](kbcli_cluster.md) - Cluster command. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_cluster_edit-config.md b/docs/user_docs/cli/kbcli_cluster_edit-config.md index 201cc04d6..8cf9025b2 100644 --- a/docs/user_docs/cli/kbcli_cluster_edit-config.md +++ b/docs/user_docs/cli/kbcli_cluster_edit-config.md @@ -5,15 +5,12 @@ title: kbcli cluster edit-config Edit the config file of the component. ``` -kbcli cluster edit-config [flags] +kbcli cluster edit-config NAME [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file] [flags] ``` ### Examples ``` - # edit config for component - kbcli cluster edit-config [--component=] [--config-spec=] [--config-file=] - # update mysql max_connections, cluster name is mycluster kbcli cluster edit-config mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf ``` @@ -21,14 +18,16 @@ kbcli cluster edit-config [flags] ### Options ``` - --component string Specify the name of Component to be updated. If the cluster has only one component, unset the parameter. - --config-file string Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'. - --config-spec string Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'. - -h, --help help for edit-config - --name string OpsRequest name. if not specified, it will be randomly generated - --replace Specify whether to replace the config file. Default to false. - --set strings Specify updated parameter list. For details about the parameters, refer to kbcli sub command: 'kbcli cluster describe-config'. - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --component string Specify the name of Component to be updated. If the cluster has only one component, unset the parameter. + --config-file string Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). For available templates and configs, refer to: 'kbcli cluster describe-config'. + --config-spec string Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). For available templates and configs, refer to: 'kbcli cluster describe-config'. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for edit-config + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --replace Boolean flag to enable replacing config file. Default with false. + --set strings Specify parameters list to be updated. For more details, refer to 'kbcli cluster describe-config'. + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_explain-config.md b/docs/user_docs/cli/kbcli_cluster_explain-config.md index 569b4d2d8..999696b22 100644 --- a/docs/user_docs/cli/kbcli_cluster_explain-config.md +++ b/docs/user_docs/cli/kbcli_cluster_explain-config.md @@ -11,28 +11,28 @@ kbcli cluster explain-config [flags] ### Examples ``` - # describe a cluster, e.g. cluster name is mycluster + # explain a cluster, e.g. cluster name is mycluster kbcli cluster explain-config mycluster - # describe a specified configure template, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl + # explain a specified configure template, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl - # describe a specified configure template, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl --trunc-document=false --trunc-enum=false + # explain a specified configure template, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl --trunc-document=false --trunc-enum=false - # describe a specified parameters, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl --param=sql_mode + # explain a specified parameters, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --param=sql_mode ``` ### Options ``` - --component-name string Specify the name of Component to be describe (e.g. for apecloud-mysql: --component-name=mysql). If the cluster has only one component, unset the parameter." - --config-specs strings Specify the name of the configuration template to be describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl) - -h, --help help for explain-config - --param string Specify the name of parameter to be query. It clearly display the details of the parameter. - --trunc-document If the document length of the parameter is greater than 100, it will be truncated. - --trunc-enum If the value list length of the parameter is greater than 20, it will be truncated. (default true) + --component string Specify the name of Component to describe (e.g. for apecloud-mysql: --component=mysql). If the cluster has only one component, unset the parameter." + --config-specs strings Specify the name of the configuration template to describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl) + -h, --help help for explain-config + --param string Specify the name of parameter to be query. It clearly display the details of the parameter. + --trunc-document If the document length of the parameter is greater than 100, it will be truncated. + --trunc-enum If the value list length of the parameter is greater than 20, it will be truncated. (default true) ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_expose.md b/docs/user_docs/cli/kbcli_cluster_expose.md index 1ce7171e0..e0a3cbbc3 100644 --- a/docs/user_docs/cli/kbcli_cluster_expose.md +++ b/docs/user_docs/cli/kbcli_cluster_expose.md @@ -2,10 +2,10 @@ title: kbcli cluster expose --- -Expose a cluster. +Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'. ``` -kbcli cluster expose [flags] +kbcli cluster expose NAME --enable=[true|false] --type=[vpc|internet] [flags] ``` ### Examples @@ -14,7 +14,7 @@ kbcli cluster expose [flags] # Expose a cluster to vpc kbcli cluster expose mycluster --type vpc --enable=true - # Expose a cluster to internet + # Expose a cluster to public internet kbcli cluster expose mycluster --type internet --enable=true # Stop exposing a cluster @@ -24,12 +24,15 @@ kbcli cluster expose [flags] ### Options ``` - --components strings Component names to this operations - --enable string Enable or disable the expose, values can be true or false - -h, --help help for expose - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed - --type string Expose type, currently supported types are 'vpc', 'internet' + --auto-approve Skip interactive approval before exposing the cluster + --components strings Component names to this operations + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --enable string Enable or disable the expose, values can be true or false + -h, --help help for expose + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --type string Expose type, currently supported types are 'vpc', 'internet' ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_grant-role.md b/docs/user_docs/cli/kbcli_cluster_grant-role.md index 8d1ad9c82..3ba1d71fa 100644 --- a/docs/user_docs/cli/kbcli_cluster_grant-role.md +++ b/docs/user_docs/cli/kbcli_cluster_grant-role.md @@ -12,17 +12,21 @@ kbcli cluster grant-role [flags] ``` # grant role to user - kbcli cluster grant-role NAME --component-name COMPNAME --username NAME --role ROLENAME + kbcli cluster grant-role CLUSTERNAME --component COMPNAME --name USERNAME --role ROLENAME + # grant role to user with default component + kbcli cluster grant-role CLUSTERNAME --name USERNAME --role ROLENAME + # grant role to user for instance + kbcli cluster grant-role --instance INSTANCE --name USERNAME --role ROLENAME ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for grant-role - -i, --instance string Specify the name of instance to be connected. - -r, --role string Role name should be one of {SUPERUSER, READWRITE, READONLY} - -u, --username string Required. Specify the name of user. + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for grant-role + -i, --instance string Specify the name of instance to be connected. + --name string Required user name, please specify it. + -r, --role string Role name should be one of {SUPERUSER, READWRITE, READONLY} ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_hscale.md b/docs/user_docs/cli/kbcli_cluster_hscale.md index 4854da0f8..49b3ded6f 100644 --- a/docs/user_docs/cli/kbcli_cluster_hscale.md +++ b/docs/user_docs/cli/kbcli_cluster_hscale.md @@ -5,24 +5,27 @@ title: kbcli cluster hscale Horizontally scale the specified components in the cluster. ``` -kbcli cluster hscale [flags] +kbcli cluster hscale NAME [flags] ``` ### Examples ``` - # expand storage resources of specified components, separate with commas when more than one - kbcli cluster hscale --components= --replicas=3 + # expand storage resources of specified components, separate with commas for multiple components + kbcli cluster hscale mycluster --components=mysql --replicas=3 ``` ### Options ``` - --components strings Component names to this operations - -h, --help help for hscale - --name string OpsRequest name. if not specified, it will be randomly generated - --replicas int Replicas with the specified components - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before horizontally scaling the cluster + --components strings Component names to this operations + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for hscale + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --replicas int Replicas with the specified components + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_create-user.md b/docs/user_docs/cli/kbcli_cluster_label.md similarity index 59% rename from docs/user_docs/cli/kbcli_cluster_create-user.md rename to docs/user_docs/cli/kbcli_cluster_label.md index c7c4a05d6..928139be7 100644 --- a/docs/user_docs/cli/kbcli_cluster_create-user.md +++ b/docs/user_docs/cli/kbcli_cluster_label.md @@ -1,33 +1,44 @@ --- -title: kbcli cluster create-user +title: kbcli cluster label --- -Create user for a cluster +Update the labels on cluster ``` -kbcli cluster create-user [flags] +kbcli cluster label NAME [flags] ``` ### Examples ``` - # create user - kbcli cluster create-user NAME --component-name COMPNAME --username NAME --password PASSWD - # create user without password - kbcli cluster create-user NAME --component-name COMPNAME --username NAME - # create user with expired interval - kbcli cluster create-user NAME --component-name COMPNAME --username NAME --password PASSWD --expiredAt 2046-01-02T15:04:05Z + # list label for clusters with specified name + kbcli cluster label mycluster --list + + # add label 'env' and value 'dev' for clusters with specified name + kbcli cluster label mycluster env=dev + + # add label 'env' and value 'dev' for all clusters + kbcli cluster label env=dev --all + + # add label 'env' and value 'dev' for the clusters that match the selector + kbcli cluster label env=dev -l type=mysql + + # update cluster with the label 'env' with value 'test', overwriting any existing value + kbcli cluster label mycluster --overwrite env=test + + # delete label env for clusters with specified name + kbcli cluster label mycluster env- ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for create-user - -i, --instance string Specify the name of instance to be connected. - -p, --password string Optional. Specify the password of user. The default value is empty, which means a random password will be generated. - -u, --username string Required. Specify the name of user, which must be unique. - --verbose Print verbose information. + --all Select all cluster + --dry-run string[="unchanged"] Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource. (default "none") + -h, --help help for label + --list If true, display the labels of the clusters + --overwrite If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels. + -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_list-accounts.md b/docs/user_docs/cli/kbcli_cluster_list-accounts.md index aa900d32a..353046a9e 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-accounts.md +++ b/docs/user_docs/cli/kbcli_cluster_list-accounts.md @@ -11,19 +11,20 @@ kbcli cluster list-accounts [flags] ### Examples ``` - # list all users from specified component of a cluster - kbcli cluster list-accounts NAME --component-name COMPNAME --show-connected-users - - # list all users from cluster's one particular instance - kbcli cluster list-accounts NAME -i INSTANCE + # list all users for component + kbcli cluster list-accounts CLUSTERNAME --component COMPNAME + # list all users with default component + kbcli cluster list-accounts CLUSTERNAME + # list all users from instance + kbcli cluster list-accounts --instance INSTANCE ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for list-accounts - -i, --instance string Specify the name of instance to be connected. + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for list-accounts + -i, --instance string Specify the name of instance to be connected. ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_list-restores.md b/docs/user_docs/cli/kbcli_cluster_list-backup-policy.md similarity index 87% rename from docs/user_docs/cli/kbcli_cluster_list-restores.md rename to docs/user_docs/cli/kbcli_cluster_list-backup-policy.md index b2f4d7448..8f92774f7 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-restores.md +++ b/docs/user_docs/cli/kbcli_cluster_list-backup-policy.md @@ -1,25 +1,28 @@ --- -title: kbcli cluster list-restores +title: kbcli cluster list-backup-policy --- -List all restore jobs. +List backups policies. ``` -kbcli cluster list-restores [flags] +kbcli cluster list-backup-policy [flags] ``` ### Examples ``` - # list all restore - kbcli cluster list-restore + # list all backup policies + kbcli cluster list-backup-policy + + # using short cmd to list backup policy of the specified cluster + kbcli cluster list-bp mycluster ``` ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. - -h, --help help for list-restores + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -h, --help help for list-backup-policy -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. --show-labels When printing, show all labels as the last column (default hide labels column) diff --git a/docs/user_docs/cli/kbcli_cluster_list-backups.md b/docs/user_docs/cli/kbcli_cluster_list-backups.md index c0d34dabe..5a3b6baf7 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-backups.md +++ b/docs/user_docs/cli/kbcli_cluster_list-backups.md @@ -11,15 +11,16 @@ kbcli cluster list-backups [flags] ### Examples ``` - # list all backup + # list all backups kbcli cluster list-backups ``` ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list-backups + --name string The backup name to get the details. -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. --show-labels When printing, show all labels as the last column (default hide labels column) diff --git a/docs/user_docs/cli/kbcli_cluster_list-components.md b/docs/user_docs/cli/kbcli_cluster_list-components.md index 1ced9d76b..0c321d5a6 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-components.md +++ b/docs/user_docs/cli/kbcli_cluster_list-components.md @@ -21,7 +21,7 @@ kbcli cluster list-components [flags] ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list-components -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. ``` diff --git a/docs/user_docs/cli/kbcli_cluster_list-events.md b/docs/user_docs/cli/kbcli_cluster_list-events.md index 18f45a03c..0f8e3a93a 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-events.md +++ b/docs/user_docs/cli/kbcli_cluster_list-events.md @@ -21,7 +21,7 @@ kbcli cluster list-events [flags] ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list-events -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. ``` diff --git a/docs/user_docs/cli/kbcli_cluster_list-instances.md b/docs/user_docs/cli/kbcli_cluster_list-instances.md index b15c43306..f54c2081a 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-instances.md +++ b/docs/user_docs/cli/kbcli_cluster_list-instances.md @@ -21,7 +21,7 @@ kbcli cluster list-instances [flags] ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list-instances -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. ``` diff --git a/docs/user_docs/cli/kbcli_cluster_list-ops.md b/docs/user_docs/cli/kbcli_cluster_list-ops.md index d30a4b324..5c16212f1 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-ops.md +++ b/docs/user_docs/cli/kbcli_cluster_list-ops.md @@ -21,13 +21,13 @@ kbcli cluster list-ops [flags] ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list-ops --name string The OpsRequest name to get the details. -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. --show-labels When printing, show all labels as the last column (default hide labels column) - --status strings Options include all, pending, running, succeeded, failed. by default, outputs the pending/running/failed OpsRequest. (default [running,pending,failed]) + --status strings Options include all, pending, creating, running, canceling, failed. by default, outputs the pending/creating/running/canceling/failed OpsRequest. (default [pending,creating,running,canceling,failed]) --type strings The OpsRequest type ``` diff --git a/docs/user_docs/cli/kbcli_cluster_list.md b/docs/user_docs/cli/kbcli_cluster_list.md index 70614a0f3..5e100ec35 100644 --- a/docs/user_docs/cli/kbcli_cluster_list.md +++ b/docs/user_docs/cli/kbcli_cluster_list.md @@ -14,7 +14,7 @@ kbcli cluster list [NAME] [flags] # list all clusters kbcli cluster list - # list a single cluster with specified NAME + # list a single cluster with specified name kbcli cluster list mycluster # list a single cluster in YAML output format @@ -30,7 +30,7 @@ kbcli cluster list [NAME] [flags] ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. diff --git a/docs/user_docs/cli/kbcli_cluster_logs.md b/docs/user_docs/cli/kbcli_cluster_logs.md index d18dd27f0..a445b8b60 100644 --- a/docs/user_docs/cli/kbcli_cluster_logs.md +++ b/docs/user_docs/cli/kbcli_cluster_logs.md @@ -45,8 +45,8 @@ kbcli cluster logs NAME [flags] ``` -c, --container string Container name. - --file-path string Log-file path. Specify target file path and have a premium priority. No set file-path and file-type will output stdout/stderr of target container. - --file-type string Log-file type. Can see the output info of list-logs cmd. No set file-path and file-type will output stdout/stderr of target container. + --file-path string Log-file path. File path has a priority over file-type. When file-path and file-type are unset, output stdout/stderr of target container. + --file-type string Log-file type. List them with list-logs cmd. When file-path and file-type are unset, output stdout/stderr of target container. -f, --follow Specify if the logs should be streamed. -h, --help help for logs --ignore-errors If watching / following pod logs, allow for any errors that occur to be non-fatal. Only take effect for stdout&stderr. @@ -56,7 +56,7 @@ kbcli cluster logs NAME [flags] -p, --previous If true, print the logs for the previous instance of the container in a pod if it exists. Only take effect for stdout&stderr. --since duration Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used. Only take effect for stdout&stderr. --since-time string Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used. Only take effect for stdout&stderr. - --tail int Lines of recent log file to display. Defaults to -1 with showing all log lines. (default -1) + --tail int Lines of recent log file to display. Defaults to -1 for showing all log lines. (default -1) --timestamps Include timestamps on each line in the log output. Only take effect for stdout&stderr. ``` diff --git a/docs/user_docs/cli/kbcli_cluster_restart.md b/docs/user_docs/cli/kbcli_cluster_restart.md index 884a18a07..67185afbf 100644 --- a/docs/user_docs/cli/kbcli_cluster_restart.md +++ b/docs/user_docs/cli/kbcli_cluster_restart.md @@ -5,26 +5,29 @@ title: kbcli cluster restart Restart the specified components in the cluster. ``` -kbcli cluster restart [flags] +kbcli cluster restart NAME [flags] ``` ### Examples ``` # restart all components - kbcli cluster restart + kbcli cluster restart mycluster - # restart specifies the component, separate with commas when more than one - kbcli cluster restart --components= + # specified component to restart, separate with commas for multiple components + kbcli cluster restart mycluster --components=mysql ``` ### Options ``` - --components strings Component names to this operations - -h, --help help for restart - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before restarting the cluster + --components strings Component names to this operations + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for restart + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_restore.md b/docs/user_docs/cli/kbcli_cluster_restore.md index 605bed030..d41594a45 100644 --- a/docs/user_docs/cli/kbcli_cluster_restore.md +++ b/docs/user_docs/cli/kbcli_cluster_restore.md @@ -13,13 +13,18 @@ kbcli cluster restore [flags] ``` # restore a new cluster from a backup kbcli cluster restore new-cluster-name --backup backup-name + + # restore a new cluster from point in time + kbcli cluster restore new-cluster-name --restore-to-time "Apr 13,2023 18:40:35 UTC+0800" --source-cluster mycluster ``` ### Options ``` - --backup string Backup name - -h, --help help for restore + --backup string Backup name + -h, --help help for restore + --restore-to-time string point in time recovery(PITR) + --source-cluster string source cluster name ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_revoke-role.md b/docs/user_docs/cli/kbcli_cluster_revoke-role.md index 2a26d7ab8..5873706cf 100644 --- a/docs/user_docs/cli/kbcli_cluster_revoke-role.md +++ b/docs/user_docs/cli/kbcli_cluster_revoke-role.md @@ -12,17 +12,21 @@ kbcli cluster revoke-role [flags] ``` # revoke role from user - kbcli cluster revoke-role NAME --component-name COMPNAME --role ROLENAME + kbcli cluster revoke-role CLUSTERNAME --component COMPNAME --name USERNAME --role ROLENAME + # revoke role from user with default component + kbcli cluster revoke-role CLUSTERNAME --name USERNAME --role ROLENAME + # revoke role from user for instance + kbcli cluster revoke-role --instance INSTANCE --name USERNAME --role ROLENAME ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for revoke-role - -i, --instance string Specify the name of instance to be connected. - -r, --role string Role name should be one of {SUPERUSER, READWRITE, READONLY} - -u, --username string Required. Specify the name of user. + --component string Specify the name of component to be connected. If not specified, pick the first one. + -h, --help help for revoke-role + -i, --instance string Specify the name of instance to be connected. + --name string Required user name, please specify it. + -r, --role string Role name should be one of {SUPERUSER, READWRITE, READONLY} ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_start.md b/docs/user_docs/cli/kbcli_cluster_start.md index ddee7e7b4..72ddd7b47 100644 --- a/docs/user_docs/cli/kbcli_cluster_start.md +++ b/docs/user_docs/cli/kbcli_cluster_start.md @@ -5,22 +5,24 @@ title: kbcli cluster start Start the cluster if cluster is stopped. ``` -kbcli cluster start [flags] +kbcli cluster start NAME [flags] ``` ### Examples ``` # start the cluster when cluster is stopped - kbcli cluster start + kbcli cluster start mycluster ``` ### Options ``` - -h, --help help for start - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for start + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_stop.md b/docs/user_docs/cli/kbcli_cluster_stop.md index 2fd02badb..a29d6ef76 100644 --- a/docs/user_docs/cli/kbcli_cluster_stop.md +++ b/docs/user_docs/cli/kbcli_cluster_stop.md @@ -5,22 +5,25 @@ title: kbcli cluster stop Stop the cluster and release all the pods of the cluster. ``` -kbcli cluster stop [flags] +kbcli cluster stop NAME [flags] ``` ### Examples ``` # stop the cluster and release all the pods of the cluster - kbcli cluster stop + kbcli cluster stop mycluster ``` ### Options ``` - -h, --help help for stop - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before stopping the cluster + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for stop + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_update.md b/docs/user_docs/cli/kbcli_cluster_update.md index d94cf8803..231b7797a 100644 --- a/docs/user_docs/cli/kbcli_cluster_update.md +++ b/docs/user_docs/cli/kbcli_cluster_update.md @@ -25,6 +25,12 @@ kbcli cluster update NAME [flags] # update cluster tolerations kbcli cluster update mycluster --tolerations='"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + + # edit cluster + kbcli cluster update mycluster --edit + + # enable cluster monitor and edit + # kbcli cluster update mycluster --monitor=true --edit ``` ### Options @@ -32,7 +38,8 @@ kbcli cluster update NAME [flags] ``` --allow-missing-template-keys If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats. (default true) --dry-run string[="unchanged"] Must be "none", "server", or "client". If client strategy, only print the object that would be sent, without sending it. If server strategy, submit server-side request without persisting the resource. (default "none") - --enable-all-logs Enable advanced application all log extraction, and true will ignore enabledLogs of component level (default true) + --edit Edit the API resource + --enable-all-logs Enable advanced application all log extraction, set to true will ignore enabledLogs of component level, default is false -h, --help help for update --monitor Set monitor enabled and inject metrics exporter (default true) --node-labels stringToString Node label selector (default []) @@ -42,7 +49,7 @@ kbcli cluster update NAME [flags] --template string Template string or path to template file to use when -o=go-template, -o=go-template-file. The template format is golang templates [http://golang.org/pkg/text/template/#pkg-overview]. --tenancy string Tenancy options, one of: (SharedNode, DedicatedNode) (default "SharedNode") --termination-policy string Termination policy, one of: (DoNotTerminate, Halt, Delete, WipeOut) (default "Delete") - --tolerations strings Tolerations for cluster, such as '"key=engineType,value=mongo,operator=Equal,effect=NoSchedule"' + --tolerations strings Tolerations for cluster, such as "key=value:effect, key:effect", for example '"engineType=mongo:NoSchedule", "diskType:NoSchedule"' --topology-keys stringArray Topology keys for affinity ``` diff --git a/docs/user_docs/cli/kbcli_cluster_upgrade.md b/docs/user_docs/cli/kbcli_cluster_upgrade.md index fa2e58dcd..0fa55c9d6 100644 --- a/docs/user_docs/cli/kbcli_cluster_upgrade.md +++ b/docs/user_docs/cli/kbcli_cluster_upgrade.md @@ -5,23 +5,26 @@ title: kbcli cluster upgrade Upgrade the cluster version. ``` -kbcli cluster upgrade [flags] +kbcli cluster upgrade NAME [flags] ``` ### Examples ``` - # upgrade the cluster to the specified version - kbcli cluster upgrade --cluster-version= + # upgrade the cluster to the target version + kbcli cluster upgrade mycluster --cluster-version=ac-mysql-8.0.30 ``` ### Options ``` - --cluster-version string Reference cluster version (required) - -h, --help help for upgrade - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before upgrading the cluster + --cluster-version string Reference cluster version (required) + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for upgrade + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_cluster_volume-expand.md b/docs/user_docs/cli/kbcli_cluster_volume-expand.md index e536aa408..172393886 100644 --- a/docs/user_docs/cli/kbcli_cluster_volume-expand.md +++ b/docs/user_docs/cli/kbcli_cluster_volume-expand.md @@ -5,23 +5,25 @@ title: kbcli cluster volume-expand Expand volume with the specified components and volumeClaimTemplates in the cluster. ``` -kbcli cluster volume-expand [flags] +kbcli cluster volume-expand NAME [flags] ``` ### Examples ``` - # restart specifies the component, separate with commas when more than one - kbcli cluster volume-expand --components= \ - --volume-claim-templates=data --storage=10Gi + # restart specifies the component, separate with commas for multiple components + kbcli cluster volume-expand mycluster --components=mysql --volume-claim-templates=data --storage=10Gi ``` ### Options ``` - --components strings Component names to this operations + --auto-approve Skip interactive approval before expanding the cluster volume + --components strings Component names to this operations + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") -h, --help help for volume-expand --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) --storage string Volume storage size (required) --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed -t, --volume-claim-templates strings VolumeClaimTemplate names in components (required) diff --git a/docs/user_docs/cli/kbcli_cluster_vscale.md b/docs/user_docs/cli/kbcli_cluster_vscale.md index 1817bc4fe..905895fc9 100644 --- a/docs/user_docs/cli/kbcli_cluster_vscale.md +++ b/docs/user_docs/cli/kbcli_cluster_vscale.md @@ -5,26 +5,32 @@ title: kbcli cluster vscale Vertically scale the specified components in the cluster. ``` -kbcli cluster vscale [flags] +kbcli cluster vscale NAME [flags] ``` ### Examples ``` - # scale the computing resources of specified components, separate with commas when more than one - kbcli cluster vscale --components= --cpu=500m --memory=500Mi + # scale the computing resources of specified components, separate with commas for multiple components + kbcli cluster vscale mycluster --components=mysql --cpu=500m --memory=500Mi + + # scale the computing resources of specified components by class, run command 'kbcli class list --cluster-definition cluster-definition-name' to get available classes + kbcli cluster vscale mycluster --components=mysql --class=general-2c4g ``` ### Options ``` - --class string Component class - --components strings Component names to this operations - --cpu string Requested and limited size of component cpu - -h, --help help for vscale - --memory string Requested and limited size of component memory - --name string OpsRequest name. if not specified, it will be randomly generated - --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed + --auto-approve Skip interactive approval before vertically scaling the cluster + --class string Component class + --components strings Component names to this operations + --cpu string Request and limit size of component cpu + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + -h, --help help for vscale + --memory string Request and limit size of component memory + --name string OpsRequest name. if not specified, it will be randomly generated + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --ttlSecondsAfterSucceed int Time to live after the OpsRequest succeed ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_clusterdefinition.md b/docs/user_docs/cli/kbcli_clusterdefinition.md index 2fee0e6cb..02f8872f2 100644 --- a/docs/user_docs/cli/kbcli_clusterdefinition.md +++ b/docs/user_docs/cli/kbcli_clusterdefinition.md @@ -38,6 +38,7 @@ ClusterDefinition command. * [kbcli clusterdefinition list](kbcli_clusterdefinition_list.md) - List ClusterDefinitions. +* [kbcli clusterdefinition list-components](kbcli_clusterdefinition_list-components.md) - List cluster definition components. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_cluster_desc-user.md b/docs/user_docs/cli/kbcli_clusterdefinition_list-components.md similarity index 77% rename from docs/user_docs/cli/kbcli_cluster_desc-user.md rename to docs/user_docs/cli/kbcli_clusterdefinition_list-components.md index acd9c6c38..503f889a4 100644 --- a/docs/user_docs/cli/kbcli_cluster_desc-user.md +++ b/docs/user_docs/cli/kbcli_clusterdefinition_list-components.md @@ -1,28 +1,24 @@ --- -title: kbcli cluster desc-user +title: kbcli clusterdefinition list-components --- -Describe user roles and related information +List cluster definition components. ``` -kbcli cluster desc-user [flags] +kbcli clusterdefinition list-components [flags] ``` ### Examples ``` - # describe user and show role information - kbcli cluster desc-user NAME --component-name COMPNAME--username NAME + # List all components belonging to the cluster definition. + kbcli clusterdefinition list-components apecloud-mysql ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for desc-user - -i, --instance string Specify the name of instance to be connected. - -u, --username string Required. Specify the name of user - --verbose Print verbose information. + -h, --help help for list-components ``` ### Options inherited from parent commands @@ -51,7 +47,7 @@ kbcli cluster desc-user [flags] ### SEE ALSO -* [kbcli cluster](kbcli_cluster.md) - Cluster command. +* [kbcli clusterdefinition](kbcli_clusterdefinition.md) - ClusterDefinition command. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_clusterdefinition_list.md b/docs/user_docs/cli/kbcli_clusterdefinition_list.md index 901814591..6a4459db5 100644 --- a/docs/user_docs/cli/kbcli_clusterdefinition_list.md +++ b/docs/user_docs/cli/kbcli_clusterdefinition_list.md @@ -11,14 +11,13 @@ kbcli clusterdefinition list [flags] ### Examples ``` - # list all ClusterDefinition + # list all ClusterDefinitions kbcli clusterdefinition list ``` ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. -h, --help help for list -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. diff --git a/docs/user_docs/cli/kbcli_clusterversion.md b/docs/user_docs/cli/kbcli_clusterversion.md index 134243281..3a11304dd 100644 --- a/docs/user_docs/cli/kbcli_clusterversion.md +++ b/docs/user_docs/cli/kbcli_clusterversion.md @@ -38,6 +38,8 @@ ClusterVersion command. * [kbcli clusterversion list](kbcli_clusterversion_list.md) - List ClusterVersions. +* [kbcli clusterversion set-default](kbcli_clusterversion_set-default.md) - Set the clusterversion to the default clusterversion for its clusterdefinition. +* [kbcli clusterversion unset-default](kbcli_clusterversion_unset-default.md) - Unset the clusterversion if it's default. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_clusterversion_list.md b/docs/user_docs/cli/kbcli_clusterversion_list.md index 6124e60e0..8f9efdfeb 100644 --- a/docs/user_docs/cli/kbcli_clusterversion_list.md +++ b/docs/user_docs/cli/kbcli_clusterversion_list.md @@ -11,18 +11,18 @@ kbcli clusterversion list [flags] ### Examples ``` - # list all ClusterVersion + # list all ClusterVersions kbcli clusterversion list ``` ### Options ``` - -A, --all-namespace If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. - -h, --help help for list - -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) - -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. - --show-labels When printing, show all labels as the last column (default hide labels column) + --cluster-definition string Specify cluster definition, run "kbcli clusterdefinition list" to show all available cluster definition + -h, --help help for list + -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) + -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. + --show-labels When printing, show all labels as the last column (default hide labels column) ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_clusterversion_set-default.md b/docs/user_docs/cli/kbcli_clusterversion_set-default.md new file mode 100644 index 000000000..05b0702a2 --- /dev/null +++ b/docs/user_docs/cli/kbcli_clusterversion_set-default.md @@ -0,0 +1,53 @@ +--- +title: kbcli clusterversion set-default +--- + +Set the clusterversion to the default clusterversion for its clusterdefinition. + +``` +kbcli clusterversion set-default NAME [flags] +``` + +### Examples + +``` + # set ac-mysql-8.0.30 as the default clusterversion + kbcli clusterversion set-default ac-mysql-8.0.30 +``` + +### Options + +``` + -h, --help help for set-default +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli clusterversion](kbcli_clusterversion.md) - ClusterVersion command. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_clusterversion_unset-default.md b/docs/user_docs/cli/kbcli_clusterversion_unset-default.md new file mode 100644 index 000000000..5543dc71a --- /dev/null +++ b/docs/user_docs/cli/kbcli_clusterversion_unset-default.md @@ -0,0 +1,53 @@ +--- +title: kbcli clusterversion unset-default +--- + +Unset the clusterversion if it's default. + +``` +kbcli clusterversion unset-default NAME [flags] +``` + +### Examples + +``` + # unset ac-mysql-8.0.30 to default clusterversion if it's default + kbcli clusterversion unset-default ac-mysql-8.0.30 +``` + +### Options + +``` + -h, --help help for unset-default +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli clusterversion](kbcli_clusterversion.md) - ClusterVersion command. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_dashboard_open.md b/docs/user_docs/cli/kbcli_dashboard_open.md index ad04aa4fd..19418d1e0 100644 --- a/docs/user_docs/cli/kbcli_dashboard_open.md +++ b/docs/user_docs/cli/kbcli_dashboard_open.md @@ -22,7 +22,7 @@ kbcli dashboard open [flags] ``` -h, --help help for open - --pod-running-timeout duration The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running (default 1m0s) + --pod-running-timeout duration The time (like 5s, 2m, or 3h, higher than zero) to wait for at least one pod is running (default 1m0s) --port string dashboard local port ``` diff --git a/docs/user_docs/cli/kbcli_fault.md b/docs/user_docs/cli/kbcli_fault.md new file mode 100644 index 000000000..600b04e7e --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault.md @@ -0,0 +1,48 @@ +--- +title: kbcli fault +--- + +Inject faults to pod. + +### Options + +``` + -h, --help help for fault +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. +* [kbcli fault node](kbcli_fault_node.md) - Node chaos. +* [kbcli fault pod](kbcli_fault_pod.md) - Pod chaos. +* [kbcli fault stress](kbcli_fault_stress.md) - Add memory pressure or CPU load to the system. +* [kbcli fault time](kbcli_fault_time.md) - Clock skew failure. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_io.md b/docs/user_docs/cli/kbcli_fault_io.md new file mode 100644 index 000000000..80f1f2ced --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_io.md @@ -0,0 +1,46 @@ +--- +title: kbcli fault io +--- + +IO chaos. + +### Options + +``` + -h, --help help for io +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. +* [kbcli fault io attribute](kbcli_fault_io_attribute.md) - Override the attributes of the file. +* [kbcli fault io errno](kbcli_fault_io_errno.md) - Causes IO operations to return specific errors. +* [kbcli fault io latency](kbcli_fault_io_latency.md) - Delayed IO operations. +* [kbcli fault io mistake](kbcli_fault_io_mistake.md) - Alters the contents of the file, distorting the contents of the file. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_io_attribute.md b/docs/user_docs/cli/kbcli_fault_io_attribute.md new file mode 100644 index 000000000..7d5d37886 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_io_attribute.md @@ -0,0 +1,94 @@ +--- +title: kbcli fault io attribute +--- + +Override the attributes of the file. + +``` +kbcli fault io attribute [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods. Delay all IO operations under the /data path by 10s. + kbcli fault io latency --delay=10s --volume-path=/data + + # Affects the first container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data + + # Affects the mysql container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data -c=mysql + + # There is a 50% probability of affecting the read IO operation of the test.txt file under the /data path. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data --path=test.txt --percent=50 --method=READ -c=mysql + + # Same as above.Make all IO operations under the /data path return the specified error number 22 (Invalid argument). + kbcli fault io errno --volume-path=/data --errno=22 + + # Same as above.Modify the IO operation permission attribute of the files under the /data path to 72.(110 in octal). + kbcli fault io attribute --volume-path=/data --perm=72 + + # Modify all files so that random positions of 1's with a maximum length of 10 bytes will be replaced with 0's. + kbcli fault io mistake --volume-path=/data --filling=zero --max-occurrences=10 --max-length=1 +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --blocks uint The number of blocks the file occupies. + -c, --container stringArray The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + --gid uint32 The owner's group ID. + -h, --help help for attribute + --ino uint ino number. + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method stringArray The file system calls that need to inject faults. For example: WRITE READ + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --nlink uint32 The number of hard links. + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The effective scope of the injection error can be a wildcard or a single file. + --percent int Probability of failure per operation, in %. (default 100) + --perm uint16 Decimal representation of file permissions. + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --size uint File size. + --uid uint32 Owner's user ID. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --volume-path string The mount point of the volume in the target container must be the root directory of the mount. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_io_errno.md b/docs/user_docs/cli/kbcli_fault_io_errno.md new file mode 100644 index 000000000..f04c961a7 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_io_errno.md @@ -0,0 +1,88 @@ +--- +title: kbcli fault io errno +--- + +Causes IO operations to return specific errors. + +``` +kbcli fault io errno [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods. Delay all IO operations under the /data path by 10s. + kbcli fault io latency --delay=10s --volume-path=/data + + # Affects the first container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data + + # Affects the mysql container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data -c=mysql + + # There is a 50% probability of affecting the read IO operation of the test.txt file under the /data path. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data --path=test.txt --percent=50 --method=READ -c=mysql + + # Same as above.Make all IO operations under the /data path return the specified error number 22 (Invalid argument). + kbcli fault io errno --volume-path=/data --errno=22 + + # Same as above.Modify the IO operation permission attribute of the files under the /data path to 72.(110 in octal). + kbcli fault io attribute --volume-path=/data --perm=72 + + # Modify all files so that random positions of 1's with a maximum length of 10 bytes will be replaced with 0's. + kbcli fault io mistake --volume-path=/data --filling=zero --max-occurrences=10 --max-length=1 +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --container stringArray The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + --errno int The returned error number. + -h, --help help for errno + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method stringArray The file system calls that need to inject faults. For example: WRITE READ + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The effective scope of the injection error can be a wildcard or a single file. + --percent int Probability of failure per operation, in %. (default 100) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --volume-path string The mount point of the volume in the target container must be the root directory of the mount. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_io_latency.md b/docs/user_docs/cli/kbcli_fault_io_latency.md new file mode 100644 index 000000000..6422ecc2b --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_io_latency.md @@ -0,0 +1,88 @@ +--- +title: kbcli fault io latency +--- + +Delayed IO operations. + +``` +kbcli fault io latency [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods. Delay all IO operations under the /data path by 10s. + kbcli fault io latency --delay=10s --volume-path=/data + + # Affects the first container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data + + # Affects the mysql container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data -c=mysql + + # There is a 50% probability of affecting the read IO operation of the test.txt file under the /data path. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data --path=test.txt --percent=50 --method=READ -c=mysql + + # Same as above.Make all IO operations under the /data path return the specified error number 22 (Invalid argument). + kbcli fault io errno --volume-path=/data --errno=22 + + # Same as above.Modify the IO operation permission attribute of the files under the /data path to 72.(110 in octal). + kbcli fault io attribute --volume-path=/data --perm=72 + + # Modify all files so that random positions of 1's with a maximum length of 10 bytes will be replaced with 0's. + kbcli fault io mistake --volume-path=/data --filling=zero --max-occurrences=10 --max-length=1 +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --container stringArray The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected. + --delay string Specific delay time. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for latency + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method stringArray The file system calls that need to inject faults. For example: WRITE READ + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The effective scope of the injection error can be a wildcard or a single file. + --percent int Probability of failure per operation, in %. (default 100) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --volume-path string The mount point of the volume in the target container must be the root directory of the mount. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_io_mistake.md b/docs/user_docs/cli/kbcli_fault_io_mistake.md new file mode 100644 index 000000000..10fb28f29 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_io_mistake.md @@ -0,0 +1,90 @@ +--- +title: kbcli fault io mistake +--- + +Alters the contents of the file, distorting the contents of the file. + +``` +kbcli fault io mistake [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods. Delay all IO operations under the /data path by 10s. + kbcli fault io latency --delay=10s --volume-path=/data + + # Affects the first container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data + + # Affects the mysql container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data -c=mysql + + # There is a 50% probability of affecting the read IO operation of the test.txt file under the /data path. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data --path=test.txt --percent=50 --method=READ -c=mysql + + # Same as above.Make all IO operations under the /data path return the specified error number 22 (Invalid argument). + kbcli fault io errno --volume-path=/data --errno=22 + + # Same as above.Modify the IO operation permission attribute of the files under the /data path to 72.(110 in octal). + kbcli fault io attribute --volume-path=/data --perm=72 + + # Modify all files so that random positions of 1's with a maximum length of 10 bytes will be replaced with 0's. + kbcli fault io mistake --volume-path=/data --filling=zero --max-occurrences=10 --max-length=1 +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --container stringArray The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + --filling string The filling content of the error data can only be zero (filling with 0) or random (filling with random bytes). + -h, --help help for mistake + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --max-length int The maximum length (in bytes) of each error. (default 1) + --max-occurrences int The maximum number of times an error can occur per operation. (default 1) + --method stringArray The file system calls that need to inject faults. For example: WRITE READ + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The effective scope of the injection error can be a wildcard or a single file. + --percent int Probability of failure per operation, in %. (default 100) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --volume-path string The mount point of the volume in the target container must be the root directory of the mount. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault io](kbcli_fault_io.md) - IO chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network.md b/docs/user_docs/cli/kbcli_fault_network.md new file mode 100644 index 000000000..414204069 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network.md @@ -0,0 +1,50 @@ +--- +title: kbcli fault network +--- + +Network chaos. + +### Options + +``` + -h, --help help for network +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. +* [kbcli fault network bandwidth](kbcli_fault_network_bandwidth.md) - Limit the bandwidth that pods use to communicate with other objects. +* [kbcli fault network corrupt](kbcli_fault_network_corrupt.md) - Distorts the messages a pod communicates with other objects. +* [kbcli fault network delay](kbcli_fault_network_delay.md) - Make pods communicate with other objects lazily. +* [kbcli fault network dns](kbcli_fault_network_dns.md) - Inject faults into DNS server. +* [kbcli fault network duplicate](kbcli_fault_network_duplicate.md) - Make pods communicate with other objects to pick up duplicate packets. +* [kbcli fault network http](kbcli_fault_network_http.md) - Intercept HTTP requests and responses. +* [kbcli fault network loss](kbcli_fault_network_loss.md) - Cause pods to communicate with other objects to drop packets. +* [kbcli fault network partition](kbcli_fault_network_partition.md) - Make a pod network partitioned from other objects. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_bandwidth.md b/docs/user_docs/cli/kbcli_fault_network_bandwidth.md new file mode 100644 index 000000000..1781a1bc3 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_bandwidth.md @@ -0,0 +1,107 @@ +--- +title: kbcli fault network bandwidth +--- + +Limit the bandwidth that pods use to communicate with other objects. + +``` +kbcli fault network bandwidth [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --buffer uint32 the maximum number of bytes that can be sent instantaneously. (default 1) + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for bandwidth + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --limit uint32 the number of bytes waiting in the queue. (default 1) + --minburst uint32 the size of the peakrate bucket. + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --peakrate uint the maximum consumption rate of the bucket. + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --rate string the rate at which the bandwidth is limited. For example : 10 bps/kbps/mbps/gbps. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_corrupt.md b/docs/user_docs/cli/kbcli_fault_network_corrupt.md new file mode 100644 index 000000000..c083c04d2 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_corrupt.md @@ -0,0 +1,104 @@ +--- +title: kbcli fault network corrupt +--- + +Distorts the messages a pod communicates with other objects. + +``` +kbcli fault network corrupt [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --correlation string Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100]. + --corrupt string Indicates the probability of a packet error occurring. Value range: [0, 100]. + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for corrupt + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_delay.md b/docs/user_docs/cli/kbcli_fault_network_delay.md new file mode 100644 index 000000000..69b78641f --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_delay.md @@ -0,0 +1,105 @@ +--- +title: kbcli fault network delay +--- + +Make pods communicate with other objects lazily. + +``` +kbcli fault network delay [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --correlation string Indicates the probability of a packet error occurring. Value range: [0, 100]. + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for delay + --jitter string the variation range of the delay time. + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --latency string the length of time to delay. + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_dns.md b/docs/user_docs/cli/kbcli_fault_network_dns.md new file mode 100644 index 000000000..e049edbbf --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_dns.md @@ -0,0 +1,44 @@ +--- +title: kbcli fault network dns +--- + +Inject faults into DNS server. + +### Options + +``` + -h, --help help for dns +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. +* [kbcli fault network dns error](kbcli_fault_network_dns_error.md) - Make DNS return an error when resolving external domain names. +* [kbcli fault network dns random](kbcli_fault_network_dns_random.md) - Make DNS return any IP when resolving external domain names. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_dns_error.md b/docs/user_docs/cli/kbcli_fault_network_dns_error.md new file mode 100644 index 000000000..517364f13 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_dns_error.md @@ -0,0 +1,68 @@ +--- +title: kbcli fault network dns error +--- + +Make DNS return an error when resolving external domain names. + +``` +kbcli fault network dns error [flags] +``` + +### Examples + +``` + // Inject DNS faults into all pods under the default namespace, so that any IP is returned when accessing the bing.com domain name. + kbcli fault dns random --patterns=bing.com --duration=1m + + // Inject DNS faults into all pods under the default namespace, so that error is returned when accessing the bing.com domain name. + kbcli fault dns error --patterns=bing.com --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for error + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --patterns stringArray Select the domain name template that matching the failure behavior & supporting placeholders ? and wildcards *. + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network dns](kbcli_fault_network_dns.md) - Inject faults into DNS server. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_dns_random.md b/docs/user_docs/cli/kbcli_fault_network_dns_random.md new file mode 100644 index 000000000..edafd17c2 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_dns_random.md @@ -0,0 +1,68 @@ +--- +title: kbcli fault network dns random +--- + +Make DNS return any IP when resolving external domain names. + +``` +kbcli fault network dns random [flags] +``` + +### Examples + +``` + // Inject DNS faults into all pods under the default namespace, so that any IP is returned when accessing the bing.com domain name. + kbcli fault dns random --patterns=bing.com --duration=1m + + // Inject DNS faults into all pods under the default namespace, so that error is returned when accessing the bing.com domain name. + kbcli fault dns error --patterns=bing.com --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for random + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --patterns stringArray Select the domain name template that matching the failure behavior & supporting placeholders ? and wildcards *. + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network dns](kbcli_fault_network_dns.md) - Inject faults into DNS server. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_duplicate.md b/docs/user_docs/cli/kbcli_fault_network_duplicate.md new file mode 100644 index 000000000..06ee7ba5d --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_duplicate.md @@ -0,0 +1,104 @@ +--- +title: kbcli fault network duplicate +--- + +Make pods communicate with other objects to pick up duplicate packets. + +``` +kbcli fault network duplicate [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --correlation string Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100]. + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duplicate string the probability of a packet being repeated. Value range: [0, 100]. + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for duplicate + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_http.md b/docs/user_docs/cli/kbcli_fault_network_http.md new file mode 100644 index 000000000..294cf3082 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_http.md @@ -0,0 +1,46 @@ +--- +title: kbcli fault network http +--- + +Intercept HTTP requests and responses. + +### Options + +``` + -h, --help help for http +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. +* [kbcli fault network http abort](kbcli_fault_network_http_abort.md) - Abort the HTTP request and response. +* [kbcli fault network http delay](kbcli_fault_network_http_delay.md) - Delay the HTTP request and response. +* [kbcli fault network http patch](kbcli_fault_network_http_patch.md) - Patch the HTTP request and response. +* [kbcli fault network http replace](kbcli_fault_network_http_replace.md) - Replace the HTTP request and response. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_http_abort.md b/docs/user_docs/cli/kbcli_fault_network_http_abort.md new file mode 100644 index 000000000..0d2f3ca1b --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_http_abort.md @@ -0,0 +1,91 @@ +--- +title: kbcli fault network http abort +--- + +Abort the HTTP request and response. + +``` +kbcli fault network http abort [flags] +``` + +### Examples + +``` + # By default, the method of GET from port 80 is blocked. + kbcli fault network http abort --duration=1m + + # Block the method of GET from port 4399. + kbcli fault network http abort --port=4399 --duration=1m + + # Block the method of POST from port 4399. + kbcli fault network http abort --port=4399 --method=POST --duration=1m + + # Delays post requests from port 4399. + kbcli fault network http delay --port=4399 --method=POST --delay=15s + + # Replace the GET method sent from port 80 with the PUT method. + kbcli fault network http replace --replace-method=PUT --duration=1m + + # Replace the GET method sent from port 80 with the PUT method, and replace the request body. + kbcli fault network http replace --body="you are good luck" --replace-method=PUT --duration=2m + + # Replace the response content "you" from port 80. + kbcli fault network http replace --target=Response --body=you --duration=30s + + # Append content to the body of the post request sent from port 4399, in JSON format. + kbcli fault network http patch --method=POST --port=4399 --body="you are good luck" --type=JSON --duration=30s +``` + +### Options + +``` + --abort Indicates whether to inject the fault that interrupts the connection. (default true) + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --code int32 The status code responded by target. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for abort + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method string The HTTP method of the target request method. For example: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH. (default "GET") + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The URI path of the target request. Supports Matching wildcards. (default "*") + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --port int32 The TCP port that the target service listens on. (default 80) + --target string Specifies whether the target of fault injection is Request or Response. The target-related fields should be configured at the same time. (default "Request") + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network http](kbcli_fault_network_http.md) - Intercept HTTP requests and responses. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_http_delay.md b/docs/user_docs/cli/kbcli_fault_network_http_delay.md new file mode 100644 index 000000000..7279a1d35 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_http_delay.md @@ -0,0 +1,91 @@ +--- +title: kbcli fault network http delay +--- + +Delay the HTTP request and response. + +``` +kbcli fault network http delay [flags] +``` + +### Examples + +``` + # By default, the method of GET from port 80 is blocked. + kbcli fault network http abort --duration=1m + + # Block the method of GET from port 4399. + kbcli fault network http abort --port=4399 --duration=1m + + # Block the method of POST from port 4399. + kbcli fault network http abort --port=4399 --method=POST --duration=1m + + # Delays post requests from port 4399. + kbcli fault network http delay --port=4399 --method=POST --delay=15s + + # Replace the GET method sent from port 80 with the PUT method. + kbcli fault network http replace --replace-method=PUT --duration=1m + + # Replace the GET method sent from port 80 with the PUT method, and replace the request body. + kbcli fault network http replace --body="you are good luck" --replace-method=PUT --duration=2m + + # Replace the response content "you" from port 80. + kbcli fault network http replace --target=Response --body=you --duration=30s + + # Append content to the body of the post request sent from port 4399, in JSON format. + kbcli fault network http patch --method=POST --port=4399 --body="you are good luck" --type=JSON --duration=30s +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --code int32 The status code responded by target. + --delay string The time for delay. (default "10s") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for delay + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method string The HTTP method of the target request method. For example: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH. (default "GET") + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The URI path of the target request. Supports Matching wildcards. (default "*") + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --port int32 The TCP port that the target service listens on. (default 80) + --target string Specifies whether the target of fault injection is Request or Response. The target-related fields should be configured at the same time. (default "Request") + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network http](kbcli_fault_network_http.md) - Intercept HTTP requests and responses. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_http_patch.md b/docs/user_docs/cli/kbcli_fault_network_http_patch.md new file mode 100644 index 000000000..755a7ef92 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_http_patch.md @@ -0,0 +1,92 @@ +--- +title: kbcli fault network http patch +--- + +Patch the HTTP request and response. + +``` +kbcli fault network http patch [flags] +``` + +### Examples + +``` + # By default, the method of GET from port 80 is blocked. + kbcli fault network http abort --duration=1m + + # Block the method of GET from port 4399. + kbcli fault network http abort --port=4399 --duration=1m + + # Block the method of POST from port 4399. + kbcli fault network http abort --port=4399 --method=POST --duration=1m + + # Delays post requests from port 4399. + kbcli fault network http delay --port=4399 --method=POST --delay=15s + + # Replace the GET method sent from port 80 with the PUT method. + kbcli fault network http replace --replace-method=PUT --duration=1m + + # Replace the GET method sent from port 80 with the PUT method, and replace the request body. + kbcli fault network http replace --body="you are good luck" --replace-method=PUT --duration=2m + + # Replace the response content "you" from port 80. + kbcli fault network http replace --target=Response --body=you --duration=30s + + # Append content to the body of the post request sent from port 4399, in JSON format. + kbcli fault network http patch --method=POST --port=4399 --body="you are good luck" --type=JSON --duration=30s +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --body string The fault of the request body or response body with patch faults. + --code int32 The status code responded by target. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for patch + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method string The HTTP method of the target request method. For example: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH. (default "GET") + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The URI path of the target request. Supports Matching wildcards. (default "*") + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --port int32 The TCP port that the target service listens on. (default 80) + --target string Specifies whether the target of fault injection is Request or Response. The target-related fields should be configured at the same time. (default "Request") + --type string The type of patch faults of the request body or response body. Currently, it only supports JSON. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network http](kbcli_fault_network_http.md) - Intercept HTTP requests and responses. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_http_replace.md b/docs/user_docs/cli/kbcli_fault_network_http_replace.md new file mode 100644 index 000000000..9a4c03d82 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_http_replace.md @@ -0,0 +1,93 @@ +--- +title: kbcli fault network http replace +--- + +Replace the HTTP request and response. + +``` +kbcli fault network http replace [flags] +``` + +### Examples + +``` + # By default, the method of GET from port 80 is blocked. + kbcli fault network http abort --duration=1m + + # Block the method of GET from port 4399. + kbcli fault network http abort --port=4399 --duration=1m + + # Block the method of POST from port 4399. + kbcli fault network http abort --port=4399 --method=POST --duration=1m + + # Delays post requests from port 4399. + kbcli fault network http delay --port=4399 --method=POST --delay=15s + + # Replace the GET method sent from port 80 with the PUT method. + kbcli fault network http replace --replace-method=PUT --duration=1m + + # Replace the GET method sent from port 80 with the PUT method, and replace the request body. + kbcli fault network http replace --body="you are good luck" --replace-method=PUT --duration=2m + + # Replace the response content "you" from port 80. + kbcli fault network http replace --target=Response --body=you --duration=30s + + # Append content to the body of the post request sent from port 4399, in JSON format. + kbcli fault network http patch --method=POST --port=4399 --body="you are good luck" --type=JSON --duration=30s +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --body string The content of the request body or response body to replace the failure. + --code int32 The status code responded by target. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for replace + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --method string The HTTP method of the target request method. For example: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH. (default "GET") + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --path string The URI path of the target request. Supports Matching wildcards. (default "*") + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --port int32 The TCP port that the target service listens on. (default 80) + --replace-method string The replaced content of the HTTP request method. + --replace-path string The URI path used to replace content. + --target string Specifies whether the target of fault injection is Request or Response. The target-related fields should be configured at the same time. (default "Request") + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network http](kbcli_fault_network_http.md) - Intercept HTTP requests and responses. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_loss.md b/docs/user_docs/cli/kbcli_fault_network_loss.md new file mode 100644 index 000000000..579e667c5 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_loss.md @@ -0,0 +1,104 @@ +--- +title: kbcli fault network loss +--- + +Cause pods to communicate with other objects to drop packets. + +``` +kbcli fault network loss [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --correlation string Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100]. + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for loss + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --loss string Indicates the probability of a packet error occurring. Value range: [0, 100]. + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_network_partition.md b/docs/user_docs/cli/kbcli_fault_network_partition.md new file mode 100644 index 000000000..e7b45d1c0 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_network_partition.md @@ -0,0 +1,102 @@ +--- +title: kbcli fault network partition +--- + +Make a pod network partitioned from other objects. + +``` +kbcli fault network partition [flags] +``` + +### Examples + +``` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --direction string You can select "to"" or "from"" or "both"". (default "to") + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -e, --external-target stringArray a network target outside of Kubernetes, which can be an IPv4 address or a domain name, + such as "www.baidu.com". Only works with direction: to. + -h, --help help for partition + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --target-label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"' (default []) + --target-mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. + --target-ns-fault stringArray Specifies the namespace into which you want to inject faults. + --target-value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault network](kbcli_fault_network.md) - Network chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_node.md b/docs/user_docs/cli/kbcli_fault_node.md new file mode 100644 index 000000000..e89be737b --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_node.md @@ -0,0 +1,45 @@ +--- +title: kbcli fault node +--- + +Node chaos. + +### Options + +``` + -h, --help help for node +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. +* [kbcli fault node detach-volume](kbcli_fault_node_detach-volume.md) - Detach volume +* [kbcli fault node restart](kbcli_fault_node_restart.md) - Restart instance +* [kbcli fault node stop](kbcli_fault_node_stop.md) - Stop instance + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_node_detach-volume.md b/docs/user_docs/cli/kbcli_fault_node_detach-volume.md new file mode 100644 index 000000000..4867d506c --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_node_detach-volume.md @@ -0,0 +1,81 @@ +--- +title: kbcli fault node detach-volume +--- + +Detach volume + +``` +kbcli fault node detach-volume [flags] +``` + +### Examples + +``` + # Stop a specified EC2 instance. + kbcli fault node stop node1 -c=aws --region=cn-northwest-1 --duration=3m + + # Stop two specified EC2 instances. + kbcli fault node stop node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Restart two specified EC2 instances. + kbcli fault node restart node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Detach two specified volume from two specified EC2 instances. + kbcli fault node detach-volume node1 node2 -c=aws --region=cn-northwest-1 --duration=1m --volume-id=v1,v2 --device-name=/d1,/d2 + + # Stop two specified GCK instances. + kbcli fault node stop node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Restart two specified GCK instances. + kbcli fault node restart node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Detach two specified volume from two specified GCK instances. + kbcli fault node detach-volume node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering --device-name=/d1,/d2 +``` + +### Options + +``` + --auto-approve Skip interactive approval before create secret. + -c, --cloud-provider string Cloud provider type, one of [aws gcp] + --device-name strings The device name of the volume. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "30s") + -h, --help help for detach-volume + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --project string The name of the GCP project. Only available when cloud-provider=gcp. + --region string The region of the node. + --secret string The name of the secret containing cloud provider specific credentials. + --volume-id strings The volume ids of the ec2. Only available when cloud-provider=aws. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault node](kbcli_fault_node.md) - Node chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_node_restart.md b/docs/user_docs/cli/kbcli_fault_node_restart.md new file mode 100644 index 000000000..66a655bb4 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_node_restart.md @@ -0,0 +1,79 @@ +--- +title: kbcli fault node restart +--- + +Restart instance + +``` +kbcli fault node restart [flags] +``` + +### Examples + +``` + # Stop a specified EC2 instance. + kbcli fault node stop node1 -c=aws --region=cn-northwest-1 --duration=3m + + # Stop two specified EC2 instances. + kbcli fault node stop node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Restart two specified EC2 instances. + kbcli fault node restart node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Detach two specified volume from two specified EC2 instances. + kbcli fault node detach-volume node1 node2 -c=aws --region=cn-northwest-1 --duration=1m --volume-id=v1,v2 --device-name=/d1,/d2 + + # Stop two specified GCK instances. + kbcli fault node stop node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Restart two specified GCK instances. + kbcli fault node restart node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Detach two specified volume from two specified GCK instances. + kbcli fault node detach-volume node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering --device-name=/d1,/d2 +``` + +### Options + +``` + --auto-approve Skip interactive approval before create secret. + -c, --cloud-provider string Cloud provider type, one of [aws gcp] + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "30s") + -h, --help help for restart + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --project string The name of the GCP project. Only available when cloud-provider=gcp. + --region string The region of the node. + --secret string The name of the secret containing cloud provider specific credentials. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault node](kbcli_fault_node.md) - Node chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_node_stop.md b/docs/user_docs/cli/kbcli_fault_node_stop.md new file mode 100644 index 000000000..36aa91977 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_node_stop.md @@ -0,0 +1,79 @@ +--- +title: kbcli fault node stop +--- + +Stop instance + +``` +kbcli fault node stop [flags] +``` + +### Examples + +``` + # Stop a specified EC2 instance. + kbcli fault node stop node1 -c=aws --region=cn-northwest-1 --duration=3m + + # Stop two specified EC2 instances. + kbcli fault node stop node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Restart two specified EC2 instances. + kbcli fault node restart node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Detach two specified volume from two specified EC2 instances. + kbcli fault node detach-volume node1 node2 -c=aws --region=cn-northwest-1 --duration=1m --volume-id=v1,v2 --device-name=/d1,/d2 + + # Stop two specified GCK instances. + kbcli fault node stop node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Restart two specified GCK instances. + kbcli fault node restart node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Detach two specified volume from two specified GCK instances. + kbcli fault node detach-volume node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering --device-name=/d1,/d2 +``` + +### Options + +``` + --auto-approve Skip interactive approval before create secret. + -c, --cloud-provider string Cloud provider type, one of [aws gcp] + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "30s") + -h, --help help for stop + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --project string The name of the GCP project. Only available when cloud-provider=gcp. + --region string The region of the node. + --secret string The name of the secret containing cloud provider specific credentials. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault node](kbcli_fault_node.md) - Node chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_pod.md b/docs/user_docs/cli/kbcli_fault_pod.md new file mode 100644 index 000000000..144789ad5 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_pod.md @@ -0,0 +1,45 @@ +--- +title: kbcli fault pod +--- + +Pod chaos. + +### Options + +``` + -h, --help help for pod +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. +* [kbcli fault pod failure](kbcli_fault_pod_failure.md) - failure pod +* [kbcli fault pod kill](kbcli_fault_pod_kill.md) - kill pod +* [kbcli fault pod kill-container](kbcli_fault_pod_kill-container.md) - kill containers + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_pod_failure.md b/docs/user_docs/cli/kbcli_fault_pod_failure.md new file mode 100644 index 000000000..9122508b7 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_pod_failure.md @@ -0,0 +1,94 @@ +--- +title: kbcli fault pod failure +--- + +failure pod + +``` +kbcli fault pod failure [flags] +``` + +### Examples + +``` + # kill all pods in default namespace + kbcli fault pod kill + + # kill any pod in default namespace + kbcli fault pod kill --mode=one + + # kill two pods in default namespace + kbcli fault pod kill --mode=fixed --value=2 + + # kill 50% pods in default namespace + kbcli fault pod kill --mode=percentage --value=50 + + # kill mysql-cluster-mysql-0 pod in default namespace + kbcli fault pod kill mysql-cluster-mysql-0 + + # kill all pods in default namespace + kbcli fault pod kill --ns-fault="default" + + # --label is required to specify the pods that need to be killed. + kbcli fault pod kill --label statefulset.kubernetes.io/pod-name=mysql-cluster-mysql-2 + + # kill pod under the specified node. + kbcli fault pod kill --node=minikube-m02 + + # kill pod under the specified node-label. + kbcli fault pod kill --node-label=kubernetes.io/arch=arm64 + + # Allow the experiment to last for one minute. + kbcli fault pod failure --duration=1m + + # kill container in pod + kbcli fault pod kill-container mysql-cluster-mysql-0 --container=mysql +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for failure + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault pod](kbcli_fault_pod.md) - Pod chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_pod_kill-container.md b/docs/user_docs/cli/kbcli_fault_pod_kill-container.md new file mode 100644 index 000000000..2101215a9 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_pod_kill-container.md @@ -0,0 +1,95 @@ +--- +title: kbcli fault pod kill-container +--- + +kill containers + +``` +kbcli fault pod kill-container [flags] +``` + +### Examples + +``` + # kill all pods in default namespace + kbcli fault pod kill + + # kill any pod in default namespace + kbcli fault pod kill --mode=one + + # kill two pods in default namespace + kbcli fault pod kill --mode=fixed --value=2 + + # kill 50% pods in default namespace + kbcli fault pod kill --mode=percentage --value=50 + + # kill mysql-cluster-mysql-0 pod in default namespace + kbcli fault pod kill mysql-cluster-mysql-0 + + # kill all pods in default namespace + kbcli fault pod kill --ns-fault="default" + + # --label is required to specify the pods that need to be killed. + kbcli fault pod kill --label statefulset.kubernetes.io/pod-name=mysql-cluster-mysql-2 + + # kill pod under the specified node. + kbcli fault pod kill --node=minikube-m02 + + # kill pod under the specified node-label. + kbcli fault pod kill --node-label=kubernetes.io/arch=arm64 + + # Allow the experiment to last for one minute. + kbcli fault pod failure --duration=1m + + # kill container in pod + kbcli fault pod kill-container mysql-cluster-mysql-0 --container=mysql +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --container stringArray the name of the container you want to kill, such as mysql, prometheus. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for kill-container + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault pod](kbcli_fault_pod.md) - Pod chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_pod_kill.md b/docs/user_docs/cli/kbcli_fault_pod_kill.md new file mode 100644 index 000000000..b6ad0e018 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_pod_kill.md @@ -0,0 +1,95 @@ +--- +title: kbcli fault pod kill +--- + +kill pod + +``` +kbcli fault pod kill [flags] +``` + +### Examples + +``` + # kill all pods in default namespace + kbcli fault pod kill + + # kill any pod in default namespace + kbcli fault pod kill --mode=one + + # kill two pods in default namespace + kbcli fault pod kill --mode=fixed --value=2 + + # kill 50% pods in default namespace + kbcli fault pod kill --mode=percentage --value=50 + + # kill mysql-cluster-mysql-0 pod in default namespace + kbcli fault pod kill mysql-cluster-mysql-0 + + # kill all pods in default namespace + kbcli fault pod kill --ns-fault="default" + + # --label is required to specify the pods that need to be killed. + kbcli fault pod kill --label statefulset.kubernetes.io/pod-name=mysql-cluster-mysql-2 + + # kill pod under the specified node. + kbcli fault pod kill --node=minikube-m02 + + # kill pod under the specified node-label. + kbcli fault pod kill --node-label=kubernetes.io/arch=arm64 + + # Allow the experiment to last for one minute. + kbcli fault pod failure --duration=1m + + # kill container in pod + kbcli fault pod kill-container mysql-cluster-mysql-0 --container=mysql +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -g, --grace-period int Grace period represents the duration in seconds before the pod should be killed + -h, --help help for kill + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault pod](kbcli_fault_pod.md) - Pod chaos. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_stress.md b/docs/user_docs/cli/kbcli_fault_stress.md new file mode 100644 index 000000000..1a2170d84 --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_stress.md @@ -0,0 +1,75 @@ +--- +title: kbcli fault stress +--- + +Add memory pressure or CPU load to the system. + +``` +kbcli fault stress [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods.Making CPU load up to 50%, and the memory up to 100MB. + kbcli fault stress --cpu-worker=2 --cpu-load=50 --memory-worker=1 --memory-size=100Mi + + # Affects the first container in mycluster-mysql-0 pod. Making the CPU load up to 50%, and the memory up to 500MB. + kbcli fault stress mycluster-mysql-0 --cpu-worker=2 --cpu-load=50 + + # Affects the mysql container in mycluster-mysql-0 pod. Making the memory up to 500MB. + kbcli fault stress mycluster-mysql-0 --memory-worker=2 --memory-size=500Mi -c=mysql +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + -c, --container stringArray The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected. + --cpu-load int Specifies the percentage of CPU occupied. 0 means no extra load added, 100 means full load. The total load is workers * load. + --cpu-worker int Specifies the number of threads that exert CPU pressure. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for stress + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --memory-size string Specify the size of the allocated memory or the percentage of the total memory, and the sum of the allocated memory is size. For example:256MB or 25% + --memory-worker int Specifies the number of threads that apply memory pressure. + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_fault_time.md b/docs/user_docs/cli/kbcli_fault_time.md new file mode 100644 index 000000000..86d15cdfd --- /dev/null +++ b/docs/user_docs/cli/kbcli_fault_time.md @@ -0,0 +1,79 @@ +--- +title: kbcli fault time +--- + +Clock skew failure. + +``` +kbcli fault time [flags] +``` + +### Examples + +``` + # Affects the first container in default namespace's all pods.Shifts the clock back five seconds. + kbcli fault time --time-offset=-5s + + # Affects the first container in default namespace's all pods. + kbcli fault time --time-offset=-5m5s + + # Affects the first container in mycluster-mysql-0 pod. Shifts the clock forward five seconds. + kbcli fault time mycluster-mysql-0 --time-offset=+5s50ms + + # Affects the mysql container in mycluster-mysql-0 pod. Shifts the clock forward five seconds. + kbcli fault time mycluster-mysql-0 --time-offset=+5s -c=mysql + + # The clock that specifies the effect of time offset is CLOCK_REALTIME. + kbcli fault time mycluster-mysql-0 --time-offset=+5s --clock-id=CLOCK_REALTIME -c=mysql +``` + +### Options + +``` + --annotation stringToString Select the pod to inject the fault according to Annotation. (default []) + --clock-id stringArray Specifies the clock on which the time offset acts.If it's empty, it will be set to ['CLOCK_REALTIME'].See clock_gettime [https://man7.org/linux/man-pages/man2/clock_gettime.2.html] document for details. + -c, --container stringArray Specifies the injected container name. For example: mysql. If it's empty, the first container will be injected. + --dry-run string[="unchanged"] Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent. (default "none") + --duration string Supported formats of the duration are: ms / s / m / h. (default "10s") + -h, --help help for time + --label stringToString label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0. (default []) + --mode string You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with. (default "all") + --node stringArray Inject faults into pods in the specified node. + --node-label stringToString label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux. (default []) + --ns-fault stringArray Specifies the namespace into which you want to inject faults. (default [default]) + -o, --output format prints the output in the specified format. Allowed values: JSON and YAML (default yaml) + --phase stringArray Specify the pod that injects the fault by the state of the pod. + --time-offset string Specifies the length of the time offset. For example: -5s, -10m100ns. + --value string If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject. +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli fault](kbcli_fault.md) - Inject faults to pod. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_kubeblocks.md b/docs/user_docs/cli/kbcli_kubeblocks.md index 058612ee4..43b075fbf 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks.md +++ b/docs/user_docs/cli/kbcli_kubeblocks.md @@ -37,6 +37,8 @@ KubeBlocks operation commands. ### SEE ALSO +* [kbcli kubeblocks config](kbcli_kubeblocks_config.md) - KubeBlocks config. +* [kbcli kubeblocks describe-config](kbcli_kubeblocks_describe-config.md) - describe KubeBlocks config. * [kbcli kubeblocks install](kbcli_kubeblocks_install.md) - Install KubeBlocks. * [kbcli kubeblocks list-versions](kbcli_kubeblocks_list-versions.md) - List KubeBlocks versions. * [kbcli kubeblocks preflight](kbcli_kubeblocks_preflight.md) - Run and retrieve preflight checks for KubeBlocks. diff --git a/docs/user_docs/cli/kbcli_backup-config.md b/docs/user_docs/cli/kbcli_kubeblocks_config.md similarity index 67% rename from docs/user_docs/cli/kbcli_backup-config.md rename to docs/user_docs/cli/kbcli_kubeblocks_config.md index c9d673175..4c003f336 100644 --- a/docs/user_docs/cli/kbcli_backup-config.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_config.md @@ -1,33 +1,51 @@ --- -title: kbcli backup-config +title: kbcli kubeblocks config --- -KubeBlocks backup config. +KubeBlocks config. ``` -kbcli backup-config [flags] +kbcli kubeblocks config [flags] ``` ### Examples ``` # Enable the snapshot-controller and volume snapshot, to support snapshot backup. - kbcli backup-config --set snapshot-controller.enabled=true + kbcli kubeblocks config --set snapshot-controller.enabled=true + Options Parameters: # If you have already installed a snapshot-controller, only enable the snapshot backup feature - kbcli backup-config --set dataProtection.enableVolumeSnapshot=true + dataProtection.enableVolumeSnapshot=true - # Schedule automatic backup at 18:00 every day (UTC timezone) - kbcli backup-config --set dataProtection.backupSchedule="0 18 * * *" + # the global pvc name which persistent volume claim to store the backup data. + # replace the pvc name when it is empty in the backup policy. + dataProtection.backupPVCName=backup-data - # Set automatic backup retention for 7 days - kbcli backup-config --set dataProtection.backupTTL="168h0m0s" + # the init capacity of pvc for creating the pvc, e.g. 10Gi. + # replace the init capacity when it is empty in the backup policy. + dataProtection.backupPVCInitCapacity=100Gi + + # the pvc storage class name. replace the storageClassName when it is unset in the backup policy. + dataProtection.backupPVCStorageClassName=csi-s3 + + # the pvc creation policy. + # if the storageClass supports dynamic provisioning, recommend "IfNotPresent" policy. + # otherwise, using "Never" policy. only affect the backupPolicy automatically created by KubeBlocks. + dataProtection.backupPVCCreatePolicy=Never + + # the configmap name of the pv template. if the csi-driver does not support dynamic provisioning, + # you can provide a configmap which contains key "persistentVolume" and value of the persistentVolume struct. + dataProtection.backupPVConfigMapName=pv-template + + # the configmap namespace of the pv template. + dataProtection.backupPVConfigMapNamespace=default ``` ### Options ``` - -h, --help help for backup-config + -h, --help help for config --set stringArray Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) --set-file stringArray Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-json stringArray Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) @@ -61,7 +79,7 @@ kbcli backup-config [flags] ### SEE ALSO - +* [kbcli kubeblocks](kbcli_kubeblocks.md) - KubeBlocks operation commands. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_kubeblocks_describe-config.md b/docs/user_docs/cli/kbcli_kubeblocks_describe-config.md new file mode 100644 index 000000000..7a5dfcb58 --- /dev/null +++ b/docs/user_docs/cli/kbcli_kubeblocks_describe-config.md @@ -0,0 +1,55 @@ +--- +title: kbcli kubeblocks describe-config +--- + +describe KubeBlocks config. + +``` +kbcli kubeblocks describe-config [flags] +``` + +### Examples + +``` + # Describe the KubeBlocks config. + kbcli kubeblocks describe-config +``` + +### Options + +``` + -A, --all show all kubeblocks configs value + -h, --help help for describe-config + -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli kubeblocks](kbcli_kubeblocks.md) - KubeBlocks operation commands. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_kubeblocks_install.md b/docs/user_docs/cli/kbcli_kubeblocks_install.md index 5e65b928d..56192928e 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks_install.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_install.md @@ -27,17 +27,23 @@ kbcli kubeblocks install [flags] ### Options ``` - --check Check kubernetes environment before install (default true) - --create-namespace Create the namespace if not present - -h, --help help for install - --monitor Auto install monitoring add-ons including prometheus, grafana and alertmanager-webhook-adaptor (default true) - --set stringArray Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --set-file stringArray Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - --set-json stringArray Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) - --set-string stringArray Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --timeout duration Time to wait for installing KubeBlocks (default 30m0s) - -f, --values strings Specify values in a YAML file or a URL (can specify multiple) - --version string KubeBlocks version + --check Check kubernetes environment before installation (default true) + --create-namespace Create the namespace if not present + --force If present, just print fail item and continue with the following steps + -h, --help help for install + --monitor Auto install monitoring add-ons including prometheus, grafana and alertmanager-webhook-adaptor (default true) + --node-labels stringToString Node label selector (default []) + --pod-anti-affinity string Pod anti-affinity type, one of: (Preferred, Required) + --set stringArray Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --set-file stringArray Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) + --set-json stringArray Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) + --set-string stringArray Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) + --timeout duration Time to wait for installing KubeBlocks, such as --timeout=10m (default 5m0s) + --tolerations strings Tolerations for Kubeblocks, such as '"dev=true:NoSchedule,large=true:NoSchedule"' + --topology-keys stringArray Topology keys for affinity + -f, --values strings Specify values in a YAML file or a URL (can specify multiple) + --version string KubeBlocks version + --wait Wait for KubeBlocks to be ready, including all the auto installed add-ons. It will wait for a --timeout period (default true) ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_kubeblocks_list-versions.md b/docs/user_docs/cli/kbcli_kubeblocks_list-versions.md index 3d00f8c74..875259da7 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks_list-versions.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_list-versions.md @@ -11,7 +11,7 @@ kbcli kubeblocks list-versions [flags] ### Examples ``` - # list KubeBlocks release version + # list KubeBlocks release versions kbcli kubeblocks list-versions # list KubeBlocks versions including development versions, such as alpha, beta and release candidate diff --git a/docs/user_docs/cli/kbcli_kubeblocks_preflight.md b/docs/user_docs/cli/kbcli_kubeblocks_preflight.md index a79271ab8..93abf5bce 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks_preflight.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_preflight.md @@ -27,13 +27,12 @@ kbcli kubeblocks preflight [flags] ### Options ``` - --collect-without-permissions always run preflight checks even if some require permissions that preflight does not have (default true) + --collect-without-permissions always run preflight checks even if some required permissions that preflight does not have (default true) --collector-image string the full name of the collector image to use --collector-pullpolicy string the pull policy of the collector image --debug enable debug logging - --format string output format, one of human, json, yaml. only used when interactive is set to false, default format is yaml (default "yaml") + --format string output format, one of json, yaml. only used when interactive is set to false, default format is yaml (default "yaml") -h, --help help for preflight - --interactive interactive preflights, default value is false -n, --namespace string If present, the namespace scope for this CLI request -o, --output string specify the output file path for the preflight checks --selector string selector (label query) to filter remote collection nodes on. diff --git a/docs/user_docs/cli/kbcli_kubeblocks_uninstall.md b/docs/user_docs/cli/kbcli_kubeblocks_uninstall.md index 745b7e669..737c267b7 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks_uninstall.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_uninstall.md @@ -23,6 +23,8 @@ kbcli kubeblocks uninstall [flags] --remove-namespace Remove default created "kb-system" namespace or not --remove-pvcs Remove PersistentVolumeClaim or not --remove-pvs Remove PersistentVolume or not + --timeout duration Time to wait for uninstalling KubeBlocks, such as --timeout=5m (default 5m0s) + --wait Wait for KubeBlocks to be uninstalled, including all the add-ons. It will wait for a --timeout period (default true) ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_kubeblocks_upgrade.md b/docs/user_docs/cli/kbcli_kubeblocks_upgrade.md index f65f7adcf..c338c2b0f 100644 --- a/docs/user_docs/cli/kbcli_kubeblocks_upgrade.md +++ b/docs/user_docs/cli/kbcli_kubeblocks_upgrade.md @@ -27,9 +27,10 @@ kbcli kubeblocks upgrade [flags] --set-file stringArray Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) --set-json stringArray Set JSON values on the command line (can specify multiple or separate values with commas: key1=jsonval1,key2=jsonval2) --set-string stringArray Set STRING values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - --timeout duration Time to wait for upgrading KubeBlocks (default 30m0s) + --timeout duration Time to wait for upgrading KubeBlocks, such as --timeout=10m (default 5m0s) -f, --values strings Specify values in a YAML file or a URL (can specify multiple) --version string Set KubeBlocks version + --wait Wait for KubeBlocks to be ready. It will wait for a --timeout period (default true) ``` ### Options inherited from parent commands diff --git a/docs/user_docs/cli/kbcli_migration.md b/docs/user_docs/cli/kbcli_migration.md new file mode 100644 index 000000000..3cf6987d9 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration.md @@ -0,0 +1,48 @@ +--- +title: kbcli migration +--- + +Data migration between two data sources. + +### Options + +``` + -h, --help help for migration +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + + +* [kbcli migration create](kbcli_migration_create.md) - Create a migration task. +* [kbcli migration describe](kbcli_migration_describe.md) - Show details of a specific migration task. +* [kbcli migration list](kbcli_migration_list.md) - List migration tasks. +* [kbcli migration logs](kbcli_migration_logs.md) - Access migration task log file. +* [kbcli migration templates](kbcli_migration_templates.md) - List migration templates. +* [kbcli migration terminate](kbcli_migration_terminate.md) - Delete migration task. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_migration_create.md b/docs/user_docs/cli/kbcli_migration_create.md new file mode 100644 index 000000000..e06fc2197 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration_create.md @@ -0,0 +1,90 @@ +--- +title: kbcli migration create +--- + +Create a migration task. + +``` +kbcli migration create NAME [flags] +``` + +### Examples + +``` + # Create a migration task to migrate the entire database under mysql: mydb1 and mytable1 under database: mydb2 to the target mysql + kbcli migration create mytask --template apecloud-mysql2mysql + --source user:123456@127.0.0.1:3306 + --sink user:123456@127.0.0.1:3305 + --migration-object '"mydb1","mydb2.mytable1"' + + # Create a migration task to migrate the schema: myschema under database: mydb1 under PostgreSQL to the target PostgreSQL + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + + # Use prechecks, data initialization, CDC, but do not perform structure initialization + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --steps precheck=true,init-struct=false,init-data=true,cdc=true + + # Create a migration task with two tolerations + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --tolerations '"step=global,key=engineType,value=pg,operator=Equal,effect=NoSchedule","step=init-data,key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + + # Limit resource usage when performing data initialization + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --resources '"step=init-data,cpu=1000m,memory=1Gi"' +``` + +### Options + +``` + -h, --help help for create + --migration-object strings Set the data objects that need to be migrated,such as '"db1.table1","db2"' + --resources strings Resources limit for migration, such as '"cpu=3000m,memory=3Gi"' + --sink string Set the sink database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}] + --source string Set the source database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}]' + --steps strings Set up migration steps,such as: precheck=true,init-struct=true,init-data=true,cdc=true + --template string Specify migration template, run "kbcli migration templates" to show all available migration templates + --tolerations strings Tolerations for migration, such as '"key=engineType,value=pg,operator=Equal,effect=NoSchedule"' +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_migration_describe.md b/docs/user_docs/cli/kbcli_migration_describe.md new file mode 100644 index 000000000..73432c7c0 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration_describe.md @@ -0,0 +1,53 @@ +--- +title: kbcli migration describe +--- + +Show details of a specific migration task. + +``` +kbcli migration describe NAME [flags] +``` + +### Examples + +``` + # describe a specified migration task + kbcli migration describe mytask +``` + +### Options + +``` + -h, --help help for describe +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_migration_list.md b/docs/user_docs/cli/kbcli_migration_list.md new file mode 100644 index 000000000..62347f476 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration_list.md @@ -0,0 +1,69 @@ +--- +title: kbcli migration list +--- + +List migration tasks. + +``` +kbcli migration list [NAME] [flags] +``` + +### Examples + +``` + # list all migration tasks + kbcli migration list + + # list a single migration task with specified NAME + kbcli migration list mytask + + # list a single migration task in YAML output format + kbcli migration list mytask -o yaml + + # list a single migration task in JSON output format + kbcli migration list mytask -o json + + # list a single migration task in wide output format + kbcli migration list mytask -o wide +``` + +### Options + +``` + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -h, --help help for list + -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) + -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. + --show-labels When printing, show all labels as the last column (default hide labels column) +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_migration_logs.md b/docs/user_docs/cli/kbcli_migration_logs.md new file mode 100644 index 000000000..2048907c9 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration_logs.md @@ -0,0 +1,72 @@ +--- +title: kbcli migration logs +--- + +Access migration task log file. + +``` +kbcli migration logs NAME [flags] +``` + +### Examples + +``` + # Logs when returning to the "init-struct" step from the migration task mytask + kbcli migration logs mytask --step init-struct + + # Logs only the most recent 20 lines when returning to the "cdc" step from the migration task mytask + kbcli migration logs mytask --step cdc --tail=20 +``` + +### Options + +``` + --all-containers Get all containers' logs in the pod(s). + -c, --container string Print the logs of this container + -f, --follow Specify if the logs should be streamed. + -h, --help help for logs + --ignore-errors If watching / following pod logs, allow for any errors that occur to be non-fatal + --insecure-skip-tls-verify-backend Skip verifying the identity of the kubelet that logs are requested from. In theory, an attacker could provide invalid log content back. You might want to use this if your kubelet serving certificates have expired. + --limit-bytes int Maximum bytes of logs to return. Defaults to no limit. + --max-log-requests int Specify maximum number of concurrent logs to follow when using by a selector. Defaults to 5. + --pod-running-timeout duration The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running (default 20s) + --prefix Prefix each log line with the log source (pod name and container name) + -p, --previous If true, print the logs for the previous instance of the container in a pod if it exists. + -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. + --since duration Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used. + --since-time string Only return logs after a specific date (RFC3339). Defaults to all logs. Only one of since-time / since may be used. + --step string Specify the step. Allow values: precheck,init-struct,init-data,cdc + --tail int Lines of recent log file to display. Defaults to -1 with no selector, showing all log lines otherwise 10, if a selector is provided. (default -1) + --timestamps Include timestamps on each line in the log output +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_migration_templates.md b/docs/user_docs/cli/kbcli_migration_templates.md new file mode 100644 index 000000000..cd25b2ee5 --- /dev/null +++ b/docs/user_docs/cli/kbcli_migration_templates.md @@ -0,0 +1,69 @@ +--- +title: kbcli migration templates +--- + +List migration templates. + +``` +kbcli migration templates [NAME] [flags] +``` + +### Examples + +``` + # list all migration templates + kbcli migration templates + + # list a single migration template with specified NAME + kbcli migration templates mytemplate + + # list a single migration template in YAML output format + kbcli migration templates mytemplate -o yaml + + # list a single migration template in JSON output format + kbcli migration templates mytemplate -o json + + # list a single migration template in wide output format + kbcli migration templates mytemplate -o wide +``` + +### Options + +``` + -A, --all-namespaces If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace. + -h, --help help for templates + -o, --output format prints the output in the specified format. Allowed values: table, json, yaml, wide (default table) + -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. + --show-labels When printing, show all labels as the last column (default hide labels column) +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_cluster_delete-restore.md b/docs/user_docs/cli/kbcli_migration_terminate.md similarity index 89% rename from docs/user_docs/cli/kbcli_cluster_delete-restore.md rename to docs/user_docs/cli/kbcli_migration_terminate.md index ca3c38294..bcec9fa01 100644 --- a/docs/user_docs/cli/kbcli_cluster_delete-restore.md +++ b/docs/user_docs/cli/kbcli_migration_terminate.md @@ -1,18 +1,18 @@ --- -title: kbcli cluster delete-restore +title: kbcli migration terminate --- -Delete a restore job. +Delete migration task. ``` -kbcli cluster delete-restore [flags] +kbcli migration terminate NAME [flags] ``` ### Examples ``` - # delete a restore named restore-name - kbcli cluster delete-restore cluster-name --name restore-name + # terminate a migration task named mytask and delete resources in k8s without affecting source and target data in database + kbcli migration terminate mytask ``` ### Options @@ -22,8 +22,7 @@ kbcli cluster delete-restore [flags] --auto-approve Skip interactive approval before deleting --force If true, immediately remove resources from API and bypass graceful deletion. Note that immediate deletion of some resources may result in inconsistency or data loss and requires confirmation. --grace-period int Period of time in seconds given to the resource to terminate gracefully. Ignored if negative. Set to 1 for immediate shutdown. Can only be set to 0 when --force is true (force deletion). (default -1) - -h, --help help for delete-restore - --name strings Restore names + -h, --help help for terminate --now If true, resources are signaled for immediate shutdown (same as --grace-period=1). -l, --selector string Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints. ``` @@ -54,7 +53,7 @@ kbcli cluster delete-restore [flags] ### SEE ALSO -* [kbcli cluster](kbcli_cluster.md) - Cluster command. +* [kbcli migration](kbcli_migration.md) - Data migration between two data sources. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_playground.md b/docs/user_docs/cli/kbcli_playground.md index 3d4823619..c6414a799 100644 --- a/docs/user_docs/cli/kbcli_playground.md +++ b/docs/user_docs/cli/kbcli_playground.md @@ -2,7 +2,7 @@ title: kbcli playground --- -Bootstrap a playground KubeBlocks in local host or cloud. +Bootstrap or destroy a playground KubeBlocks in local host or cloud. ### Options @@ -37,8 +37,7 @@ Bootstrap a playground KubeBlocks in local host or cloud. ### SEE ALSO -* [kbcli playground destroy](kbcli_playground_destroy.md) - Destroy the playground kubernetes cluster. -* [kbcli playground guide](kbcli_playground_guide.md) - Display playground cluster user guide. +* [kbcli playground destroy](kbcli_playground_destroy.md) - Destroy the playground KubeBlocks and kubernetes cluster. * [kbcli playground init](kbcli_playground_init.md) - Bootstrap a kubernetes cluster and install KubeBlocks for playground. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_playground_destroy.md b/docs/user_docs/cli/kbcli_playground_destroy.md index 923e4dbdc..72631e064 100644 --- a/docs/user_docs/cli/kbcli_playground_destroy.md +++ b/docs/user_docs/cli/kbcli_playground_destroy.md @@ -2,7 +2,7 @@ title: kbcli playground destroy --- -Destroy the playground kubernetes cluster. +Destroy the playground KubeBlocks and kubernetes cluster. ``` kbcli playground destroy [flags] @@ -18,7 +18,10 @@ kbcli playground destroy [flags] ### Options ``` - -h, --help help for destroy + --auto-approve Skip interactive approval before destroying the playground + -h, --help help for destroy + --purge Purge all resources before destroying kubernetes cluster, delete all clusters created by KubeBlocks and uninstall KubeBlocks. (default true) + --timeout duration Time to wait for installing KubeBlocks, such as --timeout=10m (default 5m0s) ``` ### Options inherited from parent commands @@ -47,7 +50,7 @@ kbcli playground destroy [flags] ### SEE ALSO -* [kbcli playground](kbcli_playground.md) - Bootstrap a playground KubeBlocks in local host or cloud. +* [kbcli playground](kbcli_playground.md) - Bootstrap or destroy a playground KubeBlocks in local host or cloud. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_playground_init.md b/docs/user_docs/cli/kbcli_playground_init.md index 88958cbc8..cbd8664bf 100644 --- a/docs/user_docs/cli/kbcli_playground_init.md +++ b/docs/user_docs/cli/kbcli_playground_init.md @@ -4,6 +4,12 @@ title: kbcli playground init Bootstrap a kubernetes cluster and install KubeBlocks for playground. +### Synopsis + +Bootstrap a kubernetes cluster and install KubeBlocks for playground. + + If no cloud provider is specified, a k3d cluster named kb-playground will be created on local host, otherwise a kubernetes cluster will be created on the specified cloud. Then KubeBlocks will be installed on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster will be created. + ``` kbcli playground init [flags] ``` @@ -15,7 +21,7 @@ kbcli playground init [flags] kbcli playground init # create an AWS EKS cluster and install KubeBlocks, the region is required - kbcli playground init --cloud-provider aws --region cn-northwest-1 + kbcli playground init --cloud-provider aws --region us-west-1 # create an Alibaba cloud ACK cluster and install KubeBlocks, the region is required kbcli playground init --cloud-provider alicloud --region cn-hangzhou @@ -24,17 +30,35 @@ kbcli playground init [flags] kbcli playground init --cloud-provider tencentcloud --region ap-chengdu # create a Google cloud GKE cluster and install KubeBlocks, the region is required - kbcli playground init --cloud-provider gcp --region us-central1 + kbcli playground init --cloud-provider gcp --region us-east1 + + # after init, run the following commands to experience KubeBlocks quickly + # list database cluster and check its status + kbcli cluster list + + # get cluster information + kbcli cluster describe mycluster + + # connect to database + kbcli cluster connect mycluster + + # view the Grafana + kbcli dashboard open kubeblocks-grafana + + # destroy playground + kbcli playground destroy ``` ### Options ``` + --auto-approve Skip interactive approval during the initialization of playground --cloud-provider string Cloud provider type, one of [local aws gcp alicloud tencentcloud] (default "local") - --cluster-definition string Cluster definition (default "apecloud-mysql") - --cluster-version string Cluster definition + --cluster-definition string Specify the cluster definition, run "kbcli cd list" to get the available cluster definitions (default "apecloud-mysql") + --cluster-version string Specify the cluster version, run "kbcli cv list" to get the available cluster versions -h, --help help for init --region string The region to create kubernetes cluster + --timeout duration Time to wait for init playground, such as --timeout=10m (default 5m0s) --version string KubeBlocks version ``` @@ -64,7 +88,7 @@ kbcli playground init [flags] ### SEE ALSO -* [kbcli playground](kbcli_playground.md) - Bootstrap a playground KubeBlocks in local host or cloud. +* [kbcli playground](kbcli_playground.md) - Bootstrap or destroy a playground KubeBlocks in local host or cloud. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_plugin.md b/docs/user_docs/cli/kbcli_plugin.md new file mode 100644 index 000000000..9d132e7e0 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin.md @@ -0,0 +1,55 @@ +--- +title: kbcli plugin +--- + +Provides utilities for interacting with plugins. + +### Synopsis + +Provides utilities for interacting with plugins. + + Plugins provide extended functionality that is not part of the major command-line distribution. + +### Options + +``` + -h, --help help for plugin +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + + +* [kbcli plugin describe](kbcli_plugin_describe.md) - Describe a plugin +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes +* [kbcli plugin install](kbcli_plugin_install.md) - Install kbcli or kubectl plugins +* [kbcli plugin list](kbcli_plugin_list.md) - List all visible plugin executables on a user's PATH +* [kbcli plugin search](kbcli_plugin_search.md) - Search kbcli or kubectl plugins +* [kbcli plugin uninstall](kbcli_plugin_uninstall.md) - Uninstall kbcli or kubectl plugins +* [kbcli plugin upgrade](kbcli_plugin_upgrade.md) - Upgrade kbcli or kubectl plugins + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_cluster_list-users.md b/docs/user_docs/cli/kbcli_plugin_describe.md similarity index 72% rename from docs/user_docs/cli/kbcli_cluster_list-users.md rename to docs/user_docs/cli/kbcli_plugin_describe.md index f1a7239c1..8d57c2fd7 100644 --- a/docs/user_docs/cli/kbcli_cluster_list-users.md +++ b/docs/user_docs/cli/kbcli_plugin_describe.md @@ -1,33 +1,27 @@ --- -title: kbcli cluster list-users +title: kbcli plugin describe --- -List users for a cluster +Describe a plugin ``` -kbcli cluster list-users [flags] +kbcli plugin describe [flags] ``` ### Examples ``` - # list all users from specified component of a cluster - kbcli cluster list-users NAME --component-name COMPNAME --show-connected-users + # Describe a plugin + kbcli plugin describe [PLUGIN] - # list all users of a cluster, by default the first component will be used - kbcli cluster list-users NAME --show-connected-users - - # list all users from cluster's one particular instance - kbcli cluster list-users NAME -i INSTANCE + # Describe a plugin with index + kbcli plugin describe [INDEX/PLUGIN] ``` ### Options ``` - --component-name string Specify the name of component to be connected. If not specified, the first component will be used. - -h, --help help for list-users - -i, --instance string Specify the name of instance to be connected. - --verbose Print verbose information. + -h, --help help for describe ``` ### Options inherited from parent commands @@ -56,7 +50,7 @@ kbcli cluster list-users [flags] ### SEE ALSO -* [kbcli cluster](kbcli_cluster.md) - Cluster command. +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_plugin_index.md b/docs/user_docs/cli/kbcli_plugin_index.md new file mode 100644 index 000000000..3a4048110 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_index.md @@ -0,0 +1,50 @@ +--- +title: kbcli plugin index +--- + +Manage custom plugin indexes + +### Synopsis + +Manage which repositories are used to discover plugins and install plugins from + +### Options + +``` + -h, --help help for index +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. +* [kbcli plugin index add](kbcli_plugin_index_add.md) - Add a new index +* [kbcli plugin index delete](kbcli_plugin_index_delete.md) - Remove a configured index +* [kbcli plugin index list](kbcli_plugin_index_list.md) - List configured indexes +* [kbcli plugin index update](kbcli_plugin_index_update.md) - update all configured indexes + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_index_add.md b/docs/user_docs/cli/kbcli_plugin_index_add.md new file mode 100644 index 000000000..052b81627 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_index_add.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin index add +--- + +Add a new index + +``` +kbcli plugin index add [flags] +``` + +### Examples + +``` + # Add a new plugin index + kbcli plugin index add myIndex +``` + +### Options + +``` + -h, --help help for add +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_index_delete.md b/docs/user_docs/cli/kbcli_plugin_index_delete.md new file mode 100644 index 000000000..47c790555 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_index_delete.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin index delete +--- + +Remove a configured index + +``` +kbcli plugin index delete [flags] +``` + +### Examples + +``` + # Delete a plugin index + kbcli plugin index delete myIndex +``` + +### Options + +``` + -h, --help help for delete +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_index_list.md b/docs/user_docs/cli/kbcli_plugin_index_list.md new file mode 100644 index 000000000..1931e4d0a --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_index_list.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin index list +--- + +List configured indexes + +``` +kbcli plugin index list [flags] +``` + +### Examples + +``` + # List all configured plugin indexes + kbcli plugin index list +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_playground_guide.md b/docs/user_docs/cli/kbcli_plugin_index_update.md similarity index 90% rename from docs/user_docs/cli/kbcli_playground_guide.md rename to docs/user_docs/cli/kbcli_plugin_index_update.md index 2377fec1f..0ab0caafd 100644 --- a/docs/user_docs/cli/kbcli_playground_guide.md +++ b/docs/user_docs/cli/kbcli_plugin_index_update.md @@ -1,17 +1,17 @@ --- -title: kbcli playground guide +title: kbcli plugin index update --- -Display playground cluster user guide. +update all configured indexes ``` -kbcli playground guide [flags] +kbcli plugin index update [flags] ``` ### Options ``` - -h, --help help for guide + -h, --help help for update ``` ### Options inherited from parent commands @@ -40,7 +40,7 @@ kbcli playground guide [flags] ### SEE ALSO -* [kbcli playground](kbcli_playground.md) - Bootstrap a playground KubeBlocks in local host or cloud. +* [kbcli plugin index](kbcli_plugin_index.md) - Manage custom plugin indexes #### Go Back to [CLI Overview](cli.md) Homepage. diff --git a/docs/user_docs/cli/kbcli_plugin_install.md b/docs/user_docs/cli/kbcli_plugin_install.md new file mode 100644 index 000000000..54933733f --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_install.md @@ -0,0 +1,56 @@ +--- +title: kbcli plugin install +--- + +Install kbcli or kubectl plugins + +``` +kbcli plugin install [flags] +``` + +### Examples + +``` + # install a kbcli or kubectl plugin by name + kbcli plugin install [PLUGIN] + + # install a kbcli or kubectl plugin by name and index + kbcli plugin install [INDEX/PLUGIN] +``` + +### Options + +``` + -h, --help help for install +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_list.md b/docs/user_docs/cli/kbcli_plugin_list.md new file mode 100644 index 000000000..945ec1445 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_list.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin list +--- + +List all visible plugin executables on a user's PATH + +``` +kbcli plugin list +``` + +### Examples + +``` + # List all available plugins file on a user's PATH. + kbcli plugin list +``` + +### Options + +``` + -h, --help help for list +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_search.md b/docs/user_docs/cli/kbcli_plugin_search.md new file mode 100644 index 000000000..e54626591 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_search.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin search +--- + +Search kbcli or kubectl plugins + +``` +kbcli plugin search [flags] +``` + +### Examples + +``` + # search a kbcli or kubectl plugin by name + kbcli plugin search myplugin +``` + +### Options + +``` + -h, --help help for search +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_uninstall.md b/docs/user_docs/cli/kbcli_plugin_uninstall.md new file mode 100644 index 000000000..f96801fa4 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_uninstall.md @@ -0,0 +1,53 @@ +--- +title: kbcli plugin uninstall +--- + +Uninstall kbcli or kubectl plugins + +``` +kbcli plugin uninstall [flags] +``` + +### Examples + +``` + # uninstall a kbcli or kubectl plugin by name + kbcli plugin uninstall [PLUGIN] +``` + +### Options + +``` + -h, --help help for uninstall +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/cli/kbcli_plugin_upgrade.md b/docs/user_docs/cli/kbcli_plugin_upgrade.md new file mode 100644 index 000000000..3ee5e5902 --- /dev/null +++ b/docs/user_docs/cli/kbcli_plugin_upgrade.md @@ -0,0 +1,57 @@ +--- +title: kbcli plugin upgrade +--- + +Upgrade kbcli or kubectl plugins + +``` +kbcli plugin upgrade [flags] +``` + +### Examples + +``` + # upgrade installed plugins with specified name + kbcli plugin upgrade myplugin + + # upgrade installed plugin to a newer version + kbcli plugin upgrade --all +``` + +### Options + +``` + --all Upgrade all installed plugins + -h, --help help for upgrade +``` + +### Options inherited from parent commands + +``` + --as string Username to impersonate for the operation. User could be a regular user or a service account in a namespace. + --as-group stringArray Group to impersonate for the operation, this flag can be repeated to specify multiple groups. + --as-uid string UID to impersonate for the operation. + --cache-dir string Default cache directory (default "$HOME/.kube/cache") + --certificate-authority string Path to a cert file for the certificate authority + --client-certificate string Path to a client certificate file for TLS + --client-key string Path to a client key file for TLS + --cluster string The name of the kubeconfig cluster to use + --context string The name of the kubeconfig context to use + --disable-compression If true, opt-out of response compression for all requests to the server + --insecure-skip-tls-verify If true, the server's certificate will not be checked for validity. This will make your HTTPS connections insecure + --kubeconfig string Path to the kubeconfig file to use for CLI requests. + --match-server-version Require server version to match client version + -n, --namespace string If present, the namespace scope for this CLI request + --request-timeout string The length of time to wait before giving up on a single server request. Non-zero values should contain a corresponding time unit (e.g. 1s, 2m, 3h). A value of zero means don't timeout requests. (default "0") + -s, --server string The address and port of the Kubernetes API server + --tls-server-name string Server name to use for server certificate validation. If it is not provided, the hostname used to contact the server is used + --token string Bearer token for authentication to the API server + --user string The name of the kubeconfig user to use +``` + +### SEE ALSO + +* [kbcli plugin](kbcli_plugin.md) - Provides utilities for interacting with plugins. + +#### Go Back to [CLI Overview](cli.md) Homepage. + diff --git a/docs/user_docs/connect_database/_category_.yml b/docs/user_docs/connect_database/_category_.yml index 9c46cf98d..518c226cf 100644 --- a/docs/user_docs/connect_database/_category_.yml +++ b/docs/user_docs/connect_database/_category_.yml @@ -1,4 +1,4 @@ -position: 8 +position: 10 label: Connect Database collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/connect_database/connect-database-in-production-environment.md b/docs/user_docs/connect_database/connect-database-in-production-environment.md index fa3be161d..ecf346b01 100644 --- a/docs/user_docs/connect_database/connect-database-in-production-environment.md +++ b/docs/user_docs/connect_database/connect-database-in-production-environment.md @@ -1,6 +1,7 @@ --- title: Connect database in production environment description: How to connect to a database in production environment +keywords: [connect to a database, production environment] sidebar_position: 3 sidebar_label: Production environment --- @@ -8,16 +9,18 @@ sidebar_label: Production environment # Connect database in production environment In the production environment, it is normal to connect a database with CLI and SDK clients. There are three scenarios. + - Scenario 1: Client1 and the database are in the same Kubernetes cluster. To connect client1 and the database, see [Procedure 3](#procedure-3-connect-database-in-the-same-kubernetes-cluster). - Scenario 2: Client2 is outside the Kubernetes cluster, but it is in the same VPC as the database. To connect client2 and the database, see [Procedure 5](#procedure-5-client-outside-the-kubernetes-cluster-but-in-the-same-vpc-as-the-kubernetes-cluster). - Scenario 3: Client3 and the database are in different VPCs, such as other VPCs or the public network. To connect client3 and the database, see [Procedure 4](#procedure-4-connect-database-with-clients-in-other-vpcs-or-public-networks). See the figure below to get a clear image of the network location. + ![Example](./../../img/connect_database_in_a_production_environment.png) ## Procedure 3. Connect database in the same Kubernetes cluster -You can connect with the database ClusterIP or domain name. To check the database endpoint, use ```kbcli cluster describe ${cluster-name}```. +You can connect with the database ClusterIP or domain name. To check the database endpoint, use `kbcli cluster describe ${cluster-name}`. ```bash kbcli cluster describe x @@ -52,13 +55,14 @@ You can enable the External LoadBalancer of the cloud vendor. :::note -The following command will create a LoadBalancer instance for the database instance, which will cause costs from your cloud vendor. +The following command creates a LoadBalancer instance for the database instance, which may incur expenses from your cloud vendor. ::: ```bash kbcli cluster expose ${cluster-name} --type internet --enable=true ``` + To disable the LoadBalancer instance, execute the following command. ```bash @@ -77,7 +81,7 @@ A stable domain name for long-term connections is required. An Internal LoadBala :::note -The following command will create a LoadBalancer instance for the database instance, which will cause costs from your cloud vendor. +The following command creates a LoadBalancer instance for the database instance, which may incur expenses from your cloud vendor. ::: @@ -95,4 +99,4 @@ Once disabled, the instance is not accessible. ```bash kbcli cluster expose ${cluster-name} --type vpc --enable=false -``` \ No newline at end of file +``` diff --git a/docs/user_docs/connect_database/connect-database-in-testing-environment.md b/docs/user_docs/connect_database/connect-database-in-testing-environment.md index 2b1bf109f..174551535 100644 --- a/docs/user_docs/connect_database/connect-database-in-testing-environment.md +++ b/docs/user_docs/connect_database/connect-database-in-testing-environment.md @@ -1,13 +1,14 @@ --- title: Connect database in testing environment description: How to connect to a database in testing environment +keywords: [connect to a database, testing environment, test environment] sidebar_position: 2 sidebar_label: Testing environment --- # Connect database in testing environment -## Procedure 1. Use kbcli cluster connect command +## Procedure 1. Use kbcli cluster connect command You can use the `kbcli cluster connect` command and specify the cluster name to be connected. @@ -26,10 +27,11 @@ kbcli cluster connect --show-example ${cluster-name} ``` Information printed includes database addresses, port No., username, password. The figure below is an example of MySQL database network information. + - Address: -h specifies the server address. In the example below it is 127.0.0.1 - Port: -P specifies port No. , In the example below it is 3306. - User: -u is the user name. -- Password: -p shows the password. In the example below, it is hQBCKZLI. +- Password: -p shows the password. In the example below, it is hQBCKZLI. :::note @@ -37,4 +39,4 @@ The password does not include -p. ::: -![Example](./../../img/connect_database_with_CLI_or_SDK_client.png) \ No newline at end of file +![Example](./../../img/connect_database_with_CLI_or_SDK_client.png) diff --git a/docs/user_docs/connect_database/overview-of-database-connection.md b/docs/user_docs/connect_database/overview-of-database-connection.md index f20b2de26..d0bbf6103 100644 --- a/docs/user_docs/connect_database/overview-of-database-connection.md +++ b/docs/user_docs/connect_database/overview-of-database-connection.md @@ -1,13 +1,17 @@ --- title: Connect database from anywhere description: How to connect to a database +keywords: [connect to a database] sidebar_position: 1 sidebar_label: Overview --- # Overview of Database Connection -After the deployment of KubeBlocks and creating clusters, the database runs on Kubernetes as a Pod. You can connect to the database via a client interface or `kbcli`. + +After the deployment of KubeBlocks and creating clusters, the database runs on Kubernetes as a Pod. You can connect to the database via a client interface or `kbcli`. As the figure below indicates, you must be clear with the purpose of connecting the database. - - To take a trial of KubeBlocks, and test the database function, or benchmarking with low-flow testing, see [Connect database in testing environment](connect-database-in-testing-environment.md). - - To connect database in a production environment, or for high-flow pressure test, see [Connect database in production environment](connect-database-in-production-environment.md). -![Connect database](./../../img/connect_database.png) \ No newline at end of file + +- To take a trial of KubeBlocks, and test the database function, or benchmarking with low-flow testing, see [Connect database in a testing environment](connect-database-in-testing-environment.md). +- To connect a database in a production environment, or for a high-flow pressure test, see [Connect database in a production environment](connect-database-in-production-environment.md). + +![Connect database](./../../img/connect_database.png) diff --git a/docs/user_docs/installation/enable-add-ons.md b/docs/user_docs/installation/enable-add-ons.md deleted file mode 100644 index 6c086f223..000000000 --- a/docs/user_docs/installation/enable-add-ons.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -title: Enable add-ons -description: Enable KubeBlocks add-ons -sidebar_position: 2 -sidebar_label: Enable add-ons ---- - -# Enable add-ons - -An add-on provides extension capabilities, i.e., manifests or application software, to KubeBlocks control plane. -By default, all add-ons supported are automatically installed. -To list supported add-ons, run `kbcli addon list` command. - -**Example** - -``` -kbcli addon list -``` - -:::note - -Some add-ons have an environment requirement. If a certain requirement is not met, the automatic installation is invalid. - -::: - -You can perform the following steps to check and enable the add-on. - -***Steps:*** - -1. Run `kbcli addon describe`, and check the *Installable* part in the output information. - - **Example** - - ``` - kbcli addon describe snapshot-controller - ``` - - For certain add-ons, the installable part might say when the kubeGitVersion content includes *eks* and *ack*, the auto-install is enabled. - In this case, you can check the version of the Kubernetes cluster, and run the following command. - ``` - kubectl version -ojson | jq '.serverVersion.gitVersion' - > - "v1.24.4+eks" - > - ``` - As the printed output suggested, *eks* is included. And you can go on with the next step. In case that *eks* is not included, it is invalid to enable the add-on. - -2. To enable the add-on, use `kbcli addon enable`. - - **Example** - - ``` - kbcli addon enable snapshot-controller - ``` - -3. List the add-ons to check whether it is enabled. - - ``` - kbcli addon list - ``` diff --git a/docs/user_docs/installation/enable-addons.md b/docs/user_docs/installation/enable-addons.md new file mode 100644 index 000000000..4423330f2 --- /dev/null +++ b/docs/user_docs/installation/enable-addons.md @@ -0,0 +1,63 @@ +--- +title: Enable add-ons when installing KubeBlocks +description: Enable add-ons when installing KubeBlocks +keywords: [addons, enable, KubeBlocks, prometheus, s3, alertmanager,] +sidebar_position: 4 +sidebar_label: Enable add-ons +--- + +# Enable add-ons + +An add-on provides extension capabilities, i.e., manifests or application software, to the KubeBlocks control plane. + +:::note + +Using `kbcli playground init` command to install KubeBlocks enables prometheus and grafana for observability by default. But if you install KubeBlocks with `kbcli kubeblocks install`, prometheus and grafana are disabled by default. + +::: + +To list supported add-ons, run `kbcli addon list` command. + +```bash +kbcli addon list +NAME TYPE STATUS EXTRAS AUTO-INSTALL AUTO-INSTALLABLE-SELECTOR +snapshot-controller Helm Enabling false {key=KubeGitVersion,op=DoesNotContain,values=[tke]} +kubeblocks-csi-driver Helm Disabled node false {key=KubeGitVersion,op=Contains,values=[eks]} +grafana Helm Enabling true +prometheus Helm Enabling alertmanager true +migration Helm Disabled false +postgresql Helm Enabling true +mongodb Helm Enabling true +aws-load-balancer-controller Helm Disabled false {key=KubeGitVersion,op=Contains,values=[eks]} +apecloud-mysql Helm Enabling true +redis Helm Enabling true +milvus Helm Enabling true +weaviate Helm Enabling true +csi-hostpath-driver Helm Disabled false {key=KubeGitVersion,op=DoesNotContain,values=[eks aliyun gke tke aks]} +nyancat Helm Disabled false +csi-s3 Helm Disabled false +alertmanager-webhook-adaptor Helm Enabled true +qdrant Helm Enabled true +``` + +:::note + +Some add-ons have environment requirements. If a certain requirement is not met, the automatic installation is invalid. So you can check the *AUTO-INSTALLABLE-SELECTOR* item of the output. +You can use `kbcli addon describe [addon name]` command to check the installation requirement. + +::: + +**To manually enable or disable add-ons** +***Steps:*** +1. To enable the add-on, use `kbcli addon enable`. + + ```bash + kbcli addon enable snapshot-controller + ``` + + To disable the add-on, use `kbcli addon disable`. +2. List the add-ons again to check whether it is enabled. + + ```bash + kbcli addon list + ``` diff --git a/docs/user_docs/installation/install-and-uninstall-kbcli-and-kubeblocks.md b/docs/user_docs/installation/install-and-uninstall-kbcli-and-kubeblocks.md deleted file mode 100644 index f7b864e92..000000000 --- a/docs/user_docs/installation/install-and-uninstall-kbcli-and-kubeblocks.md +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: Install and uninstall kbcli and KubeBlocks -description: Install KubeBlocks and kbcli developed by ApeCloud -sidebar_position: 1 -sidebar_label: kbcli and KubeBlocks ---- - -# Install and uninstall kbcli and KubeBlocks - -This guide introduces how to install KubeBlocks by `kbcli`, the command line tool of KubeBlocks. - -## Before you start - -1. A Kubernetes environment is required. -2. `kubectl` is required and can connect to your Kubernetes clusters. Refer to [Install and Set Up kubectl on macOS](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/) for installation details. - -## Install kbcli - -1. Run the command below to install `kbcli`. `kbcli` can run on macOS and Linux. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - - :::note - - Please try again if a time-out exception occurs during installation. It may relate to your network condition. - - ::: - -2. Run this command to check the version and verify whether `kbcli` is installed successfully. - ```bash - kbcli version - ``` - -## Install KubeBlocks - -1. Run the command below to install KubeBlocks. - ```bash - kbcli kubeblocks install - ``` - ***Result*** - - * KubeBlocks is installed with built-in toleration which tolerates the node with the `kb-controller=true:NoSchedule` taint. - * KubeBlocks is installed with built-in node affinity which first deploys the node with the `kb-controller:true` label. - * This command installs the latest version in your Kubernetes environment under the default namespace `kb-system` since your `kubectl` can connect to your Kubernetes clusters. If you want to install KubeBlocks in a specified namespace, run the command below. - ```bash - kbcli kubeblocks install -n --create-namespace=true - ``` - - ***Example*** - - ```bash - kbcli kubeblocks install -n kubeblocks --create-namespace=true - ``` - - You can also run the command below to check the parameters that can be specified during installation. - - ```bash - kbcli kubeblocks install --help - > - Install KubeBlocks - - Examples: - # Install KubeBlocks - kbcli kubeblocks install - - # Install KubeBlocks with specified version - kbcli kubeblocks install --version=0.4.0 - - # Install KubeBlocks with other settings, for example, set replicaCount to 3 - kbcli kubeblocks install --set replicaCount=3 - - Options: - --check=true: - Check kubernetes environment before install - - --create-namespace=false: - Create the namespace if not present - - --monitor=true: - Set monitor enabled and install Prometheus, AlertManager and Grafana (default true) - - --set=[]: - Set values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2) - - --set-file=[]: - Set values from respective files specified via the command line (can specify multiple or separate values with commas: key1=path1,key2=path2) - - --set-json=[]: - Set JSON values on the command line (can specify multiple or separate values with commas: - key1=jsonval1,key2=jsonval2) - - --set-string=[]: - Set STRING values on the command line (can specify multiple or separate values with commas: - key1=val1,key2=val2) - - --timeout=30m0s: - Time to wait for installing KubeBlocks - - -f, --values=[]: - Specify values in a YAML file or a URL (can specify multiple) - - --version='0.4.0-beta.5': - KubeBlocks version - - Usage: - kbcli kubeblocks install [flags] [options] - - Use "kbcli options" for a list of global command-line options (applies to all commands). - ``` - - * `-namespace` and its abbreviated version `-n` is used to name a namespace. `--create-namespace` is used to specify whether to create a namespace if it does not exist. `-n` is a global command line option. For global command line options, run `kbcli options` to list all options (applies to all commands). - * Use `monitor` to specify whether to install the add-ons relevant to database monitoring and visualization. - * Use `version` to specify the version you want to install. Find the supported version in [KubeBlocks Helm Charts](https://github.com/apecloud/helm-charts). - -2. Run the command below to verify whether KubeBlocks is installed successfully. - ```bash - kubectl get pod -n - ``` - - ***Example*** - - ```bash - kubectl get pod -n kb-system - ``` - - ***Result*** - - When the following pods are `Running`, it means KubeBlocks is installed successfully. - - ```bash - NAME READY STATUS RESTARTS AGE - kb-addon-alertmanager-webhook-adaptor-5549f94599-fsnmc 2/2 Running 0 84s - kb-addon-grafana-5ddcd7758f-x4t5g 3/3 Running 0 84s - kb-addon-prometheus-alertmanager-0 2/2 Running 0 84s - kb-addon-prometheus-server-0 2/2 Running 0 84s - kubeblocks-846b8878d9-q8g2w 1/1 Running 0 98s - ``` - -## (Optional) Enable kbcli automatic command line completion - -`kbcli` supports automatic command line completion. You can run the command below to enable this function. - -```bash -# Configure SHELL-TYPE as one type from bash, fish, PowerShell, and zsh -kbcli completion SHELL-TYPE -h -``` - -Here we take zsh as an example. - -1. Run the command below. - ```bash - kbcli completion zsh -h - ``` -2. Enable the completion function of your terminal first. - ```bash - echo "autoload -U compinit; compinit" >> ~/.zshrc - ``` -3. Run the command below to enable the `kbcli` automatic completion function. - ```bash - echo "source <(kbcli completion zsh); compdef _kbcli kbcli" >> ~/.zshrc - ``` - -## Uninstall KubeBlocks and kbcli - -:::note - -Uninstall KubeBlocks first. - -::: - -Run the command below to uninstall KubeBlocks if you want to delete KubeBlocks after your trial. - ```bash - kbcli kubeblocks uninstall - ``` - -Run the command below to uninstall `kbcli`. - ```bash - sudo rm /usr/local/bin/kbcli - ``` \ No newline at end of file diff --git a/docs/user_docs/installation/install-kbcli.md b/docs/user_docs/installation/install-kbcli.md new file mode 100644 index 000000000..cc6cbbae0 --- /dev/null +++ b/docs/user_docs/installation/install-kbcli.md @@ -0,0 +1,135 @@ +--- +title: Install kbcli +description: Install kbcli +keywords: [install, kbcli,] +sidebar_position: 2 +sidebar_label: Install kbcli +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Install kbcli + +You can install kbcli on your laptop or virtual machines on the cloud. + +## Environment preparation + +For Windows users, PowerShell version should be 5.0 or higher. + +## Install kbcli + +kbcli now supports MacOS and Windows. + + + + +You can install kbcli with `curl` or `homebrew`. + +- Option 1: Install kbcli using the `curl` command. + +1. Install kbcli. + + ```bash + curl -fsSL https://kubeblocks.io/installer/install_cli.sh | bash + ``` + +2. Run `kbcli version` to check the version of kbcli and ensure that it is successfully installed. + +:::note + +- If a timeout exception occurs during installation, please check your network settings and retry. + +::: + +- Option 2: Install kbcli using Homebrew. + +1. Install ApeCloud tap, the Homebrew package of ApeCloud. + + ```bash + brew tap apecloud/tap + ``` + +2. Install kbcli. + + ```bash + brew install kbcli + ``` + +3. Verify that kbcli is successfully installed. + + ```bash + kbcli -h + ``` + + + + + +There are two ways to install kbcli on Windows: + +- Option 1: Install using the script. + +:::note + +By default, the script will be installed at C:\Program Files\kbcli-windows-amd64 and cannot be modified. +If you need to customize the installation path, use the zip file. + +::: + +1. Use PowerShell and run Set-ExecutionPolicy Unrestricted. +2. Install kbcli.The following script will automatically install the environment variables at C:\Program Files\kbcli-windows-amd64. + + ```bash + powershell -Command " & ([scriptblock]::Create((iwr [https://www.kubeblocks.io/installer/install_cli.ps1])))" + ``` + + To install a specified version of kbcli, use -v after the command and describe the version you want to install. + + ```bash + powershell -Command " & ([scriptblock]::Create((iwr ))) -v 0.5.0-beta.1" + ``` + +- Option 2: Install using the installation package. + +1. Download the kbcli installation zip package from GitHub. +2. Extract the file and add it to the environment variables. + 1. Click the Windows icon and select **System Settings**. + 2. Click **Settings** -> **Related Settings** -> **Advanced system settings**. + 3. Click **Environment Variables** on the **Advanced** tab. + 4. Click **New** to add the path of the kbcli installation package to the user and system variables. + 5. Click **Apply** and **OK**. + + + + +## (Optional) Enable auto-completion for kbcli + +`kbcli` supports command line auto-completion. + +```bash +# Configure SHELL-TYPE as one type from bash, fish, PowerShell, and zsh +kbcli completion SHELL-TYPE -h +``` + +For example, enable kbcli auto-completion for zsh. + +***Steps:*** + +1. Check the user guide. + + ```bash + kbcli completion zsh -h + ``` + +2. Enable the completion function of your terminal first. + + ```bash + echo "autoload -U compinit; compinit" >> ~/.zshrc + ``` + +3. Enable the `kbcli` automatic completion function. + + ```bash + echo "source <(kbcli completion zsh); compdef _kbcli kbcli" >> ~/.zshrc + ``` diff --git a/docs/user_docs/installation/install-kubeblocks.md b/docs/user_docs/installation/install-kubeblocks.md new file mode 100644 index 000000000..e3e235ef4 --- /dev/null +++ b/docs/user_docs/installation/install-kubeblocks.md @@ -0,0 +1,125 @@ +--- +title: Install KubeBlocks +description: Install KubeBlocks on the existing Kubernetes clusters +keywords: [taints, affinity, tolerance, install, kbcli, KubeBlocks] +sidebar_position: 3 +sidebar_label: Install KubeBlocks +--- + +# Install KubeBlocks + +The quickest way to try out KubeBlocks is to create a new Kubernetes cluster and install KubeBlocks using the playground. However, production environments are more complex, with applications running in different namespaces and with resource or permission limitations. This document explains how to deploy KubeBlocks on an existing Kubernetes cluster. + +## Environment preparation + + + + + + + + + + + + + + + + + + + + + + + + + + +
Resource Requirements
Control PlaneIt is recommended to create 1 node with 4 cores, 4GB memory and 50GB storage.
Data Plane MySQL It is recommended to create at least 3 nodes with 2 cores, 4GB memory and 50GB storage.
PostgreSQL It is recommended to create at least 2 nodes with 2 cores, 4GB memory and 50GB storage.
Redis It is recommended to create at least 2 nodes with 2 cores, 4GB memory and 50GB storage.
MongoDB It is recommended to create at least 3 nodes with 2 cores, 4GB memory and 50GB storage.
+ +## Installation steps + +The command `kbcli kubeblocks install` installs KubeBlocks in the `kb-system` namespace, or you can use the `--namespace` flag to specify one. + +You can also isolate the KubeBlocks control plane and data plane resources by setting taints and tolerations. Choose from the following options. + +### Install KubeBlocks with default tolerations + +By default, KubeBlocks tolerates two taints: `kb-controller:NoSchedule` for the control plane and `kb-data:NoSchedule` for the data plane. You can add these taints to nodes so that KubeBlocks and database clusters are scheduled to the appropriate nodes. + +1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` + +2. Add taints to the selected nodes. + + ```bash + # add control plane taint + kubectl taint nodes kb-controller=true:NoSchedule + + # add data plane taint + kubectl taint nodes kb-data=true:NoSchedule + ``` + +3. Install KubeBlocks. + + ```bash + kbcli kubeblocks install + ``` + +### Install KubeBlocks with custom tolerations + +Another option is to tolerate custom taints, regardless of whether they are already set on the nodes. + +1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` + +2. If the selected nodes do not already have custom taints, add them. + + ```bash + # set control plane taint + kubectl taint nodes =true:NoSchedule + + # set data plane taint + kubectl taint nodes =true:NoSchedule + ``` + +3. Install KubeBlocks with control plane and data plane tolerations. + + ```bash + kbcli kubeblocks install --set-json 'tolerations=[ { "key": "control-plane-taint", "operator": "Equal", "effect": "NoSchedule", "value": "true" } ]' --set-json 'dataPlane.tolerations=[{ "key": "data-plane-taint", "operator": "Equal", "effect": "NoSchedule", "value": "true" } ]' + ``` + +:::note + +When executing the `kbcli kubeblocks install` command, the `preflight` checks will automatically verify the environment. If the cluster satisfies the basic requirements, the installation process will proceed. Otherwise, the process will be terminated, and an error message will be displayed. To skip the `preflight` checks, add the `--force` flag after the `kbcli kubeblocks install` command. + +::: + +## Verify KubeBlocks + +Run the following command to check whether KubeBlocks is installed successfully. + +```bash +kubectl get pod -n kb-system +``` + +***Result*** + +If the following pods are all `Running`, KubeBlocks has been installed successfully. + +```bash +NAME READY STATUS RESTARTS AGE +kb-addon-alertmanager-webhook-adaptor-5549f94599-fsnmc 2/2 Running 0 84s +kb-addon-grafana-5ddcd7758f-x4t5g 3/3 Running 0 84s +kb-addon-prometheus-alertmanager-0 2/2 Running 0 84s +kb-addon-prometheus-server-0 2/2 Running 0 84s +kubeblocks-846b8878d9-q8g2w 1/1 Running 0 98s +``` diff --git a/docs/user_docs/installation/introduction.md b/docs/user_docs/installation/introduction.md new file mode 100644 index 000000000..0bb3ae73e --- /dev/null +++ b/docs/user_docs/installation/introduction.md @@ -0,0 +1,23 @@ +--- +title: Introduction +description: A collection of installation guides +keywords: [install kbcli, install kubeblocks, kbcli, kubeblocks] +sidebar_position: 1 +sidebar_label: Introduction +--- + +# Introduction + +The installation section contains guides for installing `kbcli` and Kubeblocks under different scenarios to build a basic environment for managing clusters on Kubernetes. + +* [Install kbcli](./install-kbcli.md) +* [Install KubeBlocks](./install-kubeblocks.md) +* [Enable add-ons](./enable-addons.md) +* [Uninstall kbcli and KubeBlocks](./uninstall-kbcli-and-kubeblocks.md) + + +:::note + +You must install kbcli before installing KubeBlocks. + +::: diff --git a/docs/user_docs/installation/uninstall-kbcli-and-kubeblocks.md b/docs/user_docs/installation/uninstall-kbcli-and-kubeblocks.md new file mode 100644 index 000000000..bf9fcc7ba --- /dev/null +++ b/docs/user_docs/installation/uninstall-kbcli-and-kubeblocks.md @@ -0,0 +1,89 @@ +--- +title: Uninstall kbcli and KubeBlocks +description: Handle exception and uninstall kbcli and KubeBlocks +keywords: [kbcli, kubeblocks, exception, uninstall] +sidebar_position: 5 +sidebar_label: Uninstall KubeBlocks and kbcli +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + +# Uninstall KubeBlocks and kbcli + +Uninstallation order: + +1. Delete your cluster if you have created a cluster. + + ```bash + kbcli cluster delete + ``` + +2. Uninstall KubeBlocks. + +3. Uninstall `kbcli`. + +## Uninstall KubeBlocks + +Uninstall KubeBlocks if you want to delete KubeBlocks after your trial. + + + + +```bash +kbcli kubeblocks uninstall +``` + + + + + +```bash +helm uninstall kubeblocks -n kb-system +``` + + + + +## Uninstall kbcli + +Uninstall `kbcli` if you want to delete `kbcli` after your trial. Use the same option as the way you install `kbcli`. + + + + +For cURL, run + +```bash +sudo rm /usr/local/bin/kbcli +``` + +For Homebrew, run + +```bash +brew uninstall kbcli +``` + + + + + +1. Go to the `kbcli` installation path and delete the installation folder. + + * If you install `kbcli` by script, go to `C:\Program Files` and delete the `kbcli-windows-amd64` folder. + * If you customize the installation path, go to your specified path and delete the installation folder. + +2. Delete the environment variable. + + 1. Click the Windows icon and click **System**. + 2. Go to **Settings** -> **Related Settings** -> **Advanced system settings**. + 3. On the **Advanced** tab, click **Environment Variables**. + 4. Double-click **Path** in **User variables** or **System variables** list. + * If you install `kbcli` by script, double-click **Path** in **User variables**. + * If you customize the installation path, double-click **Path** based on where you created the variable before. + 5. Select `C:\Program Files\kbcli-windows-amd64` or your customized path and delete it. This operation requires double confirmation. + + + + diff --git a/docs/user_docs/introduction/introduction.md b/docs/user_docs/introduction/introduction.md index fea3e2934..ad23d6732 100644 --- a/docs/user_docs/introduction/introduction.md +++ b/docs/user_docs/introduction/introduction.md @@ -1,31 +1,36 @@ --- title: KubeBlocks overview description: KubeBlocks, kbcli, multicloud +keywords: [kubeblocks, overview, introduction] sidebar_position: 1 --- # KubeBlocks overview ## Introduction -KubeBlocks is an open-source tool designed to help developers and platform engineers build and manage stateful workloads, such as databases and analytics, on Kubernetes. It is cloud-neutral and supports multiple public cloud providers, providing a unified and declarative approach to increase productivity in DevOps practices. -The name KubeBlocks is derived from Kubernetes and building blocks, which indicates that standardizing databases and analytics on Kubernetes can be both productive and enjoyable, like playing with construction toys. KubeBlocks combines the large-scale production experiences of top public cloud providers with enhanced usability and stability. +KubeBlocks is an open-source, cloud-native data infrastructure designed to help application developers and platform engineers manage database and analytical workloads on Kubernetes. It is cloud-neutral and supports multiple cloud service providers, offering a unified and declarative approach to increase productivity in DevOps practices. -## Why You Need KubeBlocks +The name KubeBlocks is derived from Kubernetes and LEGO blocks, which indicates that building database and analytical workloads on Kubernetes can be both productive and enjoyable, like playing with construction toys. KubeBlocks combines the large-scale production experiences of top cloud service providers with enhanced usability and stability. -Kubernetes has become the de facto standard for container orchestration. It manages an ever-increasing number of stateless workloads with the scalability and availability provided by ReplicaSet and the rollout and rollback capabilities provided by Deployment. However, managing stateful workloads poses great challenges for Kubernetes. Although statefulSet provides stable persistent storage and unique network identifiers, these abilities are far from enough for complex stateful workloads. +## Why you need KubeBlocks + +Kubernetes has become the de facto standard for container orchestration. It manages an ever-increasing number of stateless workloads with the scalability and availability provided by ReplicaSet and the rollout and rollback capabilities provided by Deployment. However, managing stateful workloads poses great challenges for Kubernetes. Although StatefulSet provides stable persistent storage and unique network identifiers, these abilities are far from enough for complex stateful workloads. To address these challenges, and solve the problem of complexity, KubeBlocks introduces ReplicationSet and ConsensusSet, with the following capabilities: - Role-based update order reduces downtime caused by upgrading versions, scaling, and rebooting. -- Latency-based election weight reduces the possibility of related workloads or components being located in different available zones. - Maintains the status of data replication and automatically repairs replication errors or delays. -## KubeBlocks Key Features +## Key features -- Kubernetes-native and multi-cloud supported. -- Supports multiple database engines, including MySQL, PostgreSQL, Redis, MongoDB, and more. +- Be compatible with AWS, GCP, Azure, and Alibaba Cloud. +- Supports MySQL, PostgreSQL, Redis, MongoDB, Kafka, and more. - Provides production-level performance, resilience, scalability, and observability. - Simplifies day-2 operations, such as upgrading, scaling, monitoring, backup, and restore. -- Declarative configuration is made simple, and imperative commands are made powerful. -- The learning curve is flat, and you are welcome to submit new issues on GitHub. \ No newline at end of file +- Contains a powerful and intuitive command line tool. +- Sets up a full-stack, production-ready data infrastructure in minutes. + +## Architecture + +![KubeBlocks Architecture](../../img/kubeblocks-structure-new.jpg) diff --git a/docs/user_docs/kubeblocks-for-mongodb/_category_.yml b/docs/user_docs/kubeblocks-for-mongodb/_category_.yml new file mode 100644 index 000000000..aa0c435f4 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/_category_.yml @@ -0,0 +1,4 @@ +position: 6 +label: KubeBlocks for MongoDB +collapsible: true +collapsed: true diff --git a/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/_category_.yml b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/_category_.yml new file mode 100644 index 000000000..585156222 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/_category_.yml @@ -0,0 +1,4 @@ +position: 3 +label: Backup and Restore +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/data-file-backup-and-restore.md b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/data-file-backup-and-restore.md new file mode 100644 index 000000000..c8264f052 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/data-file-backup-and-restore.md @@ -0,0 +1,167 @@ +--- +title: Data file backup and restore for MongoDB +description: How to back up and restore MongoDB by data files +keywords: [backup and restore, mongodb] +sidebar_position: 2 +sidebar_label: Data file +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Data file backup and restore + +For KubeBlocks, configuring backup and restoring data is simple with 3 steps. Configure storage path and backup policy, create a backup(manually or scheduled), and then you can restore data backed up. + +## Configure target storage path + +Currently, KubeBlocks backups and restores data on storage path predefined. + + + + +Enable CSI-S3 and fill in the values based on your actual environment. + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://s3..amazonaws.com.cn \ +--set secret.region= -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + +:::note + +Endpoint format: + +* China: `https://s3..amazonaws.com.cn` +* Other countries/regions: `https://s3..amazonaws.com` + +::: + + + + + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://oss-.aliyuncs.com \ + -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + + + + + +1. Install minIO. + + ```bash + helm upgrade --install minio oci://registry-1.docker.io/bitnamicharts/minio --set persistence.enabled=true,persistence.storageClass=csi-hostpath-sc,persistence.size=100Gi,defaultBuckets=backup + ``` + +2. Install CSI-S3. + + ```bash + helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + + helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ + --set secret.accessKey= \ + --set secret.secretKey= \ + --set storageClass.singleBucket=backup \ + --set secret.endpoint=http://minio.default.svc.cluster.local:9000 \ + -n kb-system + + # CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes + --set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' + ``` + + + + +You can configure a global backup storage to make this storage the default backup destination path of all new clusters. But currently, the global backup storage cannot be synchronized as the backup destination path of created clusters. + +Set the backup policy with the following command. + +```bash +kbcli kubeblocks config --set dataProtection.backupPVCName=kubeblocks-backup-data \ +--set dataProtection.backupPVCStorageClassName=csi-s3 -n kb-system + +# dataProtection.backupPVCName: PersistentVolumeClaim Name for backup storage +# dataProtection.backupPVCStorageClassName: StorageClass Name +# -n kb-system namespace where KubeBlocks is installed +``` + +:::note + +* If there is no PVC, the system creates one automatically based on the configuration. +* It takes about 1 minute to make the configuration effective. + +::: + +## Create backup + +**Option 1. Manually Backup** + +1. Check whether the cluster is running. + + ```bash + kbcli cluster list mongodb-cluster + ``` + +2. Create a backup for this cluster. + + ```bash + kbcli cluster backup mongodb-cluster --type=datafile + ``` + +3. View the backup set. + + ```bash + kbcli cluster list-backups mongodb-cluster + ``` + +**Option 2. Enable scheduled backup** + +```bash +kbcli cluster edit-backup-policy mongodb-cluster-mongodb-backup-policy +> +spec: + ... + schedule: + baseBackup: + # UTC time zone, the example below means 2 a.m. every Monday + cronExpression: "0 18 * * 0" + # Enable this function + enable: true + # Select the basic backup type, available options: snapshot and datafile + # This example selects datafile as the basic backup type + type: datafile +``` + +## Restore data from backup + +1. Restore data from the backup. + + ```bash + kbcli cluster restore new-mongodb-cluster --backup backup-default-mongodb-cluster-20230418124113 + ``` + +2. View this new cluster. + + ```bash + kbcli cluster list new-mongodb-cluster + ``` diff --git a/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/snapshot-backup-and-restore-for-mongodb.md b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/snapshot-backup-and-restore-for-mongodb.md new file mode 100644 index 000000000..29fad37af --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/backup-and-restore/snapshot-backup-and-restore-for-mongodb.md @@ -0,0 +1,126 @@ +--- +title: Snapshot backup and restore for MongoDB +description: Guide for backup and restore for MongoDB +keywords: [mongodb, snapshot, backup, restore] +sidebar_position: 2 +sidebar_label: Snapshot backup and restore +--- +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Snapshot backup and restore for mongodb + +This section shows how to use `kbcli` to back up and restore a mongodb cluster. + +***Steps:*** + +1. Install KubeBlocks and the snapshot-controller add-on. + + ```bash + kbcli kubeblocks install --set snapshot-controller.enabled=true + ``` + + If you have installed KubeBlocks without enabling the snapshot-controller, run the command below. + + ```bash + kbcli kubeblocks upgrade --set snapshot-controller.enabled=true + ``` + + Since your `kubectl` is already connected to the cluster of cloud Kubernetes service, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your environment. + + Verify the installation with the following command. + + ```bash + kubectl get pod -n kb-system + ``` + + The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. + + ```bash + NAME READY STATUS RESTARTS AGE + kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m + kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m + ``` + + If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](./../../installation/enable-addons.md) to find the environment requirements and then enable the snapshot-controller add-on. + +2. Configure cloud managed Kubernetes environment to support the snapshot function. For ACK and GKE, the snapshot function is enabled by default, you can skip this step. + + + + + The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. + + Configure the storage class of the snapshot (the assigned EBS volume is gp3). + + ```yaml + kubectl create -f - < + + + + Configure the default volumesnapshot class. + + ```yaml + kubectl create -f - < + + +3. Create a snapshot backup. + + ```bash + kbcli cluster backup mongodb-cluster + ``` + +4. Check the backup. + + ```bash + kbcli cluster list-backups + ``` + +5. Restore to a new cluster. + + Copy the backup name to the clipboard, and restore to the new cluster. + + :::note + + You do not need to specify other parameters for creating a cluster. The restoration automatically reads the parameters of the source cluster, including specification, disk size, etc., and creates a new mongodb cluster with the same specifications. + + ::: + + Execute the following command. + + ```bash + kbcli cluster restore mongodb-new-from-snapshot --backup backup-default-mongodb-cluster-20221124113440 + ``` diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/_category_.yml b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/_category_.yml new file mode 100644 index 000000000..27efe6be9 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/_category_.yml @@ -0,0 +1,4 @@ +position: 1 +label: Cluster Management +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/create-and-connect-to-a-mongodb-cluster.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/create-and-connect-to-a-mongodb-cluster.md new file mode 100644 index 000000000..61f6b1ef8 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/create-and-connect-to-a-mongodb-cluster.md @@ -0,0 +1,176 @@ +--- +title: Create and connect to a MongoDB Cluster +description: How to create and connect to a MongoDB cluster +keywords: [mogodb, create a mongodb cluster] +sidebar_position: 1 +sidebar_label: Create and connect +--- + +# Create and connect to a MongoDB cluster + +This document shows how to create and connect to a MongoDB cluster. + +## Create a MongoDB cluster + +### Before you start + +* [Install `kbcli`](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Make sure MongoDB addon is installed with `kbcli addon list`. + + ```bash + kbcli addon list + > + NAME TYPE STATUS EXTRAS AUTO-INSTALL INSTALLABLE-SELECTOR + ... + mongodb Helm Enabled true + ... + ``` + +* View all the database types and versions available for creating a cluster. + + ```bash + kbcli clusterversion list + ``` + +### (Recommended) Create a cluster on a tainted node + +In actual scenarios, you are recommended to create a cluster on nodes with taints and customized specifications. + +***Steps*** + +1. Taint your node. + + :::note + + If you have already some tainted nodes, you can skip this step. + + ::: + + 1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` + + 2. Place taints on the selected nodes. + + ```bash + kubectl taint nodes =true:NoSchedule + ``` + +2. Create a MongoDB cluster. + + The cluster creation command is simply `kbcli cluster create`. Use tolerances to deploy it on the tainted node. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + + To create a cluster with a specified class, you can use `--set` flag and specify your requirement. + + ```bash + kbcli cluster create mongodb-cluster --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --cluster-definition=mongodb --namespace --set cpu=1,memory=1Gi,storage=10Gi,storageClass= + ``` + + Or change the corresponding parameters in the YAML file. + + ```bash + kbcli cluster create --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --cluster-definition=mongodb --namespace --set-file -< + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi + EOF + ``` + +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose the database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this option is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
`kbcli cluster create --cluster-definition='apecloud-mysql' --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=3,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a MongoDB cluster with default settings. + +```bash +kbcli cluster create --cluster-definition=mongodb --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' +``` + +### Create a MongoDB cluster on a node without taints + +Create a MongoDB cluster. + +The cluster creation command is simply `kbcli cluster create`. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + +To create a cluster with a specified class, you can use `--set` flag and specify your requirement. + +```bash +kbcli cluster create mongodb-cluster --cluster-definition=mongodb --namespace --set cpu=1,memory=1Gi,storage=10Gi,storageClass= +``` + +Or change the corresponding parameters in the YAML file. + +```bash +kbcli cluster create mongodb-cluster --cluster-definition="mongodb" --namespace --set-file -< + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 10Gi +EOF +``` + +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:----------------------------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose the database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this option is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
`kbcli cluster create --cluster-definition='apecloud-mysql' --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=3,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a MongoDB cluster with default settings. + +```bash +kbcli cluster create mongodb-cluster --cluster-definition mongodb +``` + +## Connect to a MongoDB Cluster + +```bash +kbcli cluster connect mongodb-cluster +``` + +For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-postgresql-cluster.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/delete-mongodb-cluster.md similarity index 53% rename from docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-postgresql-cluster.md rename to docs/user_docs/kubeblocks-for-mongodb/cluster-management/delete-mongodb-cluster.md index 81b5e9917..87acc80f7 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-postgresql-cluster.md +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/delete-mongodb-cluster.md @@ -1,41 +1,32 @@ --- -title: Delete a PostgreSQL Cluster -description: How to delete a PostgreSQL Cluster +title: Delete a MongoDB Cluster +description: How to delete a MongoDB Cluster +keywords: [mongodb, delete a cluster, delete protection] sidebar_position: 6 sidebar_label: Delete protection --- -# Delete a PostgreSQL Cluster +# Delete a MongoDB cluster +## Termination policy :::note -The termination policy determines how you delete a cluster. +The termination policy determines how a cluster is deleted. Set the policy when creating a cluster. ::: -## Termination policy - | **terminationPolicy** | **Deleting Operation** | |:-- | :-- | | `DoNotTerminate` | `DoNotTerminate` blocks delete operation. | | `Halt` | `Halt` deletes workload resources such as statefulset, deployment workloads but keep PVCs. | -| `Delete ` | `Delete` delete workload resources and PVCs but keep backups. | +| `Delete` | `Delete` deletes workload resources and PVCs but keep backups. | | `WipeOut` | `WipeOut` deletes workload resources, PVCs and all relevant resources included backups. | To check the termination policy, execute the following command. ```bash -kbcli cluster list -``` - -***Example*** - -```bash -kbcli cluster list pg-cluster -> -NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME -pg-cluster default postgresql postgresql-14.7.0 Delete Running Mar 03,2023 18:49 UTC+0800 +kbcli cluster list mongodb-cluster ``` ## Option 1. Use kbcli @@ -43,13 +34,7 @@ pg-cluster default postgresql postgresql-14.7.0 Delete Configure the cluster name and run the command below to delete the specified cluster. ```bash -kbcli cluster delete -``` - -***Example*** - -```bash -kbcli cluster delete pg-cluster +kbcli cluster delete mongodb-cluster ``` ## Option 2. Use kubectl @@ -57,11 +42,5 @@ kbcli cluster delete pg-cluster Configure the cluster name and run the command below to delete the specified cluster. ```bash -kubectl delete cluster -``` - -***Example*** - -```bash -kubectl delete cluster pg-cluster +kubectl delete cluster mongodb-cluster ``` diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/expand-volume.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/expand-volume.md new file mode 100644 index 000000000..37e4d452d --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/expand-volume.md @@ -0,0 +1,62 @@ +--- +title: Expand volume +description: How to expand the volume of a MongoDB cluster +keywords: [mongodb, expand volume, volume expansion] +sidebar_position: 3 +sidebar_label: Expand volume +--- + +# Expand volume + +You can expand the storage volume size of each pod. + +## Before you start + +Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. + +```bash +kbcli cluster list mongodb-cluster +``` + +## Option 1. Use kbcli + +Use `kbcli cluster volume-expand` command, configure the resources required and enter the cluster name again to expand the volume. + +```bash +kbcli cluster volume-expand --storage=30G --component-names=mongodb --volume-claim-templates=data mongodb-cluster +> +OpsRequest mongodb-cluster-volumeexpansion-gcfzp created successfully, you can view the progress: + kbcli cluster describe-ops mongodb-cluster-volumeexpansion-gcfzp -n default +``` + +- `--component-names` describes the component name for volume expansion. +- `--volume-claim-templates` describes the VolumeClaimTemplate names in components. +- `--storage` describes the volume storage size. + +## Option 2. Change the YAML file of the cluster + +Change the value of `spec.components.volumeClaimTemplates.spec.resources` in the cluster YAML file. `spec.components.volumeClaimTemplates.spec.resources` is the storage resource information of the pod and changing this value triggers the volume expansion of a cluster. + +```yaml +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: mongodb-cluster + namespace: default +spec: + clusterDefinitionRef: mongodb + clusterVersionRef: mongodb-5.0.14 + componentSpecs: + - name: mongodb + componentDefRef: mongodb + replicas: 1 + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi # Change the volume storage size. + terminationPolicy: Halt +``` diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/restart-mongodb-cluster.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/restart-mongodb-cluster.md new file mode 100644 index 000000000..c841dacc2 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/restart-mongodb-cluster.md @@ -0,0 +1,28 @@ +--- +title: Restart a MongoDB cluster +description: How to restart a MongoDB cluster +keywords: [mongodb, restart a cluster] +sidebar_position: 4 +sidebar_label: Restart +--- + +# Restart MongoDB cluster + +You can restart all pods of the cluster. When an exception occurs in a database, you can try to restart it. + +## Steps + +1. Restart a cluster with `kbcli cluster restart` command and enter the cluster name again. + + ```bash + kbcli cluster restart mongodb-cluster + > + OpsRequest mongodb-cluster-restart-pzsbj created successfully, you can view the progress: + kbcli cluster describe-ops mongodb-cluster-restart-pzsbj -n default + ``` + +2. Validate the restarting with the request code randomly generated, in this guide, it is `pzsbj`, see step 1. + + ```bash + kbcli cluster describe-ops mongodb-cluster-restart-pzsbj -n default + ``` diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/scale-for-apecloud-mongodb.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/scale-for-apecloud-mongodb.md new file mode 100644 index 000000000..30992c06e --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/scale-for-apecloud-mongodb.md @@ -0,0 +1,114 @@ +--- +title: Scale for MongoDB cluster +description: How to vertically scale a MongoDB cluster +keywords: [mongodb, vertical sclaing, vertially scale a mongodb cluster] +sidebar_position: 2 +sidebar_label: Scale +--- + +# Scale for a MongoDB cluster + +For MongoDB, vertical scaling is supported. + +## Vertical scaling + +You can vertically scale a cluster by changing resource requirements and limits (CPU and storage). For example, if you need to change the resource demand from 1C2G to 2C4G, vertical scaling is what you need. + +:::note + +During the vertical scaling process, a restart is triggered and the primary pod may change after the restarting. + +::: + +### Before you start + +Check whether the cluster status is `Running`. Otherwise, the following operations may fail. + +```bash +kbcli cluster list mongodb-cluster +``` + +### Steps + +1. Change configuration. There are 3 ways to apply vertical scaling. + + **Option 1.** (**Recommended**) Use kbcli + + 1. Use `kbcli cluster vscale` and configure the resources required. + + ***Example*** + + ```bash + kbcli cluster vscale mongodb-cluster --component-names=mongodb --cpu=500m --memory=500Mi + > + OpsRequest mongodb-cluster-verticalscaling-thglk created successfully, you can view the progress: + kbcli cluster describe-ops mongodb-cluster-verticalscaling-thglk -n default + ``` + + - `--component-names` describes the component name ready for vertical scaling. + - `--memory` describes the requested and limited size of the component memory. + - `--cpu` describes the requested and limited size of the component CPU. + + 2. Validate the scaling with `kbcli cluster describe-ops mongodb-cluster-verticalscaling-thglk -n default`. + + :::note + + `thglk` is the OpsRequest number randomly generated in step 1. + + ::: + + **Option 2.** Change the YAML file of the cluster + + Change the configuration of `spec.components.resources` in the YAML file. `spec.components.resources` controls the requirement and limit of resources and changing them triggers a vertical scaling. + + ***Example*** + + ```YAML + apiVersion: apps.kubeblocks.io/v1alpha1 + kind: Cluster + metadata: + name: mongodb-cluster + namespace: default + spec: + clusterDefinitionRef: mongodb + clusterVersionRef: mongodb-5.0.14 + componentSpecs: + - name: mongodb + componentDefRef: mongodb + replicas: 1 + resources: # Change the values of resources. + requests: + memory: "2Gi" + cpu: "1000m" + limits: + memory: "4Gi" + cpu: "2000m" + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + terminationPolicy: Halt + ``` + +2. Validate the vertical scaling. + + ```bash + kbcli cluster list mongodb-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + mongodb-cluster default mongodb mongodb-5.0.14 WipeOut Running Apr 26,2023 11:50 UTC+0800 + ``` + + - STATUS=VerticalScaling: it means the vertical scaling is in progress. + - STATUS=Running: it means the vertical scaling operation has been applied. + - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be the normal instances number is less than the total instance number or the leader instance is running properly while others are abnormal. + + :::note + + To solve the problem, you can check manually to see whether resources are sufficient. If AutoScaling is supported, the system recovers when there are enough resources, otherwise, you can create enough resources and check the result with kubectl describe command. + + ::: diff --git a/docs/user_docs/kubeblocks-for-mongodb/cluster-management/start-stop-a-cluster.md b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/start-stop-a-cluster.md new file mode 100644 index 000000000..00807a3c1 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/cluster-management/start-stop-a-cluster.md @@ -0,0 +1,35 @@ +--- +title: Stop/Start a MongoDB cluster +description: How to start/stop a MongoDB cluster +keywords: [mongodb, stop a momgodb cluster, start a mongodb cluster] +sidebar_position: 5 +sidebar_label: Stop/Start +--- + +# Stop/Start a MongoDB Cluster + +You can stop/start a cluster to save computing resources. When a cluster is stopped, the computing resources of this cluster are released, which means the pods of Kubernetes are released, but the storage resources are reserved. Start this cluster again if you want to restore the cluster resources from the original storage by snapshots. + +## Stop a cluster + +***Steps:*** + +1. Configure the name of your cluster and run the command below to stop this cluster. + + ```bash + kbcli cluster stop mongodb-cluster + ``` + +2. Check the status of the cluster to see whether it is stopped. + + ```bash + kbcli cluster list + ``` + +## Start a cluster + +Configure the name of your cluster and run the command below to stop this cluster. + +```bash +kbcli cluster start mongodb-cluster +``` diff --git a/docs/user_docs/api/observability/_category_.yml b/docs/user_docs/kubeblocks-for-mongodb/configuration/_category_.yml similarity index 68% rename from docs/user_docs/api/observability/_category_.yml rename to docs/user_docs/kubeblocks-for-mongodb/configuration/_category_.yml index 2ad5ad924..cec02728b 100644 --- a/docs/user_docs/api/observability/_category_.yml +++ b/docs/user_docs/kubeblocks-for-mongodb/configuration/_category_.yml @@ -1,4 +1,4 @@ position: 2 -label: Observebility +label: Configuration collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mongodb/configuration/configuration.md b/docs/user_docs/kubeblocks-for-mongodb/configuration/configuration.md new file mode 100644 index 000000000..bd2d8fc7e --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mongodb/configuration/configuration.md @@ -0,0 +1,78 @@ +--- +title: Configure cluster parameters +description: Configure cluster parameters +keywords: [parameter, configuration, reconfiguration] +sidebar_position: 1 +--- + +# Configure cluster parameters + +The KubeBlocks configuration function provides a set of consistent default configuration generation strategies for all the databases running on KubeBlocks and also provides a unified parameter configuration interface to facilitate managing parameter reconfiguration, searching the parameter user guide, and validating parameter effectiveness. + +## View parameter information + +View the current configuration file of a cluster. + +```bash +kbcli cluster describe-config mongodb-cluster +> +ConfigSpecs Meta: +CONFIG-SPEC-NAME FILE ENABLED TEMPLATE CONSTRAINT RENDERED COMPONENT CLUSTER +mongodb-config keyfile false mongodb5.0-config-template mongodb-config-constraints mongodb-cluster-replicaset-mongodb-config replicaset mongodb-cluster +mongodb-config mongodb.conf true mongodb5.0-config-template mongodb-config-constraints mongodb-cluster-replicaset-mongodb-config replicaset mongodb-cluster +mongodb-metrics-config metrics-config.yaml false mongodb-metrics-config mongodb-cluster-replicaset-mongodb-metrics-config replicaset mongodb-cluster + +History modifications: +OPS-NAME CLUSTER COMPONENT CONFIG-SPEC-NAME FILE STATUS POLICY PROGRESS CREATED-TIME VALID-UPDATED +``` + +From the meta information, the cluster `mongodb-cluster` has a configuration file named `mongodb.conf`. + +You can also view the details of this configuration file and parameters. + +* View the details of the current configuration file. + + ```bash + kbcli cluster describe-config mongodb-cluster --show-detail + ``` + +## Reconfigure parameters + +The example below reconfigures velocity to 1. + +1. Adjust the values of `velocity` to 1. + + ```bash + kbcli cluster configure mongodb-cluster --component mongodb --config-spec mongodb-config --config-file mongodb.conf --set systemLog.verbosity=1 + > + Warning: The parameter change you modified needs to be restarted, which may cause the cluster to be unavailable for a period of time. Do you need to continue... + Please type "yes" to confirm: yes + Will updated configure file meta: + ConfigSpec: mongodb-config ConfigFile: mongodb.conf ComponentName: mongodb ClusterName: mongodb-cluster + OpsRequest mongodb-cluster-reconfiguring-q8ndn created successfully, you can view the progress: + kbcli cluster describe-ops mongodb-cluster-reconfiguring-q8ndn -n default + ``` + +2. Check configure history. + + ```bash + + kbcli cluster describe-config mongodb-cluster + > + ConfigSpecs Meta: + CONFIG-SPEC-NAME FILE ENABLED TEMPLATE CONSTRAINT RENDERED COMPONENT CLUSTER + mongodb-config keyfile false mongodb5.0-config-template mongodb-config-constraints mongodb-cluster-mongodb-mongodb-config mongodb mongodb-cluster + mongodb-config mongodb.conf true mongodb5.0-config-template mongodb-config-constraints mongodb-cluster-mongodb-mongodb-config mongodb mongodb-cluster + mongodb-metrics-config metrics-config.yaml false mongodb-metrics-config mongodb-cluster-mongodb-mongodb-metrics-config mongodb mongodb-cluster + + History modifications: + OPS-NAME CLUSTER COMPONENT CONFIG-SPEC-NAME FILE STATUS POLICY PROGRESS CREATED-TIME VALID-UPDATED + mongodb-cluster-reconfiguring-q8ndn mongodb-cluster mongodb mongodb-config mongodb.conf Succeed restart 3/3 Apr 21,2023 18:56 UTC+0800 {"mongodb.conf":"{\"systemLog\":{\"verbosity\":\"1\"}}"}``` + ``` + +3. Verify change result. + + ```bash + root@mongodb-cluster-mongodb-0:/# cat etc/mongodb/mongodb.conf |grep verbosity + verbosity: "1" + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/apecloud-mysql-intro/apecloud-mysql-intro.md b/docs/user_docs/kubeblocks-for-mysql/apecloud-mysql-intro/apecloud-mysql-intro.md index 055c071cb..d1e2a2a1c 100644 --- a/docs/user_docs/kubeblocks-for-mysql/apecloud-mysql-intro/apecloud-mysql-intro.md +++ b/docs/user_docs/kubeblocks-for-mysql/apecloud-mysql-intro/apecloud-mysql-intro.md @@ -1,30 +1,28 @@ --- -title: ApeCloud MySQL introduction -description: What is Apeloud MySQL? +title: MySQL introduction +description: MySQL introduction +keywords: [apecloud, mysql, introduction] sidebar_position: 1 --- -# ApeCloud MySQL introduction -ApeCloud MySQL is a high-availability MySQL database provided by ApeCloud. It is fully compatible with MySQL with high availability(HA) and disaster recovery(DR) to help ensure business continuity for your database workloads. - - When there are 3 or more replicas, a strong consistent high-availability cluster is created with the consensus algorithm protocol to ensure that RPO=0 in the case of a single availability zone failure. Among them, the primary instance provides read/write capacity, and the remaining instances provide read-only services. - - When there are 2 replicas, a Primary-Secondary replication cluster is created, in which the primary instance provides read/write capacity, and the secondary instance keeps in sync with the primary instance with asynchronous replication, providing read-only and fault tolerance capabilities. - - When there is only 1 replica, a standalone cluster is created to provide read/write capacity. Automatic fault recovery capability is still provided, and RPO=0 remains ensured if the cloud disk is not damaged. +# MySQL introduction +MySQL is the world’s most popular open-source database and the second-most-popular database overall. It is used by many of the most accessed applications, such as Facebook, Twitter, Netflix, Uber, Airbnb, Shopify, and Booking.com. +KubeBlocks adopts the MySQL distribution provided by ApeCloud, which includes data compression and high availability improvements. -:::note - -In this guide, we use KubeBlocks to manage ApeCloud MySQL. - -::: +- When there are 3 or more replicas, a strong consistent high-availability cluster is created with the consensus algorithm protocol to ensure that RPO=0 in the case of a single availability zone failure. Among them, the primary instance provides read/write capacity, and the remaining instances provide read-only services. +- When there are 2 replicas, a Primary-Secondary replication cluster is created, in which the primary instance provides read/write capacity, and the secondary instance keeps in sync with the primary instance with asynchronous replication, providing read-only and fault tolerance capabilities. +- When there is only 1 replica, a standalone cluster is created to provide read/write capacity. Automatic fault recovery capability is still provided, and RPO=0 remains ensured if the cloud disk is not damaged. ## Instance Roles ApeCloud MySQL supports four roles, **Leader**, **Follower**, **Candidate**, and **Learner**. The Leader and a Follower form a high-availability cluster and ensure RPO=0. + - Leader: This role is the primary instance of the cluster, and supports R/W with forced consistency. It is voted by all the Candidates participating in the election. The Candidates with the majority of votes become the Leader, and the other Candidates become the Follower. - Follower: Follower supports data consistency with read-only capacity, and forms a high-availability cluster with Leader and other Followers. - Learner: This role is usually used for cross-regional consistent read-only data. Data synchronization is performed through the Paxos protocol, and the data source can be a Leader or a Follower. The learner is a special role in the consensus algorithm protocol, and does not participate in voting or being elected as a Candidate role. - Candidate: The Candidate is an intermediate role that exists only during the election process or when a majority number is not enough to select the Leader role. Normally, all Candidates in a high availability cluster will eventually become a Leader or a Follower after the election is completed. - Role | Leader |Follower | Learner | Candidate | + Role | Leader |Follower | Learner | Candidate | ---- |----| ----|----|----| **Capcity**|RW/HA|RO/HA|RO|-| @@ -32,7 +30,7 @@ ApeCloud MySQL supports four roles, **Leader**, **Follower**, **Candidate**, and ### Failover -A failover is the redirection of traffic and switches the running tasks from a primary instance to a secondary instance. +A failover is the redirection of traffic and switches the running tasks from a primary instance to a secondary instance. ### Read-only @@ -42,7 +40,7 @@ Replicas provide read-only capabilities. In addition to the Follower that can pr The cluster supports node fault tolerance. Suppose the number of replicas is n, then the number of faulty replicas that can be tolerated is `floor (n/2) + 1,n=[1,99]`, which meets the requirements of the Paxos algorithm protocol. Based on this, it can be obtained that under the specified tolerable number f of ApeCloud MySQL cluster nodes, the number of replicas that need to be created is n=2*f+1, f>=0. For example, if the tolerable number of faulty replicas is 1, then according to the formula, the minimum number of replicas in the cluster is 3, that is, in a Paxos group, the continuous service capability of the cluster with 1 faulty replica is guaranteed. According to the table below, it can be seen that it is more cost-effective to create an odd number of replicas. - Replicas in Cluster | Node Majority | Nodes Tolerable | + Replicas in Cluster | Node Majority | Nodes Tolerable | ---- |----| ----| 3 | 2 | 1 | 4 | 3 | 1 | @@ -51,4 +49,4 @@ The cluster supports node fault tolerance. Suppose the number of replicas is n, 7 | 4 | 3 | 8 | 5 | 3 | 9 | 5 | 4 | - 10 | 6 | 4 | \ No newline at end of file + 10 | 6 | 4 | diff --git a/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/_category_.yml b/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/_category_.yml index ff724d53b..8eac9fde3 100644 --- a/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/_category_.yml +++ b/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/_category_.yml @@ -1,4 +1,4 @@ -position: 4 +position: 5 label: Backup and Restore collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/backup-and-restore-for-mysql-paxos-group.md b/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/backup-and-restore-for-mysql-paxos-group.md deleted file mode 100644 index 252013e64..000000000 --- a/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/backup-and-restore-for-mysql-paxos-group.md +++ /dev/null @@ -1,114 +0,0 @@ ---- -title: Backup and restore for MySQL Paxos Group -description: Guide for backup and restore for an ApeCloud MySQL Paxos Group -sidebar_position: 3 -sidebar_label: MySQL Paxos Group ---- - -# Backup and restore for MySQL Paxos Group -This section shows how to use `kbcli` to back up and restore a MySQL Paxos Group instance. - -***Before you start*** - -- Prepare a clean EKS cluster, and install ebs csi driver plug-in, with at least one node and the memory of each node is not less than 4GB. -- [Install `kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/) to ensure that you can connect to the EKS cluster -- Install `kbcli`. Refer to [Install kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - -***Steps:*** - -1. Install KubeBlocks and the snapshot-controller add-on. - ```bash - kbcli kubeblocks install --set snapshot-controller.enabled=true - ``` - - Since your `kubectl` is already connected to the EKS cluster, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your EKS environment. - - Verify the installation with the following command. - ```bash - kubectl get pod -n kb-system - ``` - - The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. - ``` - NAME READY STATUS RESTARTS AGE - kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m - kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m - ``` - - If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](../../installation/enable-add-ons.md) to find the environment requirements and then enable the snapshot-controller add-on. - -2. Configure EKS to support the snapshot function. - - The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. - - Configure the storage class of the snapshot (the assigned EBS volume is gp3). - ```bash - kubectl create -f - < + + +Enable CSI-S3 and fill in the values based on your actual environment. + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://s3..amazonaws.com.cn \ +--set secret.region= -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + +:::note + +Endpoint format: + +* China: `https://s3..amazonaws.com.cn` +* Other countries/regions: `https://s3..amazonaws.com` + +::: + + + + + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://oss-.aliyuncs.com \ + -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + + + + + +1. Install minIO. + + ```bash + helm upgrade --install minio oci://registry-1.docker.io/bitnamicharts/minio --set persistence.enabled=true,persistence.storageClass=csi-hostpath-sc,persistence.size=100Gi,defaultBuckets=backup + ``` + +2. Install CSI-S3. + + ```bash + helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + + helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ + --set secret.accessKey= \ + --set secret.secretKey= \ + --set storageClass.singleBucket=backup \ + --set secret.endpoint=http://minio.default.svc.cluster.local:9000 \ + -n kb-system + + # CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes + --set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' + ``` + + + + +You can configure a global backup storage to make this storage the default backup destination path of all new clusters. But currently, the global backup storage cannot be synchronized as the backup destination path of created clusters. + +Set the backup policy with the following command. + +```bash +kbcli kubeblocks config --set dataProtection.backupPVCName=kubeblocks-backup-data \ +--set dataProtection.backupPVCStorageClassName=csi-s3 -n kb-system + +# dataProtection.backupPVCName: PersistentVolumeClaim Name for backup storage +# dataProtection.backupPVCStorageClassName: StorageClass Name +# -n kb-system: namespace where KubeBlocks is installed +``` + +:::note + +* If there is no PVC, the system creates one automatically based on the configuration. +* It takes about 1 minute to make the configuration effective. + +::: + +## Create backup + +**Option 1. Manually Backup** + +1. Check whether the cluster is running. + + ```bash + kbcli cluster list mysql-cluster + ``` + +2. Create a backup for this cluster. + + ```bash + kbcli cluster backup mysql-cluster --type=datafile + ``` + +3. View the backup set. + + ```bash + kbcli cluster list-backups mysql-cluster + ``` + +**Option 2. Enable scheduled backup** + +```bash +kbcli cluster edit-backup-policy mysql-cluster-postgresql-backup-policy +> +spec: + ... + schedule: + baseBackup: + # UTC time zone, the example below means 2 a.m. every Monday + cronExpression: "0 18 * * 0" + # Enable this function + enable: true + # Select the basic backup type, available options: snapshot and snapshot + # This example selects datafile as the basic backup type + type: datafile +``` + +## Restore data from backup + +1. Restore data from the backup. + + ```bash + kbcli cluster restore new-mysql-cluster --backup backup-default-mysql-cluster-20230418124113 + ``` + +2. View this new cluster. + + ```bash + kbcli cluster list new-mysql-cluster + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/snapshot-backup-and-restore-for-mysql.md b/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/snapshot-backup-and-restore-for-mysql.md new file mode 100644 index 000000000..4808203e9 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mysql/backup-and-restore/snapshot-backup-and-restore-for-mysql.md @@ -0,0 +1,127 @@ +--- +title: Snapshot backup and restore for MySQL +description: Guide for backup and restore for MySQL +keywords: [mysql, snapshot, backup, restore] +sidebar_position: 2 +sidebar_label: Snapshot backup and restore +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Snapshot backup and restore for MySQL + +This section shows how to use `kbcli` to back up and restore a MySQL cluster. + +***Steps:*** + +1. Install KubeBlocks and the snapshot-controller add-on. + + ```bash + kbcli kubeblocks install --set snapshot-controller.enabled=true + ``` + + If you have installed KubeBlocks without enabling the snapshot-controller, run the command below. + + ```bash + kbcli kubeblocks upgrade --set snapshot-controller.enabled=true + ``` + + Since your `kubectl` is already connected to the cluster of cloud Kubernetes service, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your environment. + + Verify the installation with the following command. + + ```bash + kubectl get pod -n kb-system + ``` + + The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. + + ```bash + NAME READY STATUS RESTARTS AGE + kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m + kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m + ``` + + If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](./../../installation/enable-addons.md) to find the environment requirements and then enable the snapshot-controller add-on. + +2. Configure cloud managed Kubernetes environment to support the snapshot function. For ACK and GKE, the snapshot function is enabled by default, you can skip this step. + + + + + The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. + + Configure the storage class of the snapshot (the assigned EBS volume is gp3). + + ```yaml + kubectl create -f - < + + + + Configure the default volumesnapshot class. + + ```yaml + kubectl create -f - < + + +3. Create a snapshot backup. + + ```bash + kbcli cluster backup mysql-cluster + ``` + +4. Check the backup. + + ```bash + kbcli cluster list-backups + ``` + +5. Restore to a new cluster. + + Copy the backup name to the clipboard, and restore to the new cluster. + + :::note + + You do not need to specify other parameters for creating a cluster. The restoration automatically reads the parameters of the source cluster, including specification, disk size, etc., and creates a new MySQL cluster with the same specifications. + + ::: + + Execute the following command. + + ```bash + kbcli cluster restore mysql-new-from-snapshot --backup backup-default-mysql-cluster-20221124113440 + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/create-and-connect-a-mysql-cluster.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/create-and-connect-a-mysql-cluster.md index 381e6973c..3090c2899 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/create-and-connect-a-mysql-cluster.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/create-and-connect-a-mysql-cluster.md @@ -1,129 +1,207 @@ --- title: Create and connect to a MySQL Cluster description: How to create and connect to a MySQL cluster +keywords: [mysql, create a mysql cluster, connect to a mysql cluster] sidebar_position: 1 sidebar_label: Create and connect --- -# Create and connect to a MySQL Cluster +# Create and connect to a MySQL cluster -## Create a MySQL Cluster +This document shows how to create and connect to a MySQL cluster. + +## Create a MySQL cluster ### Before you start -* `kbcli`: Install `kbcli` on your host. Refer to [Install/Uninstall kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - 1. Run the command below to install `kbcli`. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - 2. Run the command below to check the version and verify whether `kbcli` is installed successfully. - ```bash - kbcli version - ``` -* KubeBlocks: Install KubeBlocks on your host. Refer to [Install/Uninstall kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - 1. Run the command below to install KubeBlocks. - ```bash - kbcli kubeblocks install - ``` - - :::note - - If you want to specify a namespace for KubeBlocks, use `--namespace` or the abbreviated `-n` to name your namespace and configure `--create-namespace` as `true` to create a namespace if it does not exist. For example, - ```bash - kbcli kubeblocks install -n kubeblocks --create-namespace=true - ``` - - ::: - - 2. Run the command below to verify whether KubeBlocks is installed successfully - ```bash - kubectl get pod - ``` - - ***Result*** - - Four pods starting with `kubeblocks` are displayed. For example, - ``` - NAME READY STATUS RESTARTS AGE - kubeblocks-7d4c6fd684-9hjh7 1/1 Running 0 3m33s - kubeblocks-grafana-b765d544f-wj6c6 3/3 Running 0 3m33s - kubeblocks-prometheus-alertmanager-7c558865f5-hsfn5 2/2 Running 0 3m33s - kubeblocks-prometheus-server-5c89c8bc89-mwrx7 2/2 Running 0 3m33s - ``` -* Run the command below to view all the database types available for creating a cluster. +* [Install kbcli](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Make sure the ApeCloud MySQL addon is installed with `kbcli addon list`. + ```bash - kbcli clusterdefinition list + kbcli addon list + > + NAME TYPE STATUS EXTRAS AUTO-INSTALL INSTALLABLE-SELECTOR + ... + apecloud-mysql Helm Enabled true + ... ``` -### Steps +* View all the database types and versions available for creating a cluster. -1. Run the command below to list all the available kernel versions and choose the one that you need. - ```bash - kbcli clusterversion list - ``` + ```bash + kbcli clusterversion list + ``` -2. Run the command below to create a MySQL cluster. - ```bash - kbcli cluster create mysql-cluster --cluster-definition='apecloud-mysql' - ``` - ***Result*** +### (Recommended) Create a cluster on a tainted node - * A cluster then is created in the default namespace. You can specify a namespace for your cluster by using `--namespace` or the abbreviated `-n` option. For example, +In actual scenarios, you are recommended to create a cluster on nodes with taints and customized specifications. - ```bash - kubectl create namespace demo +1. Taint your node. - kbcli cluster create -n demo --cluster-definition='apecloud-mysql' - ``` - * A cluster is created with built-in toleration which tolerates the node with the `kb-data=true:NoSchedule` taint. - * A cluster is created with built-in node affinity which first deploys the node with the `kb-data:true` label. - * For configuring pod affinity for a cluster, refer to [Configure pod affinity for database cluster](../../resource-scheduling/resource-scheduling.md). - - To create a cluster with specified parameters, follow the steps below, and you have three options. + :::note + + If you have already some tainted nodes, you can skip this step. + + ::: + + 1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` - **Option 1.** (**Recommended**) Use --set option - - Add the `--set` option when creating a cluster. For example, - ```bash - kbcli cluster create mysql-cluster --cluster-definition apecloud-mysql --set cpu=1000m,memory=1Gi,storage=10Gi,replicas=3 - ``` + 2. Place taints on the selected nodes. - **Option 2.** Change YAML file configurations + ```bash + kubectl taint nodes =true:NoSchedule + ``` + +2. Create a MySQL cluster. + + The cluster creation command is simply `kbcli cluster create`. Use tolerances to deploy it on the tainted node. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + + To create a cluster with a specified class, you can use `--set` flag and specify your requirement. + + 1. View and select a class for this cluster. + + ```bash + kbcli class list --cluster-definition apecloud-mysql + ``` + + :::note + + If there is no suitable class listed, you can [customize your own class](./../cluster-type/customize-class-type.md) template and apply the class here. + + Creating clusters that do not meet the constraints is invalid and the system creates the cluster with the minimum CPU value specified. + + ::: + + 2. Create a cluster with a specified class and add all taints on the current node in the `--toleration` flag to tolerate them. + + ```bash + kbcli cluster create mysql-cluster --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --cluster-definition=apecloud-mysql --set class=general-2c2g,storageClass= --namespace + ``` + + Or change the corresponding parameters in the YAML file. - Change the corresponding parameters in the YAML file. ```bash - kbcli cluster create mysql-cluster --cluster-definition="apecloud-mysql" --set-file -< --set-file -< accessModes: - ReadWriteOnce resources: requests: - storage: 20Gi + cpu: 2000m + memory: 2Gi + storage: 10Gi EOF ``` -### kbcli cluster create options description +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended to turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose a database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this function is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want to set node labels, you can follow the example format:
`kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, and each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a MySQL cluster with default settings. + +```bash +kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' +``` + +### Create a cluster on a node without taints + +The cluster creation command is simply `kbcli cluster create`. Further, you are recommended to create a cluster with specified class and customize your cluster settings as demanded. + +To create a cluster with specified class, you can use `--set` flag and specify your requirement. + +1. View and select a class for this cluster. + + ```bash + kbcli class list --cluster-definition apecloud-mysql + ``` + + :::note + + If there is no suitable class listed, you can [customize your own class](./../cluster-type/customize-class-type.md) template and apply the class here. + + Creating clusters that do not meet the constraints is invalid and the system creates a cluster with the minimum CPU value specified. -| Option | Description | -| :-- | :-- | -| `--cluster-definition` | It specifies the cluster definition. Run `kbcli cd list` to show all available cluster definitions. | -| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is used by default. | -| `--enable-all-logs` | It enables you to view all application logs. When this option is enabled, enabledLogs of component level will be ignored. This option is set as true by default. | -| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | -| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | -| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
```kbcli cluster create --cluster-definition='apecloud-mysql' --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'``` | -| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=3,storage=10Gi`. | -| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | -| `--termination-policy` | It specifies the termination policy of the cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + ::: + +2. Create a cluster with a specified class and add all taints on the current node in the `--toleration` flag to tolerate them. + + ```bash + kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql --set class=general-2c2g,storageClass= --namespace + ``` + + ***Result*** + + A cluster is created in the namespace `default` with the specified class. + +Or change the corresponding parameters in the YAML file. + +```bash +kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql --set storageClass= --namespace --set-file -<`kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a MySQL cluster with default settings. + +```bash +kbcli cluster create mysql-cluster --cluster-definition=apecloud-mysql +``` ## Connect to a MySQL Cluster -Run the command below to connect to a cluster. For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). ```bash -kbcli cluster connect mysql-cluster -``` \ No newline at end of file +kbcli cluster connect --namespace +``` + +For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/delete-mysql-cluster.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/delete-mysql-cluster.md index 7e5a3ec54..db486e56e 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/delete-mysql-cluster.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/delete-mysql-cluster.md @@ -1,6 +1,7 @@ --- title: Delete a MySQL Cluster description: How to delete a MySQL Cluster +keywords: [mysql, delete a cluster] sidebar_position: 6 sidebar_label: Delete protection --- @@ -15,52 +16,26 @@ The termination policy determines how you delete a cluster. ::: -| **terminationPolicy** | **Deleting Operation** | -|:-- | :-- | -| `DoNotTerminate` | `DoNotTerminate` blocks delete operation. | -| `Halt` | `Halt` deletes workload resources such as statefulset, deployment workloads but keep PVCs. | -| `Delete ` | `Delete` delete workload resources and PVCs but keep backups. | -| `WipeOut` | `WipeOut` deletes workload resources, PVCs and all relevant resources included backups. | +| **terminationPolicy** | **Deleting Operation** | +|:----------------------|:-------------------------------------------------| +| `DoNotTerminate` | `DoNotTerminate` blocks delete operation. | +| `Halt` | `Halt` deletes workload resources such as statefulset, deployment workloads but keep PVCs. | +| `Delete` | `Delete` deletes workload resources and PVCs but keep backups. | +| `WipeOut` | `WipeOut` deletes workload resources, PVCs and all relevant resources included backups. | To check the termination policy, execute the following command. -```bash -kbcli cluster list -``` - -***Example*** - ```bash kbcli cluster list mysql-cluster > -NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME -mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Feb 06,2023 18:27 UTC+0800 +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Feb 06,2023 18:27 UTC+0800 ``` -## Option 1. Use kbcli +## Step -Configure the cluster name and run the command below to delete the specified cluster. - -```bash -kbcli cluster delete -``` - -***Example*** +Run the command below to delete a specified cluster. ```bash kbcli cluster delete mysql-cluster ``` - -## Option 2. Use kubectl - -Configure the cluster name and run the command below to delete the specified cluster. - -```bash -kubectl delete cluster -``` - -***Example*** - -```bash -kubectl delete cluster mysql-cluster -``` diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/expand-volume.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/expand-volume.md index d920a0e79..cebfc2339 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/expand-volume.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/expand-volume.md @@ -6,6 +6,7 @@ sidebar_label: Expand volume --- # Expand volume + You can expand the storage volume size of each pod. :::note @@ -16,12 +17,7 @@ Volume expansion triggers pod restart, all pods restart in the order of learner ## Before you start -Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. -```bash -kbcli cluster list -``` - -***Example*** +Check whether the cluster status is `Running`. Otherwise, the following operations may fail. ```bash kbcli cluster list mysql-cluster @@ -29,63 +25,89 @@ kbcli cluster list mysql-cluster NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Jan 29,2023 14:29 UTC+0800 ``` - -## Option 1. Use kbcli -Configure the values of `--component-names`, `--volume-claim-template-names`, and `--storage`, and run the command below to expand the volume. -```bash -kbcli cluster volume-expand mysql-cluster --component-names="mysql" \ ---volume-claim-template-names="data" --storage="2Gi" -``` +## Steps -- `--component-names` describes the component name for volume expansion. -- `--volume-claim-template-names` describes the VolumeClaimTemplate names in components. -- `--storage` describes the volume storage size. - -## Option 2. Create an OpsRequest +1. Change configuration. There are 3 ways to apply volume expansion. -Run the command below to expand the volume of a cluster. -```bash -kubectl apply -f - < + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete VolumeExpanding Jan 29,2023 14:35 UTC+0800 + ``` + + * STATUS=VolumeExpanding: it means the volume expansion is in progress. + * STATUS=Running: it means the volume expansion operation has been applied. + +3. Check whether the corresponding resources change. -## Option 3. Change the YAML file of the cluster - -Change the value of `spec.components.volumeClaimTemplates.spec.resources` in the cluster YAML file. `spec.components.volumeClaimTemplates.spec.resources` is the storage resource information of the pod and changing this value triggers the volume expansion of a cluster. - -```yaml -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - name: mysql-cluster - namespace: default -spec: - clusterDefinitionRef: apecloud-mysql - clusterVersionRef: ac-mysql-8.0.30 - componentSpecs: - - name: mysql - componentDefRef: mysql - replicas: 1 - volumeClaimTemplates: - - name: data - spec: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 1Gi # Change the volume storage size. - terminationPolicy: Halt -``` \ No newline at end of file + ```bash + kbcli cluster describe mysql-cluster + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/handle-an-exception.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/handle-an-exception.md index 30ade28e1..59b2e88e5 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/handle-an-exception.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/handle-an-exception.md @@ -1,37 +1,36 @@ --- title: Handle an exception description: How to handle an exception in a MySQL cluster +keywords: [mysql, exception] sidebar_position: 7 sidebar_label: Handle an exception --- # Handle an exception -When there is an exception during your operation, you can perform the following procedure to solve it. -## Steps +When an exception occurs during your operation, you can perform the following steps to solve it. -1. Check the cluster status. Fill in the name of the cluster you want to check and run the command below. - ```bash - kbcli cluster list - ``` +## Steps - ***Example*** +1. Check the cluster status. ```bash kbcli cluster list mysql-cluster ``` + 2. Handle the exception according to the status information. | **Status** | **Information** | | :--- | :--- | - | Abnormal | The cluster can be accessed but exceptions occur in some pods. This might be a mediate status of the operation process and the system recovers automatically without executing any extra operation. Wait until the cluster status is Running. | + | Abnormal | The cluster can be accessed but exceptions occur in some pods. This might be a mediate status of the operation process and the system recovers automatically without executing any extra operation. Wait until the cluster status changes to `Running`. | | ConditionsError | The cluster is normal but an exception occurs to the condition. It might be caused by configuration loss or exception, which further leads to operation failure. Manual recovery is required. | | Failed | The cluster cannot be accessed. Check the `status.message` string and get the exception reason. Then manually recover it according to the hints. | - + You can check the cluster's status for more information. ## Fallback strategies -If the above operation can not solve the problem, try the following steps: - - Restart this cluster. If the restart fails, you can delete the pod manually. - - Roll the cluster status back to the status before changes. \ No newline at end of file +If the above operations can not solve the problem, try the following steps: + +- Restart this cluster. If the restart fails, you can delete the pod manually. +- Roll the cluster status back to the status before changes. diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/restart-mysql-cluster.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/restart-mysql-cluster.md index 56a129136..ddd6d3ede 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/restart-mysql-cluster.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/restart-mysql-cluster.md @@ -1,11 +1,13 @@ --- title: Restart MySQL cluster description: How to restart a MySQL cluster +keywords: [mysql, restart, restart a cluster] sidebar_position: 4 sidebar_label: Restart --- # Restart MySQL cluster + You can restart all pods of the cluster. When an exception occurs in a database, you can try to restart it. :::note @@ -17,21 +19,25 @@ All pods restart in the order of learner -> follower -> leader and the leader ma ## Steps 1. Restart a cluster. - You can use `kbcli` or create an OpsRequest to restart a cluster. + + You can use `kbcli` or create an OpsRequest to restart a cluster. **Option 1.** (**Recommended**) Use kbcli - - Configure the values of `component-names` and `ttlSecondsAfterSucceed` and run the command below to restart a specified cluster. + + Configure the values of `components` and `ttlSecondsAfterSucceed` and run the command below to restart a specified cluster. + ```bash - kbcli cluster restart NAME --component-names="mysql" \ + kbcli cluster restart --components="mysql" \ --ttlSecondsAfterSucceed=30 ``` - - `component-names` describes the component name that needs to be restarted. + + - `components` describes the component name that needs to be restarted. - `ttlSecondsAfterSucceed` describes the time to live of an OpsRequest job after the restarting succeeds. **Option 2.** Create an OpsRequest - Run the command below to apply the restarting to a cluster. + Run the command below to restart a cluster. + ```bash kubectl apply -f - < follower -> leader and the leader ma - componentName: mysql EOF ``` -2. Validate the restarting. - Run the command below to check the cluster status to check the restarting status. + +2. Check the cluster status to validate the restarting. + ```bash - kbcli cluster list + kbcli cluster list mysql-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Jan 29,2023 14:29 UTC+0800 ``` - - STATUS=Updating: it means the cluster is restarting. - - STATUS=Running: it means the cluster is restarted. - - ***Example*** - - ```bash - kbcli cluster list mysql-cluster - > - NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME - mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Jan 29,2023 14:29 UTC+0800 - ``` + + - STATUS=Restarting: it means the cluster restart is in progress. + - STATUS=Running: it means the cluster has been restarted. diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-management/scale-for-apecloud-mysql.md b/docs/user_docs/kubeblocks-for-mysql/cluster-management/scale-for-apecloud-mysql.md index d0f765669..d570ca5c1 100644 --- a/docs/user_docs/kubeblocks-for-mysql/cluster-management/scale-for-apecloud-mysql.md +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-management/scale-for-apecloud-mysql.md @@ -1,15 +1,18 @@ --- -title: Scale for ApeCloud MySQL +title: Scale for a MySQL cluster description: How to scale a MySQL cluster, horizontal scaling, vertical scaling +keywords: [mysql, horizontal scaling, vertical scaling] sidebar_position: 2 sidebar_label: Scale --- -# Scale for ApeCloud MySQL -You can scale ApeCloud MySQL DB instances in two ways, horizontal scaling and vertical scaling. +# Scale for an ApeCloud MySQL cluster + +You can scale a MySQL cluster in two ways, vertical scaling and horizontal scaling. ## Vertical scaling -You can vertically scale a cluster by changing resource requirements and limits (CPU and storage). For example, if you need to change the resource demand from 1C2G to 2C4G, vertical scaling is what you need. + +You can vertically scale a cluster by changing resource requirements and limits (CPU and storage). For example, if you need to change the resource class from 1C2G to 2C4G, vertical scaling is what you need. :::note @@ -19,12 +22,7 @@ During the vertical scaling process, all pods restart in the order of learner -> ### Before you start -Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. -```bash -kbcli cluster list -``` - -***Example*** +Check whether the cluster status is `Running`. Otherwise, the following operations may fail. ```bash kbcli cluster list mysql-cluster @@ -32,28 +30,51 @@ kbcli cluster list mysql-cluster NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Running Jan 29,2023 14:29 UTC+0800 ``` + ### Steps 1. Change configuration. There are 3 ways to apply vertical scaling. - + **Option 1.** (**Recommended**) Use kbcli - - Configure the parameters `--component-names`, `--memory`, and `--cpu` and run the command. - - ***Example*** - + + Configure the parameters `--components`, `--memory`, and `--cpu` and run the command. + ```bash kbcli cluster vscale mysql-cluster \ - --component-names="mysql" \ + --components="mysql" \ --memory="4Gi" --cpu="2" \ ``` - - `--component-names` describes the component name ready for vertical scaling. + + - `--components` describes the component name ready for vertical scaling. - `--memory` describes the requested and limited size of the component memory. - `--cpu` describes the requested and limited size of the component CPU. + + You can also vertically scale a cluster with specified class type. + + 1. List all classes with `kbcli class list` command and choose the one you need, or check [class type](./../cluster-type/cluster-types.md) document for reference. + + ```bash + kbcli class list --cluster-definition apecloud-mysql + ``` + + :::note + If there is no suitable class listed, you can [customize your own class](./../cluster-type/customize-class-type.md) template and apply the class here. + + Creating clusters that does not meet the constraints is invalid and the system creates a cluster with the minimum CPU value specified. + + ::: + + 2. Use `--set` option with `kbcli cluster vscale` command to apply the vertical scaling. + + ```bash + kbcli cluster vscale mysql-clsuter --components="mysql" --cluster-definition apecloud-mysql --set class=general-2c2g + ``` + **Option 2.** Create an OpsRequest - Run the command below to apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + Apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + ```bash kubectl apply -f - < + kbcli cluster list mysql-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete VerticalScaling Jan 29,2023 14:29 UTC+0800 ``` - ***Example*** + - STATUS=VerticalScaling: it means the vertical scaling is in progress. + - STATUS=Running: it means the vertical scaling operation has been applied. + - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be that the number of the normal instances is less than that of the total instance or the leader instance is running properly while others are abnormal. + > To solve the problem, you can manually check whether this error is caused by insufficient resources. Then if AutoScaling is supported by the Kubernetes cluster, the system recovers when there are enough resources. Otherwise, you can create enough resources and troubleshoot with `kubectl describe` command. + +3. Check whether the corresponding resources change. ```bash - kbcli cluster list mysql-cluster - > - NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME - mysql-cluster default apecloud-mysql ac-mysql-8.0.30 Delete Updating Jan 29,2023 14:29 UTC+0800 + kbcli cluster describe mysql-cluster ``` - - STATUS=Running: it means the vertical scaling operation is applied. - - STATUS=Updating: it means the vertical scaling is in progress. - - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be the normal instances number is less than the total instance number or the leader instance is running properly while others are abnormal. - > To solve the problem, you can check manually to see whether resources are sufficient. If AutoScaling is supported, the system recovers when there are enough resources, otherwise, you can create enough resources and check the result with kubectl describe command. ## Horizontal scaling -Horizontal scaling changes the amount of pods. For example, you can apply horizontal scaling to scale up from three pods to five pods. The scaling process includes the backup and restoration of data. -### Before you start - -* Refer to [Backup and restore for MySQL](./../backup-and-restore/backup-and-restore-for-mysql-standalone.md) to make sure the EKS environment is configured properly since the horizontal scaling relies on the backup function. -* Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. +Horizontal scaling changes the amount of pods. For example, you can apply horizontal scaling to scale pods up from three to five. The scaling process includes the backup and restoration of data. - ```bash - kbcli cluster list - ``` +### Before you start - ***Example*** +- Refer to [Backup and restore for MySQL](./../backup-and-restore/snapshot-backup-and-restore-for-mysql.md) to make sure the EKS environment is configured properly since the horizontal scaling relies on the backup function. +- Check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. ```bash kbcli cluster list mysql-cluster @@ -154,23 +171,22 @@ Horizontal scaling changes the amount of pods. For example, you can apply horizo ### Steps 1. Change configuration. There are 3 ways to apply horizontal scaling. - + **Option 1.** (**Recommended**) Use kbcli - - Configure the parameters `--component-names` and `--replicas`, and run the command. - ***Example*** + Configure the parameters `--components` and `--replicas`, and run the command. ```bash kbcli cluster hscale mysql-cluster \ - --component-names="mysql" --replicas=3 + --components="mysql" --replicas=3 ``` - - `--component-names` describes the component name ready for vertical scaling. + + - `--components` describes the component name ready for horizontal scaling. - `--replicas` describes the replicas with the specified components. **Option 2.** Create an OpsRequest - Run the command below to apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + Apply an OpsRequest to a specified cluster. Configure the parameters according to your needs. ```bash kubectl apply -f - < -``` - -***Example*** +Configure the name of your cluster and run the command below to stop this cluster. ```bash kbcli cluster stop mysql-cluster @@ -28,6 +23,7 @@ kbcli cluster stop mysql-cluster ### Option 2. Create an OpsRequest Run the command below to stop a cluster. + ```bash kubectl apply -f - < -``` - -***Example*** +Configure the name of your cluster and run the command below to start this cluster. ```bash kbcli cluster start mysql-cluster diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-type/_category_.yml b/docs/user_docs/kubeblocks-for-mysql/cluster-type/_category_.yml new file mode 100644 index 000000000..b19dea834 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-type/_category_.yml @@ -0,0 +1,4 @@ +position: 3 +label: Configuration +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-type/cluster-types.md b/docs/user_docs/kubeblocks-for-mysql/cluster-type/cluster-types.md new file mode 100644 index 000000000..36ee0ef10 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-type/cluster-types.md @@ -0,0 +1,47 @@ +--- +title: Class types +description: Cluster Class of ApeCloud MySQL +keywords: [mysql, class type] +sidebar_position: 1 +sidebar_label: Class types +--- + +# Cluster Class of ApeCloud MySQL + +ApeCloud for MySQL predefines cluster class for different CPU, memory, and storage requirements for you to choose. It is designed to offer convenience and also set a constraints on the resources applied to the cluster to avoid lowering resource utilization and system stability by unreasonable configuration. + +You can apply the cluster class when creating or vertically scaling a cluster. + +📎 Table 1. General-purpose class type + +| COMPONENT | CLASS | CPU | MEMORY | +|-----------|:-----------------|------|--------| +| mysql | general-0.5c0.5g | 500m | 512Mi | +| mysql | general-1c1g | 1 | 1Gi | +| mysql | general-2c2g | 2 | 2Gi | +| mysql | general-2c4g | 2 | 4Gi | +| mysql | general-2c8g | 2 | 8Gi | +| mysql | general-4c16g | 4 | 16Gi | +| mysql | general-8c32g | 8 | 32Gi | +| mysql | general-16c64g | 16 | 64Gi | +| mysql | general-32c128g | 32 | 128Gi | +| mysql | general-64c256g | 64 | 256Gi | +| mysql | general-128c512g | 128 | 512Gi | + +📎 Table 2. Memory-optimized class type + +| COMPONENT | CLASS | CPU | MEMORY | +|-----------|:------------|-----|--------| +| mysql | mo-2c16g | 2 | 16Gi | +| mysql | mo-2c32g | 2 | 32Gi | +| mysql | mo-4c32g | 4 | 32Gi | +| mysql | mo-4c64g | 4 | 64Gi | +| mysql | mo-8c64g | 8 | 64Gi | +| mysql | mo-8c128g | 8 | 128Gi | +| mysql | mo-12c96g | 12 | 96Gi | +| mysql | mo-16c256g | 16 | 256Gi | +| mysql | mo-24c192g | 24 | 192Gi | +| mysql | mo-32c512g | 32 | 512Gi | +| mysql | mo-48c384g | 48 | 384Gi | +| mysql | mo-48c768g | 48 | 768Gi | +| mysql | mo-64c1024g | 64 | 1Ti | diff --git a/docs/user_docs/kubeblocks-for-mysql/cluster-type/customize-class-type.md b/docs/user_docs/kubeblocks-for-mysql/cluster-type/customize-class-type.md new file mode 100644 index 000000000..66e82083d --- /dev/null +++ b/docs/user_docs/kubeblocks-for-mysql/cluster-type/customize-class-type.md @@ -0,0 +1,71 @@ +--- +title: Customize class types +description: How to customize class types for a MySQL cluster +keywords: [mysql, class type, customize class types] +sidebar_position: 2 +sidebar_label: Customize class types +--- + +# Customize class types + +You can use KubeBlocks to create your own class types. + +## Create one class + +***Steps:*** + +1. Use `kbcli class create` to create customized class types. + + *Example:* + + ```bash + kbcli class create custom-1c1g --cluster-definition apecloud-mysql --type mysql --constraint kb-resource-constraint-general --cpu 1 --memory 1Gi + ``` + +2. Check whether the class is created with `kbcli class list` command. + + ```bash + kbcli class list --cluster-definition apecloud-mysql + ``` + + And you can see a class named `custom-1c1g` is listed. + +## Create many classes at a time + +***Steps:*** + +1. If you want to create many classes at a time, you can use yaml file. + For example, you can create the file named `/tmp/class.yaml`. + + ```bash + - resourceConstraintRef: kb-resource-constraint-general + # class template, you can declare variables and set default values here + template: | + cpu: "{{ or .cpu 1 }}" + memory: "{{ or .memory 4 }}Gi" + # template variables used to define classes + vars: [cpu, memory] + series: + - # class naming template, you can reference variables in class template + # it's also ok to define static class name in the following class definitions + namingTemplate: "custom-{{ .cpu }}c{{ .memory }}g" + + # class definitions, we support two kinds of class definitions: + # 1. define values for template variables and the full class definition will be dynamically rendered + # 2. statically define the complete class + classes: + - args: [1, 4, 1, 1] + - args: [1, 6, 1, 1] + ``` + +2. Apply the yaml file to create classes. + + ```bash + kbcli class create --cluster-definition apecloud-mysql --type mysql --file /tmp/class.yaml + ``` + +3. Check whether these classes are created with `kbcli class list` command. + + ```bash + kbcli class list --cluster-definition apecloud-mysql + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/configuration/_category_.yml b/docs/user_docs/kubeblocks-for-mysql/configuration/_category_.yml index b19dea834..cf437673c 100644 --- a/docs/user_docs/kubeblocks-for-mysql/configuration/_category_.yml +++ b/docs/user_docs/kubeblocks-for-mysql/configuration/_category_.yml @@ -1,4 +1,4 @@ -position: 3 +position: 4 label: Configuration collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mysql/configuration/configuration.md b/docs/user_docs/kubeblocks-for-mysql/configuration/configuration.md index 70ea81629..a34469fa0 100644 --- a/docs/user_docs/kubeblocks-for-mysql/configuration/configuration.md +++ b/docs/user_docs/kubeblocks-for-mysql/configuration/configuration.md @@ -1,73 +1,80 @@ --- title: Configure cluster parameters description: Configure cluster parameters +keywords: [mysql, parameter, configuration, reconfiguration] sidebar_position: 1 --- # Configure cluster parameters -The KubeBlocks configuration function provides a set of consistent default configuration generation strategies for all the databases running on KubeBlocks and also provides a unified parameter change interface to facilitate managing parameter reconfiguration, searching the parameter user guide, and validating parameter effectiveness. +The KubeBlocks configuration function provides a set of consistent default configuration generation strategies for all the databases running on KubeBlocks and also provides a unified parameter configuration interface to facilitate managing parameter reconfiguration, searching the parameter user guide, and validating parameter effectiveness. ## Before you start -1. Install KubeBlocks. For details, refer to [Install KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md). -2. Create a MySQL standalone and wait until the cluster status is Running. +1. [Install KubeBlocks](./../../installation/install-kubeblocks.md). +2. [Create a MySQL cluster](./../cluster-management/create-and-connect-a-mysql-cluster.md#create-a-mysql-cluster) and wait until the cluster status is Running. -## View the parameter information +## View parameter information + +View the current configuration file of a cluster. + +```bash +kbcli cluster describe-config mysql-cluster +``` + +From the meta information, the cluster `mysql-cluster` has a configuration file named `my.cnf`. + +You can also view the details of this configuration file and parameters. + +* View the details of the current configuration file. -1. Run the command below to search for parameter information. - ```bash - kbcli cluster explain-config mysql-cluster |head -n 20 - > - template meta: - ConfigSpec: mysql-consensusset-config ComponentName: mysql ClusterName: mysql-cluster - - Parameter Explain: - +----------------------------------------------------------+--------------------------------------------+--------+---------+---------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | PARAMETER NAME | ALLOWED VALUES | SCOPE | DYNAMIC | TYPE | DESCRIPTION | - +---------------------------------------------------------- +--------------------------------------------+--------+---------+---------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ - | activate_all_roles_on_login | "0","1","OFF", "ON" | Global | false | string | Automatically set all granted roles as active after the user has authenticated successfully. | - | allow-suspicious-udfs | "0","1","OFF","ON" | Global | false | string | Controls whether user-defined functions that have only an xxx symbol for the main function can be loaded | - | auto_generate_certs | "0","1","OFF","ON" | Global | false | string | Controls whether the server autogenerates SSL key and certificate files in the data directory, if they do not already exist. | - | auto_increment_increment | [1-65535] | Global | false | integer | Intended for use with master-to-master replication, and can be used to control the operation of AUTO_INCREMENT columns | - | auto_increment_offset | [1-65535] | Global | false | integer | Determines the starting point for the AUTO_INCREMENT column value | - | autocommit | "0","1","OFF","ON" | Global | false | string | Sets the autocommit mode | - | automatic_sp_privileges | "0","1","OFF","ON" | Global | false | string | When this variable has a value of 1 (the default), the server automatically grants the EXECUTE and ALTER ROUTINE privileges to the creator of a stored routine, if the user cannot already execute and alter or drop the routine. | - | avoid_temporal_upgrade | "0","1","OFF","ON" | Global | false | string | This variable controls whether ALTER TABLE implicitly upgrades temporal columns found to be in pre-5.6.4 format. | - | back_log | [1-65535] | Global | false | integer | The number of outstanding connection requests MySQL can have | - | basedir | | Global | false | string | The MySQL installation base directory. | - | big_tables | "0","1","OFF","ON" | Global | false | string | | - | bind_address | | Global | false | string | | - | binlog_cache_size | [4096-18446744073709548000] | Global | false | integer | The size of the cache to hold the SQL statements for the binary log during a transaction. + kbcli cluster describe-config mysql-cluster --show-detail ``` -2. View the user guide of a parameter. - ```bash - kbcli cluster explain-config mysql-cluster --param=innodb_buffer_pool_size - template meta: - ConfigSpec: mysql-consensusset-config ComponentName: mysql ClusterName: mysql-cluster - - Configure Constraint: - Parameter Name: innodb_buffer_pool_size - Allowed Values: [5242880-18446744073709552000] - Scope: Global - Dynamic: false - Type: integer - Description: The size in bytes of the memory buffer innodb uses to cache data and indexes of its tables - ``` - * Allowed Values: It defines the valid value of this parameter. - * Dynamic: The value of `Dynamic` in `Configure Constraint` defines how the parameter reconfiguration takes effect. As mentioned in [How KubeBlocks configuration works](#how-kubeblocks-configuration-works), there are two different reconfiguration strategies based on the effectiveness type of changed parameters, i.e. **dynamic** and **static**. - * When `Dynamic` is `true`, it means the effectiveness type of parameters is **dynamic** and you can follow the instructions in [Reconfigure dynamic parameters](#reconfigure-dynamic-parameters). - * When `Dynamic` is `false`, it means the effectiveness type of parameters is **static** and you can follow the instructions in [Reconfigure static parameters](#reconfigure-static-parameters). - * Description: It describes the parameter definition. +* View the parameter description. + + ```bash + kbcli cluster explain-config mysql-cluster |head -n 20 + ``` + +* View the user guide of a specified parameter. + + ```bash + kbcli cluster explain-config mysql-cluster --param=innodb_buffer_pool_size + ``` + +
+ + Output + + ```bash + template meta: + ConfigSpec: mysql-consensusset-config ComponentName: mysql ClusterName: mysql-cluster + + Configure Constraint: + Parameter Name: innodb_buffer_pool_size + Allowed Values: [5242880-18446744073709552000] + Scope: Global + Dynamic: false + Type: integer + Description: The size in bytes of the memory buffer innodb uses to cache data and indexes of its tables + ``` + +
+ * Allowed Values: It defines the valid value range of this parameter. + * Dynamic: The value of `Dynamic` in `Configure Constraint` defines how the parameter reconfiguration takes effect. There are two different reconfiguration strategies based on the effectiveness type of modified parameters, i.e. **dynamic** and **static**. + * When `Dynamic` is `true`, it means the effectiveness type of parameters is **dynamic** and can be updated online. Follow the instructions in [Reconfigure dynamic parameters](#reconfigure-dynamic-parameters). + * When `Dynamic` is `false`, it means the effectiveness type of parameters is **static** and a pod restarting is required to make reconfiguration effective. Follow the instructions in [Reconfigure static parameters](#reconfigure-static-parameters). + * Description: It describes the parameter definition. ## Reconfigure dynamic parameters -Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an example. +The example below reconfigures `max_connection` and `innodb_buffer_pool_size`. + +1. View the current values of `max_connection` and `innodb_buffer_pool_size`. -1. Run the command to view the current values of `max_connection` and `beb_buffer_pool_size`. ```bash kbcli cluster connect mysql-cluster ``` @@ -95,13 +102,14 @@ Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an exa ``` 2. Adjust the values of `max_connections` and `innodb_buffer_pool_size`. + ```bash - kbcli cluster configure rose15 --set=max_connections=600,innodb_buffer_pool_size=512M + kbcli cluster configure mysql-cluster --set=max_connections=600,innodb_buffer_pool_size=512M ``` - + :::note - Make sure the value you set is within the Allowed Values of this parameters. If you set a value that does not meet the value range, the system prompts an error. For example, + Make sure the value you set is within the Allowed Values of this parameter. If you set a value that does not meet the value range, the system prompts an error. For example, ```bash kbcli cluster configure mysql-cluster --set=max_connections=200,innodb_buffer_pool_size=2097152 @@ -114,11 +122,18 @@ Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an exa ::: 3. Search the status of the parameter reconfiguration. - `Status.Progress` shows the overall status of the parameter change and `Conditions` show the details. + + `Status.Progress` shows the overall status of the parameter reconfiguration and `Conditions` show the details. + + ```bash + kbcli cluster describe-ops mysql-cluster-reconfiguring-z2wvn -n default + ``` + +
+ + Output ```bash - kbcli cluster describe-ops mysql-cluster-reconfiguring-z2wvn - > Spec: Name: mysql-cluster-reconfiguring-z2wvn NameSpace: default Cluster: mysql-cluster Type: Reconfiguring @@ -130,7 +145,7 @@ Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an exa Completion Time: Mar 13,2023 02:55 UTC+0800 Duration: 1s Status: Succeed - Progress: -/- + Progress: 1/1 Conditions: LAST-TRANSITION-TIME TYPE REASON STATUS MESSAGE @@ -142,8 +157,11 @@ Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an exa Mar 13,2023 02:55 UTC+0800 Succeed OpsRequestProcessedSuccessfully True Successfully processed the OpsRequest: mysql-cluster-reconfiguring-z2wvn in Cluster: mysql-cluster ``` -4. Verify whether the parameters are modified. - The whole searching process has a 30-second delay since it takes some time for kubelete to synchronize changes to the volume of the pod. +
+ +4. Connect to the database to verify whether the parameters are modified. + + The whole searching process has a 30-second delay since it takes some time for kubelet to synchronize modifications to the volume of the pod. ```bash kbcli cluster connect mysql-cluster @@ -173,22 +191,12 @@ Here we take reconfiguring `max_connection` and `beb_buffer_pool_size` as an exa ## Reconfigure static parameters -Static parameter reconfiguring requires restarting the pod. Here we take reconfiguring `ngram_token_size` as an example. +Static parameter reconfiguring requires restarting the pod. The following example reconfigures `ngram_token_size`. 1. Search the current value of `ngram_token_size` and the default value is 2. - ```bash - kbcli cluster explain-config mysql-cluster --param=ngram_token_size - > - template meta: - ConfigSpec: mysql-consensusset-config ComponentName: mysql ClusterName: mysql-cluster - - Configure Constraint: - Parameter Name: ngram_token_size - Allowed Values: [1-10] - Scope: Global - Dynamic: false - Type: integer - Description: Defines the n-gram token size for the n-gram full-text parser. + + ```bash + kbcli cluster explain-config mysql-cluster --param=ngram_token_size ``` ```bash @@ -207,24 +215,30 @@ Static parameter reconfiguring requires restarting the pod. Here we take reconfi ``` 2. Adjust the value of `ngram_token_size`. + ```bash kbcli cluster configure mysql-cluster --set=ngram_token_size=6 - > - Will updated configure file meta: - TemplateName: mysql-consensusset-config ConfigureFile: my.cnf ComponentName: mysql ClusterName: mysql-cluster - OpsRequest mysql-cluster-reconfiguring-nrnpf created ``` - + :::note - Make sure the value you set is within the Allowed Values of this parameters. Otherwise, the configuration may fail. + Make sure the value you set is within the Allowed Values of this parameter. Otherwise, the reconfiguration may fail. ::: -3. Watch the progress of searching parameter reconfiguration and pay attention to the output of `Status.Progress` and `Status.Status`. +3. View the status of the parameter reconfiguration. + + `Status.Progress` and `Status.Status` shows the overall status of the parameter reconfiguration and Conditions show the details. + + When the `Status.Status` shows `Succeed`, the reconfiguration is completed. + +
+ + Output + ```bash # In progress - kbcli cluster describe-ops mysql-cluster-reconfiguring-nrnpf + kbcli cluster describe-ops mysql-cluster-reconfiguring-nrnpf -n default > Spec: Name: mysql-cluster-reconfiguring-nrnpf NameSpace: default Cluster: mysql-cluster Type: Reconfiguring @@ -241,8 +255,8 @@ Static parameter reconfiguring requires restarting the pod. Here we take reconfi ``` ```bash - # Parameter change is completed - kbcli cluster describe-ops mysql-cluster-reconfiguring-nrnpf + # Parameter reconfiguration is completed + kbcli cluster describe-ops mysql-cluster-reconfiguring-nrnpf -n default > Spec: Name: mysql-cluster-reconfiguring-nrnpf NameSpace: default Cluster: mysql-cluster Type: Reconfiguring @@ -259,17 +273,14 @@ Static parameter reconfiguring requires restarting the pod. Here we take reconfi OBJECT-KEY STATUS DURATION MESSAGE ``` -4. After the reconfiguration is completed, connect to the database and verify the changes. - ```bash - kbcli cluster connect mysql-cluster +
- Copyright (c) 2000, 2022, Oracle and/or its affiliates. +4. Connect to the database to verify whether the parameters are modified. - Oracle is a registered trademark of Oracle Corporation and/or its - affiliates. Other names may be trademarks of their respective - owners. + The whole searching process has a 30-second delay since it takes some time for kubelete to synchronize modifications to the volume of the pod. - Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + ```bash + kbcli cluster connect mysql-cluster ``` ```bash @@ -281,4 +292,39 @@ Static parameter reconfiguring requires restarting the pod. Here we take reconfi | ngram_token_size | 6 | +------------------+-------+ 1 row in set (0.09 sec) - ``` \ No newline at end of file + ``` + +## View history and compare differences + +After the reconfiguration is completed, you can search the reconfiguration history and compare the parameter differences. + +View the parameter reconfiguration history. + +```bash +kbcli cluster describe-config mysql-cluster +> +ConfigSpecs Meta: +CONFIG-SPEC-NAME FILE ENABLED TEMPLATE CONSTRAINT RENDERED COMPONENT CLUSTER +mysql-consensusset-config my.cnf true mysql8.0-config-template mysql8.0-config-constraints mysql-cluster-mysql-mysql-config mysql mysql-cluster + +History modifications: +OPS-NAME CLUSTER COMPONENT CONFIG-SPEC-NAME FILE STATUS POLICY PROGRESS CREATED-TIME VALID-UPDATED +mysql-cluster-reconfiguring-4q5kv mysql-cluster mysql mysql-consensusset-config my.cnf Succeed reload -/- Mar 16,2023 15:44 UTC+0800 {"my.cnf":"{\"mysqld\":{\"max_connections\":\"3000\",\"read_buffer_size\":\"24288\"}}"} +mysql-cluster-reconfiguring-cclvm mysql-cluster mysql mysql-consensusset-config my.cnf Succeed reload -/- Mar 16,2023 17:28 UTC+0800 {"my.cnf":"{\"mysqld\":{\"innodb_buffer_pool_size\":\"1G\",\"max_connections\":\"600\"}}"} +mysql-cluster-reconfiguring-gx58r mysql-cluster mysql mysql-consensusset-config my.cnf Succeed -/- Mar 16,2023 17:28 UTC+0800 +``` + +From the above results, there are three parameter modifications. + +Compare these modifications to view the configured parameters and their different values for different versions. + +```bash +kbcli cluster diff-config mysql-cluster-reconfiguring-4q5kv mysql-cluster-reconfiguring-gx58r +> +DIFF-CONFIGURE RESULT: + ConfigFile: my.cnf TemplateName: mysql-consensusset-config ComponentName: mysql ClusterName: mysql-cluster UpdateType: update + +PARAMETERNAME MYSQL-CLUSTER-RECONFIGURING-4Q5KV MYSQL-CLUSTER-RECONFIGURING-GX58R +max_connections 3000 600 +innodb_buffer_pool_size 128M 1G +``` diff --git a/docs/user_docs/kubeblocks-for-mysql/high-availability/high-availability.md b/docs/user_docs/kubeblocks-for-mysql/high-availability/high-availability.md index d0aac2a86..dd5c0d6e8 100644 --- a/docs/user_docs/kubeblocks-for-mysql/high-availability/high-availability.md +++ b/docs/user_docs/kubeblocks-for-mysql/high-availability/high-availability.md @@ -1,12 +1,15 @@ --- title: Failure simulation and automatic recovery description: Automatic recovery of a cluster +keywords: [mysql, high availability, failure simulation, automatic recovery] sidebar_position: 1 --- # Failure simulation and automatic recovery -As an open-source data management platform, KubeBlocks supports two database forms, ReplicationSet and ConsensusSet. ReplicationSet can be used for single source with multiple replicas, and non-automatic switching database management, such as MySQL and Redis. ConsensusSet can be used for database management with multiple replicas and automatic switching capabilities, such as ApeCloud MySQL Paxos Group with multiple replicas, MongoDB, etc. The ConsensusSet database management capability has been released in KubeBlocks v0.3.0, and ReplicationSet is under development. This guide takes ApeCloud MySQL as an example to introduce the high availability capability of the database in the form of ConsensusSet. This capability is also applicable to other database engines. +As an open-source data management platform, KubeBlocks supports two database forms, ReplicationSet and ConsensusSet. ReplicationSet can be used for single source with multiple replicas, and non-automatic switching database management, such as MySQL and Redis. ConsensusSet can be used for database management with multiple replicas and automatic switching capabilities, such as ApeCloud MySQL Raft Group with multiple replicas, MongoDB, etc. The ConsensusSet database management capability has been released in KubeBlocks v0.3.0, and ReplicationSet is under development. + +This guide takes ApeCloud MySQL as an example to introduce the high availability capability of the database in the form of ConsensusSet. This capability is also applicable to other database engines. ## Recovery simulation @@ -18,74 +21,86 @@ The faults here are all simulated by deleting a pod. When there are sufficient r ### Before you start -* Install a Kubernetes cluster and KubeBlocks, refer to [Install KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md). -* Create an ApeCloud MySQL Paxos Group, refer to [Create a MySQL cluster](./../cluster-management/create-and-connect-a-mysql-cluster.md). -* Run `kubectl get cd apecloud-mysql -o yaml` to check whether _rolechangedprobe_ is enabled in the ApeCloud MySQL Paxos Group (it is enabled by default). If the following configuration exists, it indicates that it is enabled: - ``` +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Create an ApeCloud MySQL Raft Group, refer to [Create a MySQL cluster](./../cluster-management/create-and-connect-a-mysql-cluster.md). +* Run `kubectl get cd apecloud-mysql -o yaml` to check whether _rolechangedprobe_ is enabled in the ApeCloud MySQL Raft Group (it is enabled by default). If the following configuration exists, it indicates that it is enabled: + + ```bash probes: - roleChangedProbe: + roleProbe: failureThreshold: 3 periodSeconds: 2 timeoutSeconds: 1 ``` - ### Leader pod fault ***Steps:*** -1. Run the command below to view the ApeCloud MySQL Paxos Group information. View the leader pod name in `Topology`. In this example, the leader pod's name is mysql-cluster. +1. View the ApeCloud MySQL Raft Group information. View the leader pod name in `Topology`. In this example, the leader pod's name is mysql-cluster-1. + ```bash kbcli cluster describe mysql-cluster ``` + ![describe_cluster](./../../../img/failure_simulation_describe_cluster.png) 2. Delete the leader pod `mysql-cluster-mysql-1` to simulate a pod fault. + ```bash kubectl delete pod mysql-cluster-mysql-1 ``` ![delete_pod](./../../../img/failure_simulation_delete_pod.png) -3. Run `kbcli cluster describe` and `kbcli cluster connect` to check the status of the pods and Paxos Group connection. - +3. Run `kbcli cluster describe` and `kbcli cluster connect` to check the status of the pods and Raft Group connection. + ***Results*** The following example shows that the roles of pods have changed after the old leader pod was deleted and `mysql-cluster-mysql-2` is elected as the new leader pod. + ```bash kbcli cluster describe mysql-cluster ``` + ![describe_cluster_after](./../../../img/failure_simulation_describe_cluster_after.png) - It shows that this ApeCloud MySQL Paxos Group can be connected within seconds. + It shows that this ApeCloud MySQL Raft Group can be connected within seconds. + ```bash kbcli cluster connect mysql-cluster ``` + ![connect_cluster_after](./../../../img/failure_simulation_connect_cluster_after.png) ***How the automatic recovery works*** - After the leader pod is deleted, the ApeCloud MySQL Paxos Group elects a new leader. In this example, `mysql-cluster-mysql-2` is elected as the new leader. KubeBlocks detects that the leader has changed, and sends a notification to update the access link. The original exception node automatically rebuilds and recovers to the normal Paxos Group state. It normally takes 30 seconds from exception to recovery. + After the leader pod is deleted, the ApeCloud MySQL Raft Group elects a new leader. In this example, `mysql-cluster-mysql-2` is elected as the new leader. KubeBlocks detects that the leader has changed, and sends a notification to update the access link. The original exception node automatically rebuilds and recovers to the normal Raft Group state. It normally takes 30 seconds from exception to recovery. ### Single follower pod exception ***Steps:*** -1. Run the command below to view the ApeCloud MySQL Paxos Group information and view the follower pod name in `Topology`. In this example, the follower pods are mysql-cluster-mysql-0 and mysql-cluster-mysql-2. +1. View the ApeCloud MySQL Raft Group information and view the follower pod name in `Topology`. In this example, the follower pods are mysql-cluster-mysql-0 and mysql-cluster-mysql-2. + ```bash kbcli cluster describe mysql-cluster ``` + ![describe_cluster](./../../../img/failure_simulation_describe_cluster.png) 2. Delete the follower pod mysql-cluster-mysql-0. + ```bash kubectl delete pod mysql-cluster-mysql-0 ``` ![delete_follower_pod](./../../../img/failure_simulation_delete_follower_pod.png) -3. Run the command below to view the Paxos Group status and you can find the follower pod is being terminated in `Component.Instance`. +3. View the Raft Group status and you can find the follower pod is being terminated in `Component.Instance`. + ```bash kbcli cluster describe mysql-cluster ``` ![describe_cluster_follower](./../../../img/failure_simulation_describe_cluster_follower.png) -4. Run the command below to connect to the Paxos Group and you can find this single follower exception doesn't affect the R/W of the cluster. +4. Connect to the Raft Group and you can find this single follower exception doesn't affect the R/W of the cluster. + ```bash kbcli cluster connect mysql-cluster ``` @@ -94,33 +109,39 @@ The faults here are all simulated by deleting a pod. When there are sufficient r ***How the automatic recovery works*** - One follower exception doesn't trigger re-electing of the leader or access link switch, so the R/W of the cluster is not affected. Follower exception triggers recreation and recovery. The process takes no more than 30 seconds. + One follower exception doesn't trigger re-electing of the leader or access link switch, so the R/W of the cluster is not affected. Follower exception triggers recreation and recovery. The process takes no more than 30 seconds. ### Two pods exception -The availability of the cluster generally requires the majority of pods to be in a normal state. When most pods are exceptional, the original leader will be automatically downgraded to a follower. Therefore, any two exceptional pods result in only one follower pod remaining. -Therefore, whether exceptions occur to one leader and one follower or exceptions occur to two followers, failure performance and automatic recovery are the same. +The availability of the cluster generally requires the majority of pods to be in a normal state. When most pods are exceptional, the original leader will be automatically downgraded to a follower. Therefore, any two exceptional pods result in only one follower pod remaining. + +In this way, whether exceptions occur to one leader and one follower or two followers, failure performance and automatic recovery are the same. ***Steps:*** -1. Run the command below to view the ApeCloud MySQL Paxos Group information and view the follower pod name in `Topology`. In this example, the follower pods are mysql-cluster-mysql-1 and mysql-cluster-mysql-0. +1. View the ApeCloud MySQL Raft Group information and view the follower pod name in `Topology`. In this example, the follower pods are mysql-cluster-mysql-1 and mysql-cluster-mysql-0. + ```bash kbcli cluster describe mysql-cluster ``` + ![describe_cluster](./../../../img/failure_simulation_describe_cluster_2.png) 2. Delete these two follower pods. + ```bash kubectl delete pod mysql-cluster-mysql-1 mysql-cluster-mysql-0 ``` ![delete_two_pods](./../../../img/failure_simulation_delete_two_pods.png) -3. Run the command below to view the Paxos Group status and you can find the follower pods are being terminated in `Component.Instance`. +3. View the Raft Group status and you can find the follower pods are pending and a new leader pod is selected. + ```bash kbcli cluster describe mysql-cluster ``` ![describe_two_pods](./../../../img/failure_simulation_describe_two_pods.png) -4. Run `kbcli cluster connect mysql-cluster` again after a few seconds and you can find the pods in the Paxos Group work normally again in `Component.Instance`. +4. Run `kbcli cluster connect mysql-cluster` again after a few seconds and you can find the pods in the Raft Group work normally again in `Component.Instance`. + ```bash kbcli cluster connect mysql-cluster ``` @@ -129,30 +150,35 @@ Therefore, whether exceptions occur to one leader and one follower or exceptions ***How the automatic recovery works*** - When two pods of the ApeCloud MySQL Paxos Group are exceptional, pods are unavailable and cluster R/W is unavailable. After the recreation of pods, a new leader is elected to recover to R/W status. The process takes less than 30 seconds. + When two pods of the ApeCloud MySQL Raft Group are exceptional, pods are unavailable and cluster R/W is unavailable. After the recreation of pods, a new leader is elected to recover to R/W status. The process takes less than 30 seconds. ### All pods exception ***Steps:*** -1. Run the command below to view the ApeCloud MySQL Paxos Group information and view the pods' names in `Topology`. +1. Run the command below to view the ApeCloud MySQL Raft Group information and view the pods' names in `Topology`. + ```bash kbcli cluster describe mysql-cluster ``` + ![describe_cluster](./../../../img/failure_simulation_describe_cluster.png) 2. Delete all pods. + ```bash kubectl delete pod mysql-cluster-mysql-1 mysql-cluster-mysql-0 mysql-cluster-mysql-2 ``` ![delete_three_pods](./../../../img/failure_simulation_delete_three_pods.png) -3. Run the command below to view the deleting process. You can find the pods are being deleted in `Component.Instance` and the follower pod is the last one to be deleted. +3. Run the command below to view the deleting process. You can find the pods are pending. + ```bash kbcli cluster describe mysql-cluster ``` ![describe_three_clusters](./../../../img/failure_simulation_describe_three_pods.png) -4. Run `kbcli cluster connect mysql-cluster` again after a few seconds and you can find the pods in the Paxos Group work normally again in `Component.Instance`. +4. Run `kbcli cluster connect mysql-cluster` again after a few seconds and you can find the pods in the Raft Group work normally again. + ```bash kbcli cluster connect mysql-cluster ``` @@ -161,4 +187,4 @@ Therefore, whether exceptions occur to one leader and one follower or exceptions ***How the automatic recovery works*** - Every time the pod is deleted, recreation is triggered. And then ApeCloud MySQL automatically completes the cluster recovery and the election of a new leader. After the election of the leader is completed, KubeBlocks detects the new leader and updates the access link. This process takes less than 30 seconds. \ No newline at end of file + Every time the pod is deleted, recreation is triggered. And then ApeCloud MySQL automatically completes the cluster recovery and the election of a new leader. After the election of the leader is completed, KubeBlocks detects the new leader and updates the access link. This process takes less than 30 seconds. diff --git a/docs/user_docs/kubeblocks-for-mysql/migration/migration.md b/docs/user_docs/kubeblocks-for-mysql/migration/migration.md index e6f4c6694..7b1de7c46 100644 --- a/docs/user_docs/kubeblocks-for-mysql/migration/migration.md +++ b/docs/user_docs/kubeblocks-for-mysql/migration/migration.md @@ -1,6 +1,7 @@ --- title: Migrate data to ApeCloud MySQL by AWS DMS description: How to migrate data to ApeCloud MySQL by AWS DMS +keywords: [mysql, migration, aws dms] sidebar_position: 1 --- @@ -8,71 +9,92 @@ sidebar_position: 1 :::note -* Using the public network and network load balancer causes extra fees. +* Using the public network and network load balancer may incur expenses. * The following tutorial is based on the prerequisite that ApeCloud MySQL is deployed on AWS EKS. Using other Kubernetes clusters to deploy ApeCloud MySQL is not included. ::: -## Step 1. Network configuration +## Network configuration ### Expose the target ApeCloud MySQL network -The Kubernetes ClusterIP of ApeCloud MySQL is exposed by default in the EKS environment. But the migration task of DMS (Database Migration Service) runs in an independent Replication Instance, in which the Replication Instance can be set with the same VPC used by the Kubernetes clusters, but visiting ClusterIP still fails. This solution aims to connect this part of the network. +The Kubernetes ClusterIP of ApeCloud MySQL is exposed by default in the EKS environment. But the migration task of DMS (Database Migration Service) runs in an independent Replication Instance, in which the Replication Instance can be set with the same VPC used by the Kubernetes clusters, but visiting ClusterIP still fails. This solution aims to connect this part of the network. #### KubeBlocks native solution -1. Check whether the loadbalancer add-on is enabled. + +***Before you start*** + +* [Install `kbcli`](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Enable the AWS loadbalancer controller add-on. + ```bash kbcli addon list - ``` - If the loadbalancer is disabled, it may relate to your environment since the loadbalancer add-on relies on the EKS environment. - Build your EKS environment, and refer to [Enable add-ons](./../../installation/enable-add-ons.md) to enable the loadbalancer add-on. -2. Install ApeCloud MySQL. Refer to [Create an ApeCloud MySQL cluster on AWS](./../../quick-start/create-a-mysql-cluster-on-aws.md) for details. -3. Fill in the cluster name and run the command below to expose the external IP of the cluster. - ```bash - kbcli cluster expose ${mysql clustrName} --enable=true --type=vpc + kbcli addon enable aws-load-balancer-controller + > + addon.extensions.kubeblocks.io/aws-load-balancer-controller enabled ``` - - Run the command below to view the external IP:Port address which can be accessed by the same VPC machine but outside the EKS cluster. + + If the loadbalancer is not enabled successfully, it may relate to your environment since the loadbalancer add-on relies on the EKS environment. + + Check your EKS environment and enable this add-on again. For enabling add-on details, refer to [Enable add-ons](./../../installation/enable-addons.md). + +***Steps*** + +1. Create an ApeCloud MySQL cluster on AWS. Refer to [Create an ApeCloud MySQL cluster](./../cluster-management/create-and-connect-a-mysql-cluster.md) for details. +2. Fill in the cluster name and run the command below to expose the external IP of the cluster. + ```bash - kbcli cluster describe ${clustrName} | grep -A 3 Endpoints + kbcli cluster expose mysql-cluster --enable=true --type='vpc' ``` - ***Example*** + :::note - ```bash - KBCLI_EXPERIMENTAL_EXPOSE="1" kb cluster expose mysql-cluster --on=true + For the above `kbcli cluster expose` command, the available value for `--type` are `vpc` and `internet`. Use `--type=vpc` for access within the same VPC and `--type=internet` for cross VPC access under the public network. + + ::: + Run the command below to view the external IP:Port address which can be accessed by the same VPC machine but outside the EKS cluster. + + ```bash kbcli cluster describe mysql-cluster | grep -A 3 Endpoints > Endpoints: COMPONENT MODE INTERNAL EXTERNAL mysql ReadWrite 10.100.51.xxx:3306 172.31.35.xxx:3306 ``` -4. Configure the external IP:Port as the target endpoint on AWS DMS. + +3. Configure the external IP:Port as the target endpoint on AWS DMS. + This operation generates an ENI (Elastic Network Interface) on EC2. If the quota of the low-spec machine is small, pay more attention to the available level of ENI. + For the corresponding ENI specifications, refer to [Elastic network interfaces - Amazon Elastic Compute Cloud](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/using-eni.html). #### Use Network Load Balancer (NLB) to expose the service + 1. Install Load Balancer Controller on EKS. + For installation details, refer to [Installing the AWS Load Balancer Controller add-on](https://docs.aws.amazon.com/eks/latest/userguide/aws-load-balancer-controller.html). + For how to create NLB in a cluster, refer to [Network load balancing on Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/network-load-balancing.html). -2. Create and use the NLB service to expose the ApeCloud MySQL service. +2. Create a service that uses NLB to expose the ApeCloud MySQL service. + Configure `metadata.name`, `metadata.annotations`, `metadata.labels`, and `spec.selector` according to your actual environment. - ```bash + ```yaml cat <, labels: apps.kubeblocks.io/component-name: mysql - app.kubernetes.io/instance: ${mysql clustername} + app.kubernetes.io/instance: app.kubernetes.io/managed-by: kubeblocks app.kubernetes.io/name: apecloud-mysql spec: @@ -80,7 +102,7 @@ The Kubernetes ClusterIP of ApeCloud MySQL is exposed by default in the EKS envi type: LoadBalancer selector: apps.kubeblocks.io/component-name: mysql - app.kubernetes.io/instance: ${mysql clustername} + app.kubernetes.io/instance: app.kubernetes.io/managed-by: kubeblocks kubeblocks.io/role: leader ports: @@ -89,16 +111,19 @@ The Kubernetes ClusterIP of ApeCloud MySQL is exposed by default in the EKS envi port: 3306 targetPort: mysql EOF - ``` -3. Run the command below to make sure the service and NLB run normally. + ``` + +3. Check whether this new service and NLB run normally. + ```bash kubectl get svc > - NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) - mysql-service LoadBalancer 10.100.xx.xx k8s-xx-xx-xx.elb.cn-northwest-1.amazonaws.com.cn 3306:xx/TCP + NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) + apecloud-mysql-service LoadBalancer 10.100.xx.xx k8s-xx-xx-xx.elb.cn-northwest-1.amazonaws.com.cn 3306:xx/TCP ``` Make sure the server runs normally and can generate EXTERNAL-IP. Meanwhile, verify whether the NLB state is `Active` by the AWS console, then you can access the cluster by EXTERNAL-IP:Port. + ![NLB-active](./../../../img/mysql_migration_active.png) ### Expose the source network @@ -111,25 +136,32 @@ There exist four different conditions for the source network. Choose one method * RDS within the same VPC in AWS - You only need to specify an RDS when creating an endpoint in DMS and no extra operation is required. - For creating an endpoint, refer to step 2 in [Configure AWS DMS tasks](#step-2-configure-aws-dms-tasks). + You only need to specify an RDS when creating an endpoint in DMS and no extra operation is required. + + For creating an endpoint, refer to step 2 in [Configure AWS DMS tasks](#configure-aws-dms-tasks). * RDS within different VPCs in AWS - Use the public network to create an endpoint. Refer to [this document](https://aws.amazon.com/premiumsupport/knowledge-center/aurora-mysql-connect-outside-vpc/?nc1=h_ls) to make public network access available, then create an endpoint in AWS DMS. For creating an endpoint, refer to step 2 in [Configure AWS DMS tasks](#step-2-configure-aws-dms-tasks). + Use the public network to create an endpoint. Refer to [this document](https://aws.amazon.com/premiumsupport/knowledge-center/aurora-mysql-connect-outside-vpc/?nc1=h_ls) to make public network access available, then create an endpoint in AWS DMS. + + For creating an endpoint, refer to step 2 in [Configure AWS DMS tasks](#configure-aws-dms-tasks). * MySQL in AWS EKS Use NLB to expose the service. - 1. Install Load Balancer Controller + 1. Install Load Balancer Controller. + For installation details, refer to [Installing the AWS Load Balancer Controller add-on](https://docs.aws.amazon.com/eks/latest/userguide/aws-load-balancer-controller.html). + For how to create NLB in a cluster, refer to [Network load balancing on Amazon EKS](https://docs.aws.amazon.com/eks/latest/userguide/network-load-balancing.html). - 2. Create the service using NLB - Make sure the value of `some.label.key` in `metadata.labels` is consistent with the value of ApeCloud MySQL you created. + 2. Create the service using NLB. + + Make sure the value of `some.label.key` in `metadata.labels` is consistent with the value of ApeCloud MySQL you created. + Configure `port` and `targetPort` in `spec.ports` according to your current environment. - ```bash + ```yaml cat < **Replication Instance** and click **Create replication instance**. + + Go to **DMS** -> **Replication Instance** and click **Create replication instance**. :::caution @@ -213,7 +256,7 @@ Pay attention to the following potential issues during the migration task. ![Create replication instance](./../../../img/mysql_migration_replication_instance.png) 2. Create endpoints. - + Go to **DMS** -> **Endpoints** and click **Create endpoint**. ![Create endpoint](./../../../img/mysql_migration_create_endpoints.png) @@ -227,20 +270,20 @@ Pay attention to the following potential issues during the migration task. ![Test connection](./../../../img/mysql_migration_test_connection.png) 3. Create migration tasks. - + ![Create task](./../../../img/mysql_migration_create_task.png) - - Click **Create task** and configure the task according to the instructions. - + + Click **Create task** and configure the task according to the instructions. + Pay attention to the following parameters. * Migration Type - + ![Migration type](./../../../img/mysql_migration_migration_type.png) - AWS DMS provides three migration types: + AWS DMS provides three migration types: - * Migrate existing data: AWS DMS migrates only your existing data. Changes to your source data aren’t captured and applied to your target. + * Migrate existing data: AWS DMS migrates only your existing data. Changes to your source data aren’t captured and applied to your target. * Migrate existing data and replicate ongoing changes: AWS DMS migrates both existing data and ongoing data changes, i.e. the existing data before the migration task and the data changes during the migration task will be synchronized to the target instance. * Replicate data changes only: AWS DMS only migrates the ongoing data changes. If you select this type, you can use **CDC start mode for source transactions** to specify a location and migrate the data changes. For this tutorial, select **Migrate existing data and replicate ongoing changes**. @@ -249,7 +292,8 @@ Pay attention to the following potential issues during the migration task. ![Target table preparation mode](./../../../img/mysql_migration_target_table_preparation_mode.png) - The target table preparation mode specifies the initial mode of the data structure. You can click Info beside the options to view the definition of each mode. For example, if ApeCloud MySQL is a newly created empty instance, you can select **Do nothing** mode. + The target table preparation mode specifies the initial mode of the data structure. You can click the Info link beside the options to view the definition of each mode. For example, if ApeCloud MySQL is a newly created empty instance, you can select **Do nothing** mode. + In addition, create a database on ApeCloud MySQL before migration because AWS DMS does not create a database. * Turn on validation @@ -265,38 +309,39 @@ Pay attention to the following potential issues during the migration task. ![Batch-optimized apply](./../../../img/mysql_migration_batch_optimized_apply.png) * Full load tuning settings: Maximum number of tables to load in parallel - - This number decides how many concurrencies DMS uses to get source table data. Theoretically speaking, this will cause pressure on the source table during the full-load migration. Lower this number when the business in the source table is delicate. + + This number decides how many concurrencies DMS uses to get source table data. Theoretically speaking, this can cause pressure on the source table during the full-load migration. Lower this number when the business in the source table is delicate. ![Full load tuning settings](./../../../img/mysql_migration_full_load_tuning_settings.png) * Table Mapping - + Table mapping decides which tables in the database are used for migration and can also apply easy conversions. It is recommended to enable **Wizard** mode to configure this parameter. -4. Click the button to start the migration task. - -## Step 3. Switch applications +4. Start the migration task. + +## Switch applications ***Before you start*** -* DMS migration tasks run normally. If you perform a validation task, make sure the results are as expected. -* To differentiate conversation and improve data security, it is recommended to create and authorize a database account solely for migration. -* It is recommended to switch applications during business off-peak hours because for the sake of safety during the switching process, it is necessary to stop business write. +* Make sure DMS migration tasks run normally. If you perform a validation task, make sure the results are as expected. +* To differentiate conversation and improve data security, it is recommended to create and authorize a database account solely for migration. +* It is recommended to switch applications during business off-peak hours because for safety concerns during the switching process, it is necessary to stop business write. ***Steps:*** 1. Make sure the transmission task runs normally. - + Pay attention to **Status**, **Last updated in Table statistics**, and **CDC latency target** in **CloudWatch metrics**. - + You can also refer to [this document](https://aws.amazon.com/premiumsupport/knowledge-center/dms-stuck-task-progress/?nc1=h_ls) to verify the migration task. - + ![Status](./../../../img/mysql_migration_application_status.png) ![CDC](./../../../img/mysql_migration_application_cdc.png) 2. Pause business and prohibit new business write in the source database. 3. Verify the transmission task status again to make sure the task runs normally and the running status lasts at least 1 minute. + Refer to step 1 above to observe whether the link is normal and whether latency exists. 4. Use the target database to resume business. -5. Verify the migration with business. \ No newline at end of file +5. Verify the migration with business. diff --git a/docs/user_docs/kubeblocks-for-mysql/observability/access-logs.md b/docs/user_docs/kubeblocks-for-mysql/observability/access-logs.md deleted file mode 100644 index 77f66529d..000000000 --- a/docs/user_docs/kubeblocks-for-mysql/observability/access-logs.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Access logs -description: How to access cluster log files -sidebar_position: 3 ---- - -# Access logs -The KubeBlocks log enhancement function aims to simplify the complexities of troubleshooting. kbcli, the command line tool of KubeBlocks, supports viewing all kinds of logs generated by the database clusters running on KubeBlocks, such as slow logs, error logs, audit logs, and the container running logs (Stdout and Stderr). -The KubeBlocks log enhancement function uses methods similar to kubectl exec and kubectl logs to ensure a self-closed loop and lightweight. - -## Before you start - -- The container image supports `tail` and `xargs` commands. -- KubeBlocks Operator is installed on the target Kubernetes cluster. - -## Steps - -1. Enable the log enhancement function. - - If you create a cluster by running the `kbcli cluster create` command, add the `--enable-all-logs=true` option to enable the log enhancement function. When this option is `true`, all the log types defined by `spec.components.logConfigs` in `ClusterDefinition` are enabled automatically. - - ```bash - kbcli cluster create mysql-cluster --cluster-definition='apecloud-mysql' --enable-all-logs=true - ``` - - If you create a cluster by applying a YAML file, add the log type you need in `spec.components.enabledLogs`. As for MySQL, error logs, slow logs, and general logs are supported. - - ```YAML - apiVersion: apps.kubeblocks.io/v1alpha1 - kind: Cluster - metadata: - name: mysql-cluster - namespace: default - spec: - clusterDefinitionRef: mysql-cluster-definition - appVersionRef: appversion-mysql-latest - components: - - name: replicasets - type: replicasets - enabledLogs: - - slow - - error - - general - ``` - -2. View the supported logs. - - Run the `kbcli cluster list-logs` command to view the enabled log types of the target cluster and the log file details. INSTANCE of each node is displayed. - - ***Example*** - - ```bash - kbcli cluster list-logs mysql-cluster - > - INSTANCE LOG-TYPE FILE-PATH SIZE LAST-WRITTEN COMPONENT - mysql-cluster-mysql-0 error /data/mysql/log/mysqld-error.log 6.4K Feb 06, 2023 09:13 (UTC+00:00) mysql - mysql-cluster-mysql-0 general /data/mysql/log/mysqld.log 5.9M Feb 06, 2023 09:13 (UTC+00:00) mysql - mysql-cluster-mysql-0 slow /data/mysql/log/mysqld-slowquery.log 794 Feb 06, 2023 09:13 (UTC+00:00) mysql - ``` - -3. Access the cluster log file. - - Run the `kbcli cluster logs` command to view the details of the target log file generated by the target instance on the target cluster. You can use different options to view the log file details you need. - You can also run `kbcli cluster logs -h` to see the examples and option descriptions. - - ```bash - kbcli cluster logs -h - > - Access cluster log file - - Examples: - # Return snapshot logs from cluster mycluster with default primary instance (stdout) - kbcli cluster logs mycluster - - # Display only the most recent 20 lines from cluster mycluster with default primary instance (stdout) - kbcli cluster logs --tail=20 mycluster - - # Return snapshot logs from cluster mycluster with specify instance my-instance-0 (stdout) - kbcli cluster logs mycluster --instance my-instance-0 - - # Return snapshot logs from cluster mycluster with specify instance my-instance-0 and specify container - # my-container (stdout) - kbcli cluster logs mycluster --instance my-instance-0 -c my-container - - # Return slow logs from cluster mycluster with default primary instance - kbcli cluster logs mycluster --file-type=slow - - # Begin streaming the slow logs from cluster mycluster with default primary instance - kbcli cluster logs -f mycluster --file-type=slow - - # Return the specify file logs from cluster mycluster with specify instance my-instance-0 - kbcli cluster logs mycluster --instance my-instance-0 --file-path=/var/log/yum.log - - # Return the specify file logs from cluster mycluster with specify instance my-instance-0 and specify - # container my-container - kbcli cluster logs mycluster --instance my-instance-0 -c my-container --file-path=/var/log/yum.log - ``` - -4. (Optional) Troubleshooting. - - The log enhancement function does not affect the core process of KubeBlocks. If a configuration exception occurs, a warning shows to help troubleshoot. - `warning` is recorded in the `event` and `status.Conditions` of the target database cluster. - - View `warning` information. - - Run `kbcli cluster describe ` to view the status of the target cluster. You can also run `kbcli cluster list events ` to view the event information of the target cluster directly. - - - Run `kubectl describe cluster ` to view the warning. - - ***Example*** - - ``` - Status: - Cluster Def Generation: 3 - Components: - Replicasets: - Phase: Running - Conditions: - Last Transition Time: 2022-11-11T03:57:42Z - Message: EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Reason: EnableLogsListValidateFail - Status: False - Type: ValidateEnabledLogs - Observed Generation: 2 - Operations: - Horizontal Scalable: - Name: replicasets - Restartable: - replicasets - Vertical Scalable: - replicasets - Phase: Running - Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Creating 49s cluster-controller Start Creating in Cluster: release-name-error - Warning EnableLogsListValidateFail 49s cluster-controller EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Normal Running 36s cluster-controller Cluster: release-name-error is ready, current phase is Running - ``` \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mysql/observability/monitor-database.md b/docs/user_docs/kubeblocks-for-mysql/observability/monitor-database.md deleted file mode 100644 index e4f44d8b8..000000000 --- a/docs/user_docs/kubeblocks-for-mysql/observability/monitor-database.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -title: Monitor database -description: How to monitor your database -sidebar_position: 1 ---- - -# Observability of KubeBlocks -With the built-in database observability, you can observe the database health status and track and measure your database in real-time to optimize database performance. This section shows you how database observability works with KubeBlocks and how to use the function. - -## Enable database monitor - -***Steps:*** - -1. Install KubeBlocks and the monitoring add-ons are installed by default. - - ```bash - kbcli kubeblocks install - ``` - - If you do not want to enable the monitoring add-ons when installing KubeBlocks, set `--monitor` to cancel the add-on installation. But it is not recommended to disable the monitoring function. - - ```bash - kbcli kubeblocks install --monitor=false - ``` - - If you have installed KubeBlocks without the monitoring add-ons, you can use `kbcli addon` to enable the monitoring add-ons. To ensure the completeness of the monitoring function, it is recommended to enable three monitoring add-ons. - - ```bash - # View all add-ons supported - kbcli addon list - - # Enable prometheus add-on - kbcli addon enable prometheus - - # Enable granfana add-on - kbcli addon enable granfana - - # Enable alertmanager-webhook-adaptor add-on - kbcli addon enable alertmanager-webhook-adaptor - ``` - - :::note - - Refer to [Enable add-ons](./../../installation/enable-add-ons.md) for details. - - ::: - -2. View the Web Console of the monitoring components. - - Run the command below to view the Web Console list of the monitoring components after the components are installed. - ```bash - kbcli dashboard list - > - NAME NAMESPACE PORT CREATED-TIME - kubeblocks-grafana default 3000 Jan 13,2023 10:53 UTC+0800 - kubeblocks-prometheus-alertmanager default 9093 Jan 13,2023 10:53 UTC+0800 - kubeblocks-prometheus-server default 9090 Jan 13,2023 10:53 UTC+0800 - ``` - For the Web Console list returned by the above command, if you want to view the Web Console of a specific monitoring component, run the command below and this command enables the port-forward of your local host and opens the default browser: - ```bash - kbcli dashboard open - ``` -3. Enable the database monitoring function. - - The monitoring function is enabled by default when a database is created. The open-source or customized Exporter is injected after the monitoring function is enabled. This Exporter can be found by Prometheus server automatically and scrape monitoring indicators at regular intervals. - - For a new cluster, run the command below to create a database cluster. - ```bash - # Search the cluster definition - kbcli clusterdefinition list - - # Create a cluster - kbcli cluster create --cluster-definition='xxx' - ``` - - ***Example*** - - ```bash - kbcli cluster create mysql-cluster --cluster-definition='apecloud-mysql' - ``` - - :::note - - The setting of `monitor` is `true` by default and it is not recommended to disable it. For example, - ```bash - kbcli cluster create mycluster --cluster-definition='apecloud-mysql' --monitor=false - ``` - - ::: - - You can change the value to `false` to disable the monitor function if required. - - For the existing cluster, you can update it to enable the monitor function with `update` command. - ```bash - kbcli cluster update --monitor=true - ``` - - ***Example*** - - ```bash - kbcli cluster update mysql-cluster --monitor=true - ``` - -You can view the dashboard of the corresponding cluster via Grafana Web Console. For more detailed information, see [Grafana documentation](https://grafana.com/docs/grafana/latest/dashboards/). \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-mysql/storage-management/.gitkeep b/docs/user_docs/kubeblocks-for-mysql/storage-management/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/docs/user_docs/kubeblocks-for-mysql/storage-management/_category_.yml b/docs/user_docs/kubeblocks-for-mysql/storage-management/_category_.yml deleted file mode 100644 index 2f827c59e..000000000 --- a/docs/user_docs/kubeblocks-for-mysql/storage-management/_category_.yml +++ /dev/null @@ -1,4 +0,0 @@ -position: 9 -label: Storage Management -collapsible: true -collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/_category_.yml index 853e7ca46..e4310ca55 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/_category_.yml @@ -1,4 +1,4 @@ position: 5 label: KubeBlocks for PostgreSQL collapsible: true -collapsed: true \ No newline at end of file +collapsed: true diff --git a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/_category_.yml index 585156222..ff724d53b 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/_category_.yml @@ -1,4 +1,4 @@ -position: 3 +position: 4 label: Backup and Restore collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/backup-and-restore-for-postgresql-standalone.md b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/backup-and-restore-for-postgresql-standalone.md deleted file mode 100644 index 1e4ab0ce7..000000000 --- a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/backup-and-restore-for-postgresql-standalone.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -title: Backup and restore for PostgreSQL Standalone -description: Guide for backup and restore for a PostgreSQL Standalone -sidebar_position: 2 -sidebar_label: PostgreSQL Standalone ---- - -# Backup and restore for PostgreSQL Standalone -This section shows how to use `kbcli` to back up and restore a PostgreSQL Standalone. - -***Before you start*** - -- Prepare a clean EKS cluster, and install EBS CSI driver plug-in, with at least one node and the memory of each node is not less than 4GB. -- [Install `kubectl`](https://kubernetes.io/docs/tasks/tools/install-kubectl-macos/) to ensure that you can connect to the EKS cluster. -- Install `kbcli`. Refer to [Install kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - -***Steps:*** - -1. Install KubeBlocks and the snapshot-controller add-on. - ```bash - kbcli kubeblocks install --set snapshot-controller.enabled=true - ``` - - Since your `kubectl` is already connected to the EKS cluster, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your EKS environment. - - Verify the installation with the following command. - ```bash - kubectl get pod -n kb-system - ``` - - The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. - ``` - NAME READY STATUS RESTARTS AGE - kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m - kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m - ``` - - If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](../../installation/enable-add-ons.md) to find the environment requirements and then enable the snapshot-controller add-on. - -2. Configure EKS to support the snapshot function. - - The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. - - Configure the storage class of snapshot (the assigned EBS volume is gp3). - ```bash - kubectl create -f - < + + +Enable CSI-S3 and fill in the values based on your actual environment. + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://s3..amazonaws.com.cn \ +--set secret.region= -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + +:::note + +Endpoint format: + +* China: `https://s3..amazonaws.com.cn` +* Other countries/regions: `https://s3..amazonaws.com` + +::: + + + + + +```bash +helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + +helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ +--set secret.accessKey= \ +--set secret.secretKey= \ +--set storageClass.singleBucket= \ +--set secret.endpoint=https://oss-.aliyuncs.com \ + -n kb-system + +# CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes +--set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' +``` + + + + + +1. Install minIO. + + ```bash + helm upgrade --install minio oci://registry-1.docker.io/bitnamicharts/minio --set persistence.enabled=true,persistence.storageClass=csi-hostpath-sc,persistence.size=100Gi,defaultBuckets=backup + ``` + +2. Install CSI-S3. + + ```bash + helm repo add kubeblocks https://jihulab.com/api/v4/projects/85949/packages/helm/stable + + helm install csi-s3 kubeblocks/csi-s3 --version=0.5.0 \ + --set secret.accessKey= \ + --set secret.secretKey= \ + --set storageClass.singleBucket=backup \ + --set secret.endpoint=http://minio.default.svc.cluster.local:9000 \ + -n kb-system + + # CSI-S3 installs a daemonSet pod on all nodes and you can set tolerations to install daemonSet pods on the specified nodes + --set-json tolerations='[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"taintValue"}]' + ``` + + + + +You can configure a global backup storage to make this storage the default backup destination path of all new clusters. But currently, the global backup storage cannot be synchronized as the backup destination path of created clusters. + +Set the backup policy with the following command. + +```bash +kbcli kubeblocks config --set dataProtection.backupPVCName=kubeblocks-backup-data \ +--set dataProtection.backupPVCStorageClassName=csi-s3 -n kb-system + +# dataProtection.backupPVCName: PersistentVolumeClaim Name for backup storage +# dataProtection.backupPVCStorageClassName: StorageClass Name +# -n kb-system: namespace where KubeBlocks is installed +``` + +:::note + +* If there is no PVC, the system creates one automatically based on the configuration. +* It takes about 1 minute to make the configuration effective. + +::: + +## Create backup + +**Option 1. Manually Backup** + +1. Check whether the cluster is running. + + ```bash + kbcli cluster list pg-cluster + ``` + +2. Create a backup for this cluster. + + ```bash + kbcli cluster backup pg-cluster --type=datafile + ``` + +3. View the backup set. + + ```bash + kbcli cluster list-backups pg-cluster + ``` + +**Option 2. Enable scheduled backup** + +```bash +kbcli cluster edit-backup-policy pg-cluster-postgresql-backup-policy +> +spec: + ... + schedule: + baseBackup: + # UTC time zone, the example below means 2 a.m. every Monday + cronExpression: "0 18 * * 0" + # Enable this function + enable: true + # Select the basic backup type, available options: snapshot and snapshot + # This example selects datafile as the basic backup type + type: datafile +``` + +## Restore data from backup + +1. Restore data from the backup. + + ```bash + kbcli cluster restore new-pg-cluster --backup backup-default-pg-cluster-20230418124113 + ``` + +2. View this new cluster. + + ```bash + kbcli cluster list new-pg-cluster + ``` diff --git a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/pitr-for-postgresql.md b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/pitr-for-postgresql.md new file mode 100644 index 000000000..b14816474 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/pitr-for-postgresql.md @@ -0,0 +1,272 @@ +--- +title: PITR for PostgreSQL +description: PITR guide for a PostgreSQL cluster +sidebar_position: 3 +sidebar_label: PITR +--- + +# PITR for PostgreSQL + +PITR (Point-in-time-recovery) for PostgreSQL by KubeBlocks is realized on the basis of the full backup and log backup. For KubeBlocks v0.5.0, PITR is only supported on the cloud. PITR for a local self-managed database is coming soon. + +This section shows how to use `kbcli` to back up and restore a PostgreSQL Standalone. + +## Configure the snapshot backup function + +***Before you start*** + +Prepare a clean EKS cluster, and install EBS CSI driver plug-in, with at least one node and the memory of each node is not less than 4GB. + +***Steps:*** + +1. Enable the snapshot-controller add-on. + +- When installing KubeBlocks, use: + + ```bash + kbcli kubeblocks install --set snapshot-controller.enabled=true + ``` + +- In case you have KubeBlocks installed already, and the snapshot addon is not enabled, use: + + ```bash + kbcli addon enable snapshot-controller + kbcli kubeblocks config --set dataProtection.enableVolumeSnapshot=true + ``` + +2. Verify the installation. + + ```bash + kbcli addon list + ``` + + The `snapshot-controller` status is enabled. See the information below. + +
+ + Expected output + + ```bash + NAME TYPE STATUS EXTRAS AUTO-INSTALL AUTO-INSTALLABLE-SELECTOR + snapshot-controller Helm Enabled true + ``` + +
+ + If the output result does not show `snapshot-controller`, refer to [Enable add-ons](../../installation/enable-addons.md) to find the environment requirements and then enable the snapshot-controller add-on. It may be caused by failing to meet the installation condition of this add-on. + +3. Configure Cloud-based Kubernetes managed service to support the snapshot function. + + Now KubeBlocks supports the snapshot function in EKS, ACK, and GKE. + + - Configure the storage class of snapshot (the assigned EBS volume is gp3). + + ```bash + kubectl create -f - <,secret.secretKey=,secret.endpoint=https://s3.cn-northwest-1.amazonaws.com.cn,secret.region=cn-northwest-1,storageClass.singleBucket=demo + ``` + +| Parameters | Description | +| :------------------------ | :--------------------------------------- | +| secret.accessKey | S3 access key. | +| secret.secretKey | S3 secret key. | +| secret.endpoint | S3 access address. Example: AWS global: `.amazonaws.com` | +| secret.region | S3 region | +| storageClass.singleBucket | S3 bucket | +| secret.region | S3 region | +| storageClass.singleBucket | S3 bucket | + +5. Check StorageClass. + + ```bash + kubectl get storageclasses + ``` + +
+ + Expected output + + ```bash + NAME PROVISIONER RECLAIMPOLICY VOLUMEBINDINGMODE ALLOWVOLUMEEXPANSION AGE + csi-s3 ru.yandex.s3.csi Retain Immediate false 14m + ``` + +
+ +6. Configure the automatically created PVC name and storageclass. + + ```bash + kbcli kubeblocks config --set dataProtection.backupPVCName=backup-data --set dataProtection.backupPVCStorageClassName=csi-s3 + ``` + +7. Enable the log backup and upload it to S3. + + Please make sure you have a PostgreSQL cluster, if not, create it with `kbcli cluster create` command. In the following code blocks, `my-pg` is the name of the PostgreSQL cluster. + + ```bash + kbcli cluster list-backup-policy + NAME DEFAULT CLUSTER CREATE-TIME + my-pg-postgresql-backup-policy true my-pg Apr 20,2023 18:13 UTC+0800 + + # Edit the backup policy and enable incremental log backups + kbcli cluster edit-backup-policy my-pg-postgresql-backup-policy + + # Find spec.schedule.logfile.enable, change it from false to true + + #Save and exit + :wq + ``` + +## Test backup function + +1. Create an empty snapshot backup. + + ```bash + kbcli cluster backup my-pg + ``` + + Check it by `kbcli cluster list-backup`. + +2. Create a user account. + + ```bash + kbcli cluster create-account my-pg --username myuser + +---------+-------------------------------------------------+ + | RESULT | MESSAGE | + +---------+-------------------------------------------------+ + | Success | created user: myuser, with password: oJ3bAiK7pr | + +---------+-------------------------------------------------+ + # Copy the user password generated: oJ3bAiK7pr + + ``` + +3. Grant roles to the user created. + + ```bash + kbcli cluster grant-role my-pg --username myuser --role READWRITE + ``` + + Connect to the database. + + ```bash + kbcli cluster connect my-pg --as-user myuser + password: #Copy the user password generated + ``` + +4. Insert test data to test backup. + + ```bash + create table if not exists msg(id SERIAL PRIMARY KEY, msg text, time timestamp); + insert into msg (msg, time) values ('hello', now()); + + # Wait for 5 minutes and insert another row + insert into msg (msg, time) values ('hello', now()); + + # Check data + select * from msg; + id | msg | time + ----+-------+---------------------------- + 1 | hello | 2023-04-17 11:56:38.269572 + 2 | hello | 2023-04-17 11:56:42.988197 + (2 rows) + ``` + +5. Configure backup to a specified time. + + Check the `RECOVERABLE-TIME`. + + ```bash + kbcli cluster describe my-pg + ... + Data Protection: + AUTO-BACKUP BACKUP-SCHEDULE TYPE BACKUP-TTL LAST-SCHEDULE RECOVERABLE-TIME + Disabled 0 18 * * 0 snapshot 7d Apr 17,2023 19:55:48 UTC+0800 ~ Apr 17,2023 19:57:01 UTC+0800 + ``` + + :::note + + The recoverable time refreshes every 5 minutes. + + ::: + + Choose any time after the recoverable time. + + ```bash + kbcli cluster restore new-cluster --restore-to-time "Apr 17,2023 19:56:40 UTC+0800" --source-cluster my-pg + ``` + +6. Check the backup data. + + :::note + + PostgreSQL uses Patroni and the kernel process restarts after backup. Wait after 30 minutes before connecting to the backup cluster. + + ::: + + Connect to the backup cluster. + + ```bash + kbcli cluster connect new-cluster --as-user myuser + + # Verify data + select * from msg; + id | msg | time + ----+-------+---------------------------- + 1 | hello | 2023-04-17 11:56:38.269572 + (1 row) + ``` + +In this example, data inserted before 19:56:40 is restored. + +7. (**Caution**) Delete the PostgreSQL cluster and clean up the backup. + +:::danger + +Data deleted here is only for testing. In real scenarios, deleting backup is a critically high-risk operation. + +::: + + Delete a PostgreSQL cluster with the following command. + + ```bash + kbcli cluster delete my-pg + kbcli cluster delete new-cluster + ``` + + Delete the specified backup. + + ```bash + kbcli cluster delete-backup my-pg --name backup-default-my-pg-20230417195547 + ``` + + Force delete all backups with `my-pg`. + + ```bash + kbcli cluster delete-backup my-pg --force + ``` + +:::note + +Expenses incurred when you have snapshots on the cloud. So it is recommended to delete the test cluster. + +::: diff --git a/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/snapshot-backup-and-restore-for-pgsql.md b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/snapshot-backup-and-restore-for-pgsql.md new file mode 100644 index 000000000..592603842 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/backup-and-restore/snapshot-backup-and-restore-for-pgsql.md @@ -0,0 +1,127 @@ +--- +title: Snapshot backup and restore for PostgreSQL +description: Guide for backup and restore for PostgreSQL +keywords: [postgresql, snapshot, backup, restore] +sidebar_position: 2 +sidebar_label: Snapshot backup and restore +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Snapshot backup and restore for PostgreSQL + +This section shows how to use `kbcli` to back up and restore a PostgreSQL cluster. + +***Steps:*** + +1. Install KubeBlocks and the snapshot-controller add-on. + + ```bash + kbcli kubeblocks install --set snapshot-controller.enabled=true + ``` + + If you have installed KubeBlocks without enabling the snapshot-controller, run the command below. + + ```bash + kbcli kubeblocks upgrade --set snapshot-controller.enabled=true + ``` + + Since your `kubectl` is already connected to the cluster of cloud Kubernetes service, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your environment. + + Verify the installation with the following command. + + ```bash + kubectl get pod -n kb-system + ``` + + The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. + + ```bash + NAME READY STATUS RESTARTS AGE + kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m + kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m + ``` + + If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](./../../installation/enable-addons.md) to find the environment requirements and then enable the snapshot-controller add-on. + +2. Configure cloud managed Kubernetes environment to support the snapshot function. For ACK and GKE, the snapshot function is enabled by default, you can skip this step. + + + + + The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. + + Configure the storage class of the snapshot (the assigned EBS volume is gp3). + + ```bash + kubectl create -f - < + + + + Configure the default volumesnapshot class. + + ```yaml + kubectl create -f - < + + +3. Create a snapshot backup. + + ```bash + kbcli cluster backup pg-cluster + ``` + +4. Check the backup. + + ```bash + kbcli cluster list-backups + ``` + +5. Restore to a new cluster. + + Copy the backup name to the clipboard, and restore to the new cluster. + + :::note + + You do not need to specify other parameters for creating a cluster. The restoration automatically reads the parameters of the source cluster, including specification, disk size, etc., and creates a new PostgreSQL cluster with the same specifications. + + ::: + + Execute the following command. + + ```bash + kbcli cluster restore pg-new-from-snapshot --backup backup-default-pg-cluster-20221124113440 + ``` diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/_category_.yml index 27efe6be9..200a2feb5 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/_category_.yml @@ -1,4 +1,4 @@ -position: 1 +position: 2 label: Cluster Management collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/create-and-connect-a-postgresql-cluster.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/create-and-connect-a-postgresql-cluster.md index c69e13004..454d335c1 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/create-and-connect-a-postgresql-cluster.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/create-and-connect-a-postgresql-cluster.md @@ -1,128 +1,172 @@ --- title: Create and connect to a PostgreSQL Cluster description: How to create and connect to a PostgreSQL cluster +keywords: [postgresql, create a postgresql cluster, connect to a postgresql cluster] sidebar_position: 1 sidebar_label: Create and connect --- -# Create and connect to a PostgreSQL Cluster -## Create a PostgreSQL Cluster +# Create and connect to a PostgreSQL cluster + +This document shows how to create and connect to a PostgreSQL cluster. + +## Create a PostgreSQL cluster ### Before you start -* `kbcli`: Install `kbcli` on your host. Refer to [Install/Uninstall kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - 1. Run the command below to install `kbcli`. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - 2. Run the command below to check the version and verify whether `kbcli` is installed successfully. - ```bash - kbcli version - ``` -* KubeBlocks: Install KubeBlocks on your host. Refer to [Install/Uninstall kbcli and KubeBlocks](./../../installation/install-and-uninstall-kbcli-and-kubeblocks.md) for details. - 1. Run the command below to install KubeBlocks. - ```bash - kbcli kubeblocks install - ``` - - :::note - - If you want to specify a namespace for KubeBlocks, use `--namespace` or the abbreviated `-n` to name your namespace and configure `--create-namespace` as `true` to create a namespace if it does not exist. For example, - ```bash - kbcli kubeblocks install -n kubeblocks --create-namespace=true - ``` - - ::: - - 2. Run the command below to verify whether KubeBlocks is installed successfully. - ```bash - kubectl get pod - ``` - - ***Result*** - - Four pods starting with `kubeblocks` are displayed. For example, - ``` - NAME READY STATUS RESTARTS AGE - kubeblocks-7d4c6fd684-9hjh7 1/1 Running 0 3m33s - kubeblocks-grafana-b765d544f-wj6c6 3/3 Running 0 3m33s - kubeblocks-prometheus-alertmanager-7c558865f5-hsfn5 2/2 Running 0 3m33s - kubeblocks-prometheus-server-5c89c8bc89-mwrx7 2/2 Running 0 3m33s - ``` -* Run the command below to view all the database types available for creating a cluster. +* [Install kbcli](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Make sure the PostgreSQL add-on is installed and enabled with `kbcli addon list`. + ```bash - kbcli clusterdefinition list + kbcli addon list + > + NAME TYPE STATUS EXTRAS AUTO-INSTALL INSTALLABLE-SELECTOR + ... + postgresql Helm Enabled true + ... ``` -### Steps +* View all the database types and versions available for creating a cluster. -1. Run the command below to list all the available kernel versions and choose the one that you need. - ```bash - kbcli clusterversion list - ``` + ```bash + kbcli clusterversion list + ``` -2. Run the command below to create a PostgreSQL cluster. - ```bash - kbcli cluster create pg-cluster --cluster-definition='postgresql' - ``` - ***Result*** +### (Recommended) Create a cluster on a tainted node - * A cluster then is created in the default namespace. You can specify a namespace for your cluster by using `--namespace` or the abbreviated `-n` option. For example, +In actual scenarios, you are recommended to create a cluster on nodes with taints and customized specifications. - ```bash - kubectl create namespace demo +1. Taint your node. - kbcli cluster create -n demo --cluster-definition='postgresql' - ``` - * A cluster is created with built-in toleration which tolerates the node with the `kb-data=true:NoSchedule` taint. - * A cluster is created with built-in node affinity which first deploys the node with the `kb-data:true` label. - * For configuring pod affinity for a cluster, refer to [Configure pod affinity for database cluster](../../resource-scheduling/resource-scheduling.md). - - To create a cluster with specified parameters, follow the steps below, and you have three options. + :::note + + If you have already some tainted nodes, you can skip this step. + + ::: + + 1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` + + 2. Place taints on the selected nodes. + + ```bash + kubectl taint nodes =true:NoSchedule + kubectl taint nodes =true:NoSchedule + ``` + +2. Create a PostgreSQL cluster. - **Option 1.** (**Recommended**) Use `--set` option - - Add the `--set` option when creating a cluster. For example, - ```bash - kbcli cluster create pg-cluster --cluster-definition postgresql --set cpu=1000m,memory=1Gi,storage=10Gi - ``` + The cluster creation command is simply `kbcli cluster create`. Use tolerances to deploy it on the tainted node. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. - **Option 2.** Change YAML file configurations + Create a cluster with a specified class, you can use `--set` flag and specify your requirement. - Change the corresponding parameters in the YAML file. ```bash - kbcli cluster create pg-cluster --cluster-definition="postgresql" --set-file -< --namespace + ``` + + Or change the corresponding parameters in the YAML file. + + ```bash + kbcli cluster create pg-cluster --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --cluster-definition=postgresql --namespace --set-file -< accessModes: - ReadWriteOnce resources: requests: - storage: 20Gi + cpu: 2000m + memory: 2Gi + storage: 10Gi EOF - ``` -### kbcli cluster create options description +See the table below for the detailed descriptions for customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended to turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose a database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this function is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want to set node labels, you can follow the example format:
`kbcli cluster create pg-cluster --cluster-definition=postgresql --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, ang each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information specified, you create a PostgreSQL cluster with default settings. + +```bash +kbcli cluster create pg-cluster --cluster-definition=postgresql --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' +``` + +### Create a cluster on a node without taints + +The cluster creation command is simply `kbcli cluster create`. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + +To create a cluster with a specified class, you can use the `--set` flag and specify your requirement. + +```bash +kbcli cluster create pg-cluster --cluster-definition=postgresql --namespace --set cpu=2,memory=2Gi,replicas=2,storage=20Gi,storageClass= +``` + +Or you can directly change the corresponding parameters in the YAML file. -| Option | Description | -| :-- | :-- | -| `--cluster-definition` | It specifies the cluster definition. Run `kbcli cd list` to show all available cluster definitions. | -| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is used by default. | -| `--enable-all-logs` | It enables you to view all application logs. When this option is enabled, enabledLogs of component level will be ignored. This option is set as true by default. | -| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | -| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | -| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
```kbcli cluster create --cluster-definition='postgresql' --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'``` | -| `--set` | It sets the cluster resource including CPU, memory, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,storage=10Gi`. | -| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | -| `--termination-policy` | It specifies the termination policy of the cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | +```bash +kbcli cluster create pg-cluster --cluster-definition=postgresql --namespace --set-file -< + accessModes: + - ReadWriteOnce + resources: + requests: + cpu: 2000m + memory: 2Gi + storage: 10Gi +EOF +``` + +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition, choose the database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this function is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
`kbcli cluster create pg-cluster --cluster-definition=postgresql --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a PostgreSQL cluster with default settings. + +```bash +kbcli cluster create pg-cluster --cluster-definition=postgresql +``` ## Connect to a PostgreSQL Cluster -Run the command below to connect to a cluster. For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). ```bash -kbcli cluster connect pg-cluster -``` \ No newline at end of file +kbcli cluster connect --namespace +``` + +For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-a-postgresql-cluster.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-a-postgresql-cluster.md new file mode 100644 index 000000000..c7e1787ec --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/delete-a-postgresql-cluster.md @@ -0,0 +1,53 @@ +--- +title: Delete a PostgreSQL Cluster +description: How to delete a PostgreSQL Cluster +keywords: [postgresql, delete a cluster] +sidebar_position: 6 +sidebar_label: Delete protection +--- + +# Delete a PostgreSQL Cluster + +:::note + +The termination policy determines how a cluster is deleted. + +::: + +## Termination policy + +| **terminationPolicy** | **Deleting Operation** | +|:----------------------|:-------------------------------------------------------------------------------------------| +| `DoNotTerminate` | `DoNotTerminate` blocks delete operation. | +| `Halt` | `Halt` deletes workload resources such as statefulset, deployment workloads but keep PVCs. | +| `Delete` | `Delete` deletes workload resources and PVCs but keep backups. | +| `WipeOut` | `WipeOut` deletes workload resources, PVCs and all relevant resources included backups. | + +To check the termination policy, execute the following command. + +```bash +kbcli cluster list +``` + +***Example*** + +```bash +kbcli cluster list pg-cluster +> +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +pg-cluster default postgresql postgresql-14.7.0 Delete Running Mar 03,2023 18:49 UTC+0800 +``` + +## Step + +Configure the cluster name and run the command below to delete the specified cluster. + +```bash +kbcli cluster delete +``` + +***Example*** + +```bash +kbcli cluster delete pg-cluster +``` diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/expand-volume.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/expand-volume.md index cc895af01..e9b50916c 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/expand-volume.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/expand-volume.md @@ -1,22 +1,25 @@ --- title: Expand volume description: How to expand the volume of a PostgreSQL cluster +keywords: [postgresql, expand volume] sidebar_position: 3 sidebar_label: Expand volume --- # Expand volume + You can expand the storage volume size of each pod. :::note -Volume expansion triggers pod restart, all pods restart in the order of learner -> follower -> leader and the leader pod may change after the operation. +Volume expansion triggers a concurrent restart and the leader pod may change after the operation. ::: ## Before you start -Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. +Check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. + ```bash kbcli cluster list ``` @@ -26,66 +29,90 @@ kbcli cluster list ```bash kbcli cluster list pg-cluster > -NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME pg-cluster default postgresql postgresql-14.7.0 Delete Running Mar 3,2023 10:29 UTC+0800 ``` - -## Option 1. Use kbcli -Configure the values of `--component-names`, `--volume-claim-template-names`, and `--storage`, and run the command below to expand the volume. -```bash -kbcli cluster volume-expand pg-cluster --component-names="pg-replication" \ ---volume-claim-template-names="data" --storage="2Gi" -``` +## Steps -- `--component-names` describes the component name for volume expansion. -- `--volume-claim-template-names` describes the VolumeClaimTemplate names in components. -- `--storage` describes the volume storage size. - -## Option 2. Create an OpsRequest +1. Change configuration. There are 3 ways to apply volume expansion. -Run the command below to expand the volume of a cluster. -```bash -kubectl apply -f - < + ``` + + ***Example*** + + ```bash + kbcli cluster list pg-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + pg-cluster default postgresql postgresql-14.7.0 Delete VolumeExpanding Apr 10,2023 16:27 UTC+0800 + ``` + + * STATUS=VolumeExpanding: it means the volume expansion is in progress. + * STATUS=Running: it means the volume expansion operation has been applied. diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/handle-an-exception.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/handle-an-exception.md index 439ede24a..bc8f00bd1 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/handle-an-exception.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/handle-an-exception.md @@ -1,15 +1,18 @@ --- title: Handle an exception description: How to handle an exception in a PostgreSQL cluster +keywords: [postgresql, exception] sidebar_position: 7 --- # Handle an exception -When there is an exception during your operation, you can perform the following procedure to solve it. + +When there is an exception during your operation, you can perform the following procedures to solve it. ## Steps 1. Check the cluster status. Fill in the name of the cluster you want to check and run the command below. + ```bash kbcli cluster list ``` @@ -19,6 +22,7 @@ When there is an exception during your operation, you can perform the following ```bash kbcli cluster list pg-cluster ``` + 2. Handle the exception according to the status information. | **Status** | **Information** | @@ -26,11 +30,12 @@ When there is an exception during your operation, you can perform the following | Abnormal | The cluster can be accessed but exceptions occur in some pods. This might be a mediate status of the operation process and the system recovers automatically without executing any extra operation. Wait until the cluster status is Running. | | ConditionsError | The cluster is normal but an exception occurs to the condition. It might be caused by configuration loss or exception, which further leads to operation failure. Manual recovery is required. | | Failed | The cluster cannot be accessed. Check the `status.message` string and get the exception reason. Then manually recover it according to the hints. | - + You can check the cluster's status for more information. ## Fallback strategies If the above operation can not solve the problem, try the following steps: - - Restart this cluster. If the restart fails, you can delete the pod manually. - - Roll the cluster status back to the status before changes. \ No newline at end of file + +- Restart this cluster. If the restart fails, you can delete the pod manually. +- Roll the cluster status back to the status before changes. diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-postgresql-cluster.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-a-postgresql-cluster.md similarity index 63% rename from docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-postgresql-cluster.md rename to docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-a-postgresql-cluster.md index c39c26644..f22c8b8b0 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-postgresql-cluster.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/restart-a-postgresql-cluster.md @@ -1,38 +1,44 @@ --- title: Restart PostgreSQL cluster description: How to restart a PostgreSQL cluster +keywords: [postgresql, restart] sidebar_position: 4 sidebar_label: Restart --- # Restart PostgreSQL cluster + You can restart all pods of the cluster. When an exception occurs in a database, you can try to restart it. :::note -All pods restart in the order of learner -> follower -> leader and the leader may change after the cluster restarts. +Restarting a PostgreSQL cluster triggers a concurrent restart and the leader may change after the cluster restarts. ::: ## Steps 1. Restart a cluster. + You can use `kbcli` or create an OpsRequest to restart a cluster. **Option 1.** (**Recommended**) Use kbcli - - Configure the values of `component-names` and `ttlSecondsAfterSucceed` and run the command below to restart a specified cluster. + + Configure the values of `components` and `ttlSecondsAfterSucceed` and run the command below to restart a specified cluster. + ```bash - kbcli cluster restart NAME --component-names="pg-replication" \ + kbcli cluster restart NAME --components="pg-replication" \ --ttlSecondsAfterSucceed=30 ``` - - `component-names` describes the component name that needs to be restarted. - - `ttlSecondsAfterSucceed` describes the time to live of OpsRequest job after the restarting succeeds. + + - `components` describes the component name that needs to be restarted. + - `ttlSecondsAfterSucceed` describes the time to live of an OpsRequest job after the restarting succeeds. **Option 2.** Create an OpsRequest - Run the command below to apply the restarting to a cluster. + Run the command below to apply the restarting to a cluster. + ```bash kubectl apply -f - < follower -> leader and the leader ma - componentName: pg-replication EOF ``` + 2. Validate the restarting. + Run the command below to check the cluster status to check the restarting status. + ```bash kbcli cluster list ``` - - STATUS=Updating: it means the cluster is restarting. - - STATUS=Running: it means the cluster is restarted. - + ***Example*** ```bash @@ -62,3 +69,7 @@ All pods restart in the order of learner -> follower -> leader and the leader ma NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME pg-cluster default postgresql-cluster postgresql-14.7.0 Delete Running Mar 03,2023 18:28 UTC+0800 ``` + + * STATUS=Restarting: it means the cluster restart is in progress. + * STATUS=Running: it means the cluster has been restarted. + diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-postgresql.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-a-postgresql-cluster.md similarity index 81% rename from docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-postgresql.md rename to docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-a-postgresql-cluster.md index 0f1c18cbe..34223407e 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-postgresql.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/scale-for-a-postgresql-cluster.md @@ -1,25 +1,29 @@ --- title: Scale for PostgreSQL -description: How to scale a PostgreSQL cluster, horizontal scaling, vertical scaling +description: How to vertically scale a PostgreSQL cluster +keywords: [postgresql, vertical scale] sidebar_position: 2 sidebar_label: Scale --- # Scale for PostgreSQL -You can scale PostgreSQL DB instances in two ways, horizontal scaling and vertical scaling. + +Currently, only vertical scaling for PostgreSQL is supported. ## Vertical scaling + You can vertically scale a cluster by changing resource requirements and limits (CPU and storage). For example, if you need to change the resource demand from 1C2G to 2C4G, vertical scaling is what you need. :::note -During the vertical scaling process, all pods restart in the order of learner -> follower -> leader and the leader pod may change after the restarting. +During the vertical scaling process, a concurrent restart is triggered and the leader pod may change after the restarting. ::: ### Before you start -Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. +Check whether the cluster status is `Running`. Otherwise, the following operations may fail. + ```bash kbcli cluster list ``` @@ -36,25 +40,27 @@ pg-cluster default postgresql-cluster postgresql-14.7.0 Delete ### Steps 1. Change configuration. There are 3 ways to apply vertical scaling. - + **Option 1.** (**Recommended**) Use kbcli - - Configure the parameters `--component-names`, `--memory`, and `--cpu` and run the command. - + + Configure the parameters `--components`, `--memory`, and `--cpu` and run the command. + ***Example*** - + ```bash kbcli cluster vscale pg-cluster \ - --component-names="pg-replication" \ + --components="pg-replication" \ --memory="4Gi" --cpu="2" \ ``` - - `--component-names` describes the component name ready for vertical scaling. + + - `--components` describes the component name ready for vertical scaling. - `--memory` describes the requested and limited size of the component memory. - `--cpu` describes the requested and limited size of the component CPU. **Option 2.** Create an OpsRequest Run the command below to apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + ```bash kubectl apply -f - < ``` @@ -126,7 +134,8 @@ pg-cluster default postgresql-cluster postgresql-14.7.0 Delete NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME pg-cluster default postgresql-cluster postgresql-14.7.0 Delete Running Mar 03,2023 18:00 UTC+0800 ``` - - STATUS=Running: it means the vertical scaling operation is applied. - - STATUS=Updating: it means the vertical scaling is in progress. - - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be the normal instances number is less than the total instance number or the leader instance is running properly while others are abnormal. + + - STATUS=VerticalScaling: it means the vertical scaling is in progress. + - STATUS=Running: it means the vertical scaling has been applied. + - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be the normal instances number is less than the total instance number or the leader instance is running properly while others are abnormal. > To solve the problem, you can check manually to see whether resources are sufficient. If AutoScaling is supported, the system recovers when there are enough resources, otherwise, you can create enough resources and check the result with kubectl describe command. diff --git a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/start-stop-a-cluster.md b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/start-stop-a-cluster.md index e631f79ac..815295572 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/cluster-management/start-stop-a-cluster.md +++ b/docs/user_docs/kubeblocks-for-postgresql/cluster-management/start-stop-a-cluster.md @@ -1,6 +1,7 @@ --- title: Stop/Start a PostgreSQL cluster description: How to start/stop a PostgreSQL cluster +keywords: [postgresql, stop a cluster, start a cluster] sidebar_position: 5 sidebar_label: Stop/Start --- @@ -9,9 +10,11 @@ sidebar_label: Stop/Start You can stop/start a cluster to save computing resources. When a cluster is stopped, the computing resources of this cluster are released, which means the pods of Kubernetes are released, but the storage resources are reserved. Start this cluster again if you want to restore the cluster resources from the original storage by snapshots. +## Stop a cluster + ### Option 1. (Recommended) Use kbcli -Configure the name of your cluster and run the command below to stop this cluster. +Configure the name of your cluster and run the command below to stop this cluster. ```bash kbcli cluster stop @@ -26,6 +29,7 @@ kbcli cluster stop pg-cluster ### Option 2. Create an OpsRequest Run the command below to stop a cluster. + ```bash kubectl apply -f - < diff --git a/docs/user_docs/kubeblocks-for-postgresql/configuration/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/configuration/_category_.yml index cec02728b..b19dea834 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/configuration/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/configuration/_category_.yml @@ -1,4 +1,4 @@ -position: 2 +position: 3 label: Configuration collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/configuration/configuration.md b/docs/user_docs/kubeblocks-for-postgresql/configuration/configuration.md new file mode 100644 index 000000000..d000b014d --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/configuration/configuration.md @@ -0,0 +1,305 @@ +--- +title: Configure cluster parameters +description: Configure cluster parameters +keywords: [postgresql, parameter, configuration, reconfiguration] +sidebar_position: 1 +--- + +# Configure cluster parameters + +The KubeBlocks configuration function provides a set of consistent default configuration generation strategies for all the databases running on KubeBlocks and also provides a unified parameter configuration interface to facilitate managing parameter reconfiguration, searching the parameter user guide, and validating parameter effectiveness. + +## Before you start + +1. [Install KubeBlocks](./../../installation/install-kubeblocks.md). +2. [Create a PostgreSQL cluster](./../cluster-management/create-and-connect-a-postgresql-cluster.md#create-a-postgresql-cluster) and wait until the cluster status is Running. + +## View parameter information + +View the current configuration file of a cluster. + +```bash +kbcli cluster describe-config pg-cluster +``` + +From the meta information, you can find the configuration files of this PostgreSQL cluster. + +You can also view the details of this configuration file and parameters. + +* View the details of the current configuration file. + + ```bash + kbcli cluster describe-config pg-cluster --show-detail + ``` + +* View the parameter description. + + ```bash + kbcli cluster explain-config pg-cluster |head -n 20 + ``` + +* View the user guide of a specified parameter. + + ```bash + kbcli cluster explain-config pg-cluster --param=max_connections + ``` + +
+ + Output + + ```bash + template meta: + ConfigSpec: postgresql-configuration ComponentName: postgresql ClusterName: pg-cluster + + Configure Constraint: + Parameter Name: max_connections + Allowed Values: [6-8388607] + Scope: Global + Dynamic: true + Type: integer + Description: Sets the maximum number of concurrent connections. + ``` +
+ + * Allowed Values: It defines the valid value range of this parameter. + * Dynamic: The value of `Dynamic` in `Configure Constraint` defines how the parameter reconfiguration takes effect. There are two different reconfiguration strategies based on the effectiveness type of modified parameters, i.e. **dynamic** and **static**. + * When `Dynamic` is `true`, it means the effectiveness type of parameters is **dynamic** and can be updated online. Follow the instructions in [Reconfigure dynamic parameters](#reconfigure-dynamic-parameters). + * When `Dynamic` is `false`, it means the effectiveness type of parameters is **static** and a pod restarting is required to make reconfiguration effective. Follow the instructions in [Reconfigure static parameters](#reconfigure-static-parameters). + * Description: It describes the parameter definition. + +## Reconfigure dynamic parameters + +The example below reconfigures `max_connections`. + +1. View the current values of `max_connections`. + + ```bash + kbcli cluster connect pg-cluster + ``` + + ```bash + postgres=# show max_connections; + max_connections + ----------------- + 100 + (1 row) + ``` + +2. Adjust the values of `max_connections`. + + ```bash + kbcli cluster configure pg-cluster --set=max_connections=200 + ``` + + :::note + + Make sure the value you set is within the Allowed Values of this parameter. If you set a value that does not meet the value range, the system prompts an error. For example, + + ```bash + kbcli cluster configure pg-cluster --set=max_connections=5 + error: failed to validate updated config: [failed to cue template render configure: [pg.acllog-max-len: invalid value 5 (out of bound 6-8388607): + 343:34 + ] + ] + ``` + + ::: + +3. View the status of the parameter reconfiguration. + + `Status.Progress` and `Status.Status` shows the overall status of the parameter reconfiguration and `Conditions` show the details. + + When the `Status.Status` shows `Succeed`, the reconfiguration is completed. + + ```bash + kbcli cluster describe-ops pg-cluster-reconfiguring-fq6q7 -n default + ``` + +
+ + Output + + ```bash + Spec: + Name: pg-cluster-reconfiguring-fq6q7 NameSpace: default Cluster: pg-cluster Type: Reconfiguring + + Command: + kbcli cluster configure pg-cluster --components=postgresql --config-spec=postgresql-configuration --config-file=postgresql.conf --set max_connections=100 --namespace=default + + Status: + Start Time: Mar 17,2023 19:25 UTC+0800 + Completion Time: Mar 17,2023 19:25 UTC+0800 + Duration: 2s + Status: Succeed + Progress: 1/1 + OBJECT-KEY STATUS DURATION MESSAGE + + Conditions: + LAST-TRANSITION-TIME TYPE REASON STATUS MESSAGE + Mar 17,2023 19:25 UTC+0800 Progressing OpsRequestProgressingStarted True Start to process the OpsRequest: pg-cluster-reconfiguring-fq6q7 in Cluster: pg-cluster + Mar 17,2023 19:25 UTC+0800 Validated ValidateOpsRequestPassed True OpsRequest: pg-cluster-reconfiguring-fq6q7 is validated + Mar 17,2023 19:25 UTC+0800 Reconfigure ReconfigureStarted True Start to reconfigure in Cluster: pg-cluster, Component: postgresql + Mar 17,2023 19:25 UTC+0800 ReconfigureMerged ReconfigureMerged True Reconfiguring in Cluster: pg-cluster, Component: postgresql, ConfigSpec: postgresql-configuration, info: updated: map[postgresql.conf:{"max_connections":"200"}], added: map[], deleted:map[] + Mar 17,2023 19:25 UTC+0800 ReconfigureSucceed ReconfigureSucceed True Reconfiguring in Cluster: pg-cluster, Component: postgresql, ConfigSpec: postgresql-configuration, info: updated policy: , updated: map[postgresql.conf:{"max_connections":"100"}], added: map[], deleted:map[] + Mar 17,2023 19:25 UTC+0800 Succeed OpsRequestProcessedSuccessfully True Successfully processed the OpsRequest: pg-cluster-reconfiguring-fq6q7 in Cluster: pg-cluster + ``` + +
+ +4. Connect to the database to verify whether the parameters are modified. + + The whole searching process has a 30-second delay since it takes some time for kubelet to synchronize modifications to the volume of the pod. + + ```bash + kbcli cluster connect pg-cluster + ``` + + ```bash + postgres=# show max_connections; + max_connections + ----------------- + 200 + (1 row) + ``` + +## Reconfigure static parameters + +The example below reconfigures `shared_buffers`. + +1. View the current values of `shared_buffers`. + + ```bash + kbcli cluster connect pg-cluster + ``` + + ```bash + postgres=# show shared_buffers; + shared_buffers + ---------------- + 256MB + (1 row) + ``` + +2. Adjust the values of `shared_buffers`. + + ```bash + kbcli cluster configure pg-cluster --set=shared_buffers=512M + ``` + + :::note + + Make sure the value you set is within the Allowed Values of this parameter. If you set a value that does not meet the value range, the system prompts an error. For example, + + ```bash + kbcli cluster configure pg-cluster --set=shared_buffers=5M + error: failed to validate updated config: [failed to cue template render configure: [pg.maxclients: invalid value 5 (out of bound 16-107374182): + 343:34 + ] + ] + ``` + + ::: + +3. View the status of the parameter reconfiguration. + + `Status.Progress` and `Status.Status` shows the overall status of the parameter reconfiguration and `Conditions` show the details. + + When the `Status.Status` shows `Succeed`, the reconfiguration is completed. + + ```bash + kbcli cluster describe-ops pg-cluster-reconfiguring-rcnzb -n default + ``` + +
+ + Output + + ```bash + Spec: + Name: pg-cluster-reconfiguring-rcnzb NameSpace: default Cluster: pg-cluster Type: Reconfiguring + + Command: + kbcli cluster configure pg-cluster --components=postgresql --config-spec=postgresql-configuration --config-file=postgresql.conf --set shared_buffers=512M --namespace=default + + Status: + Start Time: Mar 17,2023 19:31 UTC+0800 + Duration: 2s + Status: Running + Progress: 0/1 + OBJECT-KEY STATUS DURATION MESSAGE + + Conditions: + LAST-TRANSITION-TIME TYPE REASON STATUS MESSAGE + Mar 17,2023 19:31 UTC+0800 Progressing OpsRequestProgressingStarted True Start to process the OpsRequest: pg-cluster-reconfiguring-rcnzb in Cluster: pg-cluster + Mar 17,2023 19:31 UTC+0800 Validated ValidateOpsRequestPassed True OpsRequest: pg-cluster-reconfiguring-rcnzb is validated + Mar 17,2023 19:31 UTC+0800 Reconfigure ReconfigureStarted True Start to reconfigure in Cluster: pg-cluster, Component: postgresql + Mar 17,2023 19:31 UTC+0800 ReconfigureMerged ReconfigureMerged True Reconfiguring in Cluster: pg-cluster, Component: postgresql, ConfigSpec: postgresql-configuration, info: updated: map[postgresql.conf:{"shared_buffers":"512M"}], added: map[], deleted:map[] + Mar 17,2023 19:31 UTC+0800 ReconfigureRunning ReconfigureRunning True Reconfiguring in Cluster: pg-cluster, Component: postgresql, ConfigSpec: postgresql-configuration + ``` + +
+ +4. Connect to the database to verify whether the parameters are modified. + + The whole searching process has a 30-second delay since it takes some time for kubelete to synchronize modifications to the volume of the pod. + + ```bash + kbcli cluster connect pg-cluster + ``` + + ```bash + postgres=# show shared_buffers; + shared_buffers + ---------------- + 512MB + (1 row) + ``` + +## View history and compare differences + +After the reconfiguration is completed, you can search the reconfiguration history and compare the parameter differences. + +View the parameter reconfiguration history. + +```bash +kbcli cluster describe-config pg-cluster +``` + +
+ +Output + +```bash +ConfigSpecs Meta: +CONFIG-SPEC-NAME FILE ENABLED TEMPLATE CONSTRAINT RENDERED COMPONENT CLUSTER +postgresql-configuration kb_restore.conf false postgresql-configuration postgresql14-cc pg-cluster-postgresql-postgresql-configuration postgresql pg-cluster +postgresql-configuration pg_hba.conf false postgresql-configuration postgresql14-cc pg-cluster-postgresql-postgresql-configuration postgresql pg-cluster +postgresql-configuration postgresql.conf true postgresql-configuration postgresql14-cc pg-cluster-postgresql-postgresql-configuration postgresql pg-cluster +postgresql-configuration kb_pitr.conf false postgresql-configuration postgresql14-cc pg-cluster-postgresql-postgresql-configuration postgresql pg-cluster +postgresql-custom-metrics custom-metrics.yaml false postgresql-custom-metrics pg-cluster-postgresql-postgresql-custom-metrics postgresql pg-cluster + +History modifications: +OPS-NAME CLUSTER COMPONENT CONFIG-SPEC-NAME FILE STATUS POLICY PROGRESS CREATED-TIME VALID-UPDATED +pg-cluster-reconfiguring-fq6q7 pg-cluster postgresql postgresql-configuration postgresql.conf Succeed 1/1 Mar 17,2023 19:25 UTC+0800 {"postgresql.conf":"{\"max_connections\":\"100\"}"} +pg-cluster-reconfiguring-bm84z pg-cluster postgresql postgresql-configuration postgresql.conf Succeed 1/1 Mar 17,2023 19:27 UTC+0800 {"postgresql.conf":"{\"max_connections\":\"200\"}"} +pg-cluster-reconfiguring-cbqxd pg-cluster postgresql postgresql-configuration postgresql.conf Succeed 1/1 Mar 17,2023 19:35 UTC+0800 {"postgresql.conf":"{\"max_connections\":\"500\"}"} +pg-cluster-reconfiguring-rcnzb pg-cluster postgresql postgresql-configuration postgresql.conf Succeed restart 1/1 Mar 17,2023 19:38 UTC+0800 {"postgresql.conf":"{\"shared_buffers\":\"512MB\"}"} +``` + +
+ +From the above results, there are three parameter modifications. + +Compare these modifications to view the configured parameters and their different values for different versions. + +```bash +kbcli cluster diff-config pg-cluster-reconfiguring-bm84z pg-cluster-reconfiguring-rcnzb +> +DIFF-CONFIG RESULT: + ConfigFile: postgresql.conf TemplateName: postgresql-configuration ComponentName: postgresql ClusterName: pg-cluster UpdateType: update + +PARAMETERNAME PG-CLUSTER-RECONFIGURING-BM84Z PG-CLUSTER-RECONFIGURING-RCNZB +max_connections 200 500 +shared_buffers 256MB 512MB +``` diff --git a/docs/user_docs/kubeblocks-for-mysql/observability/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/high-availability/_category_.yml similarity index 64% rename from docs/user_docs/kubeblocks-for-mysql/observability/_category_.yml rename to docs/user_docs/kubeblocks-for-postgresql/high-availability/_category_.yml index ad61062eb..56d9ca671 100644 --- a/docs/user_docs/kubeblocks-for-mysql/observability/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/high-availability/_category_.yml @@ -1,4 +1,4 @@ position: 5 -label: Observability +label: High availability collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/high-availability/high-availability.md b/docs/user_docs/kubeblocks-for-postgresql/high-availability/high-availability.md new file mode 100644 index 000000000..14b1ca303 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/high-availability/high-availability.md @@ -0,0 +1,116 @@ +--- +title: High Availability for PostgreSQL +description: High availability for a PostgreSQL cluster +keywords: [postgresql, high availability] +sidebar_position: 1 +--- + +# High availability + +KubeBlocks integrates [the open-source Patroni solution](https://patroni.readthedocs.io/en/latest/) to realize high availability and adopts Noop as the switch policy. + +## Before you start + +* [Install `kbcli`](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* [Create a PostgreSQL PrimarySecondary cluster](./../cluster-management/create-and-connect-a-postgresql-cluster.md#create-a-postgresql-cluster). +* Check the Switch Policy and the role probe. + * Check whether the switch policy is `Noop`. + + ```bash + kubectl get cluster pg-cluster -o yaml + > + spec: + componentSpecs: + - name: postgresql + componentDefRef: postgresql + switchPolicy: + type: Noop + ``` + + * Check whether the following role probe parameters exist to verify the role probe is enabled. + + ```bash + kubectl get cd postgresql -o yaml + > + probes: + roleProbe: + failureThreshold: 3 + periodSeconds: 2 + timeoutSeconds: 1 + ``` + +## Steps + +1. View the initial status of the PostgreSQL cluster. + + ```bash + kbcli cluster describe pg-cluster + ``` + + ![PostgreSQL cluster original status](../../../img/pgsql-ha-before.png) + + Currently, `pg-cluster-postgresql-0` is the primary pod and `pg-cluster-postgresql-1` is the secondary pod. + +2. Simulate a primary pod exception. + + ```bash + # Enter the primary pod + kubectl exec -it pg-cluster-postgresql-0 -- bash + + # Delete the data directory of PostgreSQL to simulate an exception + root@postgres-postgresql-0:/home/postgres# rm -fr /home/postgres/pgdata/pgroot/data + ``` + +3. View logs to observe how the roles of pods switch when an exception occurs. + + ```bash + # View the primary pod logs + kubectl logs pg-cluster-postgresql-0 + ``` + + In the logs, the leader lock is released from the primary pod and an HA switch occurs. + + ```bash + 2023-04-18 08:06:52,338 INFO: Lock owner: pg-cluster-postgresql-0; I am pg-cluster-postgresql-0 + 2023-04-18 08:06:52,460 INFO: Leader key released + 2023-04-18 08:06:52,552 INFO: released leader key voluntarily as data dir empty and currently leader + 2023-04-18 08:06:52,553 INFO: Lock owner: pg-cluster-postgresql-1; I am pg-cluster-postgresql-0 + 2023-04-18 08:06:52,553 INFO: trying to bootstrap from leader 'pg-cluster-postgresql-1' + ``` + + ```bash + # View secondary pod logs + kubectl logs pg-cluster-postgresql-1 + ``` + + In the logs, the original secondary pod has obtained the lock and become the leader. + + ```bash + 2023-04-18 08:07:14,441 INFO: no action. I am (pg-cluster-postgresql-1), the leader with the lock + 2023-04-18 08:07:24,354 INFO: no action. I am (pg-cluster-postgresql-1), the leader with the lock + ``` + +4. Connect to the PostgreSQL cluster to view the replication information. + + ```bash + kbcli cluster connect pg-cluster + ``` + + ```bash + postgres=# select * from pg_stat_replication; + ``` + + ![PostgreSQL replication info](../../../img/pgsql-ha-pg_stat_replication.png) + + From the output, `pg-cluster-postgresql-0` has been assigned as the secondary's pod. + +5. Describe the cluster and check the instance role. + + ```bash + kbcli cluster describe pg-cluster + ``` + + ![PostgreSQL cluster status after HA](../../../img/pgsql-ha-after.png) + + After the failover, `pg-cluster-postgresql-0` becomes the secondary pod and `pg-cluster-postgresql-1` becomes the primary pod. diff --git a/docs/user_docs/kubeblocks-for-postgresql/introduction/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/introduction/_category_.yml new file mode 100644 index 000000000..12b891496 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/introduction/_category_.yml @@ -0,0 +1,4 @@ +position: 1 +label: Introduction +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/introduction/introduction.md b/docs/user_docs/kubeblocks-for-postgresql/introduction/introduction.md new file mode 100644 index 000000000..3427e09e2 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/introduction/introduction.md @@ -0,0 +1,21 @@ +--- +title: PostgreSQL introduction +description: PostgreSQL introduction +keywords: [postgresql, introduction] +sidebar_position: 1 +--- + +# PostgreSQL introduction + +PostgreSQL is a powerful, scalable, secure, and customizable open-source relational database management system that is suitable for various applications and environments. In addition to the PostgreSQL core features, the KubeBlocks PostgreSQL cluster supports a number of extensions and users can also add the extensions by themselves. + +* PostGIS: PostGIS is an open-source spatial database extension that adds geographic information system (GIS) functionality to the PostgreSQL database. It provides a set of spatial functions and types that can be used for storing, querying, and analyzing geospatial data, allowing you to run location queries with SQL. +* pgvector: Pgvector is a PostgreSQL extension that supports vector data types and vector operations. It provides an efficient way to store and query vector data, supporting various vector operations such as similarity search, clustering, classification, and recommendation systems. Pgvector can be used to store AI model embeddings, adding persistent memory to AI. +* pg_trgm: The pg_trgm module provides functions and operators for determining the similarity of alphanumeric text based on trigram matching, as well as index operator classes that support fast searching for similar strings. +* postgres_fdw: The postgres_fdw extension can map tables from a remote database to a local table in PostgreSQL, allowing users to query and manipulate the data through the local database. + +**Reference** + +* [PostgreSQL features](https://www.postgresql.org/about/featurematrix/) +* [PostGIS](https://postgis.net/) +* [pgvector](https://github.com/pgvector/pgvector) diff --git a/docs/user_docs/kubeblocks-for-kafka/_category_.yml b/docs/user_docs/kubeblocks-for-postgresql/migration/_category_.yml similarity index 62% rename from docs/user_docs/kubeblocks-for-kafka/_category_.yml rename to docs/user_docs/kubeblocks-for-postgresql/migration/_category_.yml index ba7bded4e..483453084 100644 --- a/docs/user_docs/kubeblocks-for-kafka/_category_.yml +++ b/docs/user_docs/kubeblocks-for-postgresql/migration/_category_.yml @@ -1,4 +1,4 @@ position: 6 -label: KubeBlocks for Kafka +label: Migration collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/migration/feature-and-limit-list.md b/docs/user_docs/kubeblocks-for-postgresql/migration/feature-and-limit-list.md new file mode 100644 index 000000000..71bb33da1 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/migration/feature-and-limit-list.md @@ -0,0 +1,57 @@ +--- +title: Full feature and limit list +description: The full feature and limit list of KubeBlocks migration function for PostgreSQL +keywords: [postgresql, migration, migrate data in PostgreSQL to KubeBlocks, full feature, limit] +sidebar_position: 1 +sidebar_label: Full feature and limit list +--- + +# Full feature and limit list + +## Full feature list + +* Precheck + * Database connection + * Database version + * Whether the incremental migration is supported by a database + * The existence of the table structure + * Whether the table structure of the source database is supported +* Structure initialization + * PostgreSQL + * Table Struct + * Table Constraint + * Table Index + * Table Comment + * Table Sequence +* Data initialization + * Supports all major data types +* Incremental data migration + * Supports all major data types + * Support the resumable upload capability of eventual consistency + +## Limit list + +* Overall limits + * If the incremental data migration is used, the source database should enable CDC (Change Data Capture) related configurations (both are checked and blocked in precheck). For detailed configurations, see [Configure the source database](./migration_postgresql.md#configure-the-source). + * A table without a primary key is not supported. And a table with a foreign key is not supported (both are checked and blocked in precheck). + * Except for the incremental data migration module, other modules do not support resumable upload, i.e. if an exception occurs in this module, such as pod failure caused by downtime and network disconnection, a re-migration is required. + * During the data transmission task, DDL on the migration objects in the source database is not supported. + * The table name and field name cannot contain Chinese characters and special characters like a single quotation mark (') and a comma (,). + * During the migration process, the PrimarySecondary switchover in the source library is not supported, which may cause the connection string specified in the task configuration to change. This further causes the migration link failure. +* Permission limits + * The source account + * LOGIN + * The read permission of the source migration objects + * REPLICATION + * The sink account + * LOGIN + * The read/write permission of the sink database +* Precheck module: None +* Init-struct module + * The Array data type is not supported, such as text[], text[3][3], integer[]. + * The user-defined type is not supported. + * The database character set other than UTF-8 is not supported. +* Init-data module + * Character sets of the source and sink databases should be the same. +* Data incremental migration module + * Character sets of the source and sink databases should be the same. diff --git a/docs/user_docs/kubeblocks-for-postgresql/migration/migration_postgresql.md b/docs/user_docs/kubeblocks-for-postgresql/migration/migration_postgresql.md new file mode 100644 index 000000000..0285fc301 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-postgresql/migration/migration_postgresql.md @@ -0,0 +1,307 @@ +--- +title: Migrate data in PostgreSQL to KubeBlocks +description: How to migrate data in PostgreSQL v14 to KubeBlocks by kbcli migration +keywords: [postgresql, migration, kbcli migration, migrate data in PostgreSQL to KubeBlocks] +sidebar_position: 2 +sidebar_label: Migration +--- + +# Migrate data in PostgreSQL to KubeBlocks + +## Environment and aim + +* Source: PostgreSQL version 14 installed by Pigsty on Alibaba Cloud ECS. PostGIS plugin is also installed and the open-source map data is imported by osm2pgsql. + +* Sink: PostgreSQL version 14.7.0 installed by KubeBlocks on AWS EKS. No plugin is installed. + +* Aim: Migrate db_test.public.table_test_1 and db_test.public.table_test2 in the source to the sink. + +## Before you start + +### Enable kbcli migration + +1. [Install KubeBlocks](./../../installation/install-kubeblocks.md). +2. [Enable the migration add-on](./../../installation/enable-addons.md). + + ```bash + kbcli addon list + + kbcli addon enable migration + ``` + +### Configure the source + +Modify the configuration of the source to support CDC. + +1. Set 'wal_level' configuration to 'logical'. +2. Make sure that the number of 'max_replication_slots' configured is sufficient. +3. Make sure that the number of 'max_wal_senders' configured is sufficient. + +:::note + +If you install PostgreSQL by Pigsty, you can modify the configuration by executing the `pg edit-config` command. + +::: + +### Enable public network access to the source + +```bash +# Other hosts can access the source +vim /pg/data/pg_hba.conf + +# Add the following configuration +# IPv4 host connections: +host all all 0.0.0.0/0 md5 +``` + +:::note + +* Configure the above parameters based on your actual environment, especially for the allowlist address of pg_hba.conf. +* Modifying WAL (Write Ahead Log) and remote access requires restarting the database. Make sure the configuration modification is performed during off-peak hours. + +::: + +### Check the account permission + +Make sure both the source and sink account meet the following permissions. + +* The source account + * LOGIN permission + * The read permission of the source migration objects + * REPLICATION permission +* The sink account + * LOGIN permission + * The read/write permission of the sink + +### Initialize the sink + +1. Create a database named `db_test`. + + ```bash + create database db_test; + ``` + +2. Install PostGIS and import osm data. + + * [Install PostGIS](https://postgis.net/install/). If you install PostgreSQL by Pigsty, PostGIS is built-in and you can execute `CREATE EXTENSION` according to your need. + * [Import osm data](https://github.com/openstreetmap/osm2pgsql). + + :::note + + For the migration function in KubeBlocks version 5.0, there are limits for the structure initialization. + + 1. User-defined types are not supported. + 2. A field filled with an Array data type (such as text[], text[3][3], and integer[]) is not supported for migration. + + ::: + +### Prepare data sampling + +It is recommended to prepare data sampling for verification after the migration to ensure correctness. + +## Migrate data + +### Steps + +1. Create a migration task. + + ```bash + kbcli migration create mytask --template apecloud-pg2pg \ + --source user:123456@127.0.0.1:5432/db_test \ + --sink user:123456@127.0.0.2:5432/db_test \ + --migration-object '"public.table_test_1","public.table_test_2"' + ``` + + :paperclip: Table 1. Options explanation + + | Option | Descriprion | + | :--------- | :---------- | + | mystask | The name of the migration task. You can customize it. | + | `--template` | It specifies the migration template. `--template apecloud-pg2pg` stands for this migration task uses the template of migrating from PostgreSQL to PostgreSQL created by KubeBlocks. Run `kbcli migration templates` to view all available templates and the supported database information. | + | `--source` | It specifies the source. `user:123456@127.0.0.1:5432/db_test` in the above example stands for `${user_name}:${password}@${database connection url}/${database}`. For this guide, the connect URL uses the public network address. | + | `--sink` | It specifies the destination. `user:123456@127.0.0.2:5432/db_test` in the above example stands for `${user_name}:${password}@${database connection url}/${database}`. For this guide, the connection URL uses the service address inside the Kubernetes cluster. | + | `--migration-object` | It specifies the migration object. The above example describes data in "public.table_test_1" and "public.table_test_2", including structure data, stock data, and incremental data generated during running migration task, will be migrated to the sink. | + + :::note + + An example of the `--sink` URL: + + ![Sink](../../../img/pgsql-migration-sink.png) + + ::: + +2. (Optional) Specify migration steps by the option `--steps`. + + The default steps follow the order precheck -> structure initialization -> data initialization -> incremental migration. You can use `--steps` to specify migration steps. For example, perform tasks in the order of precheck, full initialization, and incremental migration. + + ```bash + kbcli migration create mytask --template apecloud-pg2pg \ + --source user:123456@127.0.0.1:5432/db_test \ + --sink user:123456@127.0.0.2:5432/db_test \ + --migration-object '"public.table_test_1","public.table_test_2"' + --steps precheck=true,init-struct=false,init-data=true,cdc=true + ``` + +3. View the task status. + + ```bash + # View the migration task list + kbcli migration list + + # View the details of a specified task + kbcli migration describe ${migration-task-name} + ``` + + ![Describe migration task](../../../img/pgsql-migration-describe-task.png) + + Pay attention to Initialization, CDC, and CDC Metrics. + + * Initialization + * Precheck: If the status shows `Failed`, it means the initialization precheck does not pass. Troubleshoot the initialization by [the following examples in troubleshooting](#troubleshooting). + * Init-struct: Structure initialization. Idempotent processing logic is adopted. A failure occurs only when a severe problem occurs, such as failing to connect a database. + * Init-data: Data initialization. If there are a large amount of stock data, it takes a long time to perform this step and you should pay attention to Status. + * CDC: Incremental migration. Based on the timestamp recorded by the system before the init-data step, the system starts data migration following eventual consistency and performs capturing the source library WAL (Write Ahead Log) changes -> writing to the sink. Under normal circumstances, the CDC phase continues if the migration link is not actively terminated. + * CDC Metrics: Incremental migration indicators. Currently, the indicators mainly provide the WAL LSN (Log Sequencer Number) of the source library and the corresponding timestamp (note that the timestamp shows the local time zone of the Pod Container runtime) when the CDC process has completed "capturing -> writing" process. + + :::note + + The CDC Metrics are updated every 10 minutes by the system, i.e. if there exists continuous data writing into the source, metrics.timestamp here delays 10 minutes compared with the current time. + + ::: + +4. Validate the migration with the prepared data sampling. + +### Troubleshooting + +If any step above fails, run the command below to troubleshoot the failure. + +```bash +# --step: Specify the step. Allow values: precheck,init-struct,init-data,cdc +kbcli migration logs ${migration-task-name} --step ${step-name} +``` + +## Switch applications + +### Before you start + +* Make sure the KubeBlocks migration task runs normally. +* To differentiate the dialogue information and to improve data security, it is recommended to create and authorize another account dedicated to data migration. +* For safety concerns, it is necessary to stop the business write and switch the application during off-peak hours. +* Before switching the application, it is recommended to prepare data sampling for verification after switching to ensure correctness. +* Pay attention to serial, sequence, and slot. +* Serial and sequence + + Search and record the max. value of Sequence before switching applications and set it as the initial value of Sequence in the sink. + + After the business is switched to the sink, the new written Sequence does not take the max. value in the source as the initial value to continue in an increment order by default. You need to set it manually. + + ```bash + # PostgreSQL Function Sample: + + CREATE FUNCTION build_setvals() returns void + AS $$ + declare + nsp name; + rel name; + val int8; + begin + for nsp,rel in select nspname,relname from pg_class t2 , pg_namespace t3 where t2.relnamespace=t3.oid and t2.relkind='S' + loop + execute format($_$select last_value from %I.%I$_$, nsp, rel) into val; + raise notice '%', + format($_$select setval('%I.%I'::regclass, %s);$_$, nsp, rel, val+1); + end loop; + end; + $$ + LANGUAGE plpgsql; + + # Execute: + psql -hxx -p xx -U xx -d xx -c "set client_min_messages = notice; select build_setvals();" | grep setval + + # Output like: + NOTICE: select setval('public.seq_test_1'::regclass, 2); + NOTICE: select setval('public.seq_test_2'::regclass, 1001); + NOTICE: select setval('public.seq_test_3'::regclass, 203); + NOTICE: select setval('public.seq_test_4'::regclass, 476); + + # Execute the above script in the sink + ``` + +* Slot lifecycle + + The CDC phase (the incremental migration) relies on the slot. A replication slot and publication are created during the CDC phase and these metadata should be deleted first before deleting the migration task metadata. + + If the incremental migration is performed, the migration task creates a replication slot named after the task name during the initialization phase of the init-data step (hyphens, "-", in the task name are replaced with underlines, "_"). When the incremental migration starts, the migration task creates a publication with the previous replication slot name and "_publication_for_all_tables" in the name to perform WAL consumption. + + When the migration task is deleted (running `kbcli migration terminate`), this task changes to the Terminating status. The termination operation first stops the CDC process, then tries to clear the above replication slot and publication. Only when the above metadata are cleared, is the metadata of the migration task deleted. + + :::note + + If the slot is not cleaned up, it affects the log-cleaning logic in PostgreSQL. When the PostgreSQL disk is insufficient and a redundant slot prevents the log from being cleaned up, a serious failure may occur. + + Therefore, the migration task can be deleted only when the slot cleanup is completed. If there is a PostgreSQL task that keeps the Terminating status for a long time, you need to pay attention to the status of the slot and disk water level and intervene manually when necessary. + + Cleanup operations adopt the idempotent logic, and the reasons for general cleanup failures include but are not limited to the following: + + * While the migration task is running, the connection string of the source library changes, which causes the migration task cannot connect to the source. + * While the migration task is running, the account and password of the source change, which causes the migration task cannot connect to the source. + * While the migration task is running, the permissions of the source library account change, which causes the migration task cannot be deleted. + + ::: + +### Steps + +1. Check the migration task status and ensure the task is performed normally. + 1. Describe the migration task details and all steps in Initialization are `Complete` and CDC is `Running`. + + ```bash + kbcli migration describe ${migration-task-name} + ``` + + 2. Under the prerequisite that there exists continuous write into the source, observe whether the timestamp is still in progress and whether there is almost no delay. For example, + + ```bash + kbcli migration logs ${migration-task-name} --step cdc | grep current_position + ``` + + The results update every minute. + + ![Timestamp](../../../img/pgsql-migration-timestamp.png) +2. Pause the business and stop new business data from being written in the source. +3. View the migration status again and ensure the migration task runs normally, lasting at least one minute. + + Refer to the operations in step 1 and observe whether the link is normal and the timestamp meets the expectation. +4. Use the sink to restore the business. +5. Validate the switch with the prepared data sampling. + +## Clean up the environment + +After the migration task is completed, you can terminate the migration task and function. + +### Terminate the migration task + +Deleting the migration task does not affect the data in the source and sink. + +```bash +kbcli migration terminate ${migration-task-name} +``` + +### Terminate kbcli migration + +1. Check whether there are running migration tasks. + + ```bash + kbcli migration list + ``` + +2. Disable the migration add-on. + + ```bash + kbcli addon disable migration + ``` + +3. Delete the Kubernetes CRD (Custom Resource Definition) manually. + + ```bash + kubectl delete crd migrationtasks.datamigration.apecloud.io migrationtemplates.datamigration.apecloud.io serialjobs.common.apecloud.io + ``` diff --git a/docs/user_docs/kubeblocks-for-postgresql/observability/access-logs.md b/docs/user_docs/kubeblocks-for-postgresql/observability/access-logs.md deleted file mode 100644 index 16cd33dd8..000000000 --- a/docs/user_docs/kubeblocks-for-postgresql/observability/access-logs.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -title: Access logs -description: How to access cluster log files -sidebar_position: 3 ---- - -# Access logs -The KubeBlocks log enhancement function aims to simplify the complexities of troubleshooting. With kbcli, the command line tool of KubeBlocks, you can view all kinds of logs generated by the database clusters running on KubeBlocks, such as slow logs, error logs, audit logs, and the container running logs (Stdout and Stderr). -The KubeBlocks log enhancement function uses methods similar to kubectl exec and kubectl logs to ensure a self-closed loop and lightweight. - -## Before you start - -- The container image supports `tail` and `xargs` commands. -- KubeBlocks Operator is installed on the target Kubernetes cluster. - -## Steps - -1. Enable the log enhancement function. - - If you create a cluster by running the `kbcli cluster create` command, add the `--enable-all-logs=true` option to enable the log enhancement function. When this option is `true`, all the log types defined by `spec.components.logConfigs` in `ClusterDefinition` are enabled automatically. - - ```bash - kbcli cluster create pg-cluster --cluster-definition='postgresql' --enable-all-logs=true - ``` - - If you create a cluster by applying a YAML file, add the log type you need in `spec.components.enabledLogs`. As for PostgreSQL, running log is supported. - - ```YAML - apiVersion: apps.kubeblocks.io/v1alpha1 - kind: Cluster - metadata: - name: pg-cluster - namespace: default - spec: - clusterDefinitionRef: postgresql-cluster-definition - appVersionRef: appversion-postgresql-latest - components: - - name: replicasets - type: replicasets - enabledLogs: - - running - ``` - -2. View the supported logs. - - Run the `kbcli cluster list-logs` command to view the enabled log types of the target cluster and the log file details. INSTANCE of each node is displayed. - - ***Example*** - - ```bash - kbcli cluster list-logs pg-cluster - > - INSTANCE LOG-TYPE FILE-PATH SIZE LAST-WRITTEN COMPONENT - pg-cluster-postgresql-0-0 running /postgresql/data/log/postgresql-2023-03-03.csv 6.9K Mar 03, 2023 07:17 (UTC+00:00) postgresql - pg-cluster-postgresql-0-0 running /postgresql/data/log/postgresql-2023-03-03.log 326 Mar 03, 2023 07:17 (UTC+00:00) postgresql - ``` - -3. Access the cluster log file. - - Run the `kbcli cluster logs ` command to view the details of the target log file generated by the target instance on the target cluster. You can use different options to view the log file details you need. - You can also run `kbcli cluster logs -h` to see the examples and option descriptions. - ```bash - kbcli cluster logs -h - > - Access cluster log file - - Examples: - # Return snapshot logs from cluster mycluster with default primary instance (stdout) - kbcli cluster logs mycluster - - # Display only the most recent 20 lines from cluster mycluster with default primary instance (stdout) - kbcli cluster logs --tail=20 mycluster - - # Return snapshot logs from cluster mycluster with specify instance my-instance-0 (stdout) - kbcli cluster logs mycluster --instance my-instance-0 - - # Return snapshot logs from cluster mycluster with specify instance my-instance-0 and specify container - # my-container (stdout) - kbcli cluster logs mycluster --instance my-instance-0 -c my-container - - # Return slow logs from cluster mycluster with default primary instance - kbcli cluster logs mycluster --file-type=slow - - # Begin streaming the slow logs from cluster mycluster with default primary instance - kbcli cluster logs -f mycluster --file-type=slow - - # Return the specify file logs from cluster mycluster with specify instance my-instance-0 - kbcli cluster logs mycluster --instance my-instance-0 --file-path=/var/log/yum.log - - # Return the specify file logs from cluster mycluster with specify instance my-instance-0 and specify - # container my-container - kbcli cluster logs mycluster --instance my-instance-0 -c my-container --file-path=/var/log/yum.log - ``` - -4. (Optional) Troubleshooting. - - The log enhancement function does not affect the core process of KubeBlocks. If a configuration exception occurs, a warning shows to help troubleshoot. - `warning` is recorded in the `event` and `status.Conditions` of the target database cluster. - - View `warning` information. - - Run `kbcli cluster describe ` to view the status of the target cluster. You can also run `kbcli cluster list events ` to view the event information of the target cluster directly. - - - Run `kubectl describe cluster ` to view the warning. - - ***Example*** - - ``` - Status: - Cluster Def Generation: 3 - Components: - Replicasets: - Phase: Running - Conditions: - Last Transition Time: 2022-11-11T03:57:42Z - Message: EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Reason: EnableLogsListValidateFail - Status: False - Type: ValidateEnabledLogs - Observed Generation: 2 - Operations: - Horizontal Scalable: - Name: replicasets - Restartable: - replicasets - Vertical Scalable: - replicasets - Phase: Running - Events: - Type Reason Age From Message - ---- ------ ---- ---- ------- - Normal Creating 49s cluster-controller Start Creating in Cluster: release-name-error - Warning EnableLogsListValidateFail 49s cluster-controller EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets - Normal Running 36s cluster-controller Cluster: release-name-error is ready, current phase is Running - ``` \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/observability/alert.md b/docs/user_docs/kubeblocks-for-postgresql/observability/alert.md deleted file mode 100644 index ef6089db5..000000000 --- a/docs/user_docs/kubeblocks-for-postgresql/observability/alert.md +++ /dev/null @@ -1,139 +0,0 @@ ---- -title: Configure IM alert -description: How to enable IM alert -sidebar_position: 2 ---- - -# Configure IM alert - -Alerts are mainly used for daily error response to improve system availability. Kubeblocks has a set of built-in common alert rules and integrates multiple notification channels. The alert capability of Kubeblocks can meet the operation and maintenance requirements of production-level online clusters. - -## Alert rules - -KubeBlocks has a set of general built-in alter rules to meet the alert needs of different types of data products and provides an out-of-the-box experience without further configurations. These alert rules provide the best practice for cluster operation and maintenance. These alarm rules further improve alert accuracy and reduce the probability of false negatives and false positives through experience-based smoothing windows, alarm thresholds, alarm levels, and alarm indicators. - -Taking PostgreSQL as an example, the alert rules have built-in common abnormal events, such as instance down, instance restart, slow query, connection amount, deadlock, and cache hit rate. -The following example shows PostgreSQL alert rules (refer to [Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/) for syntax). When the amount of active connections exceeds 80% of the threshold and lasts for 2 minutes, Prometheus triggers a warning and sends it to AlertManager. - -```bash -alert: PostgreSQLTooManyConnections - expr: | - sum by (namespace,app_kubernetes_io_instance,pod) (pg_stat_activity_count{datname!~"template.*|postgres"}) - > on(namespace,app_kubernetes_io_instance,pod) - (pg_settings_max_connections - pg_settings_superuser_reserved_connections) * 0.8 - for: 2m - labels: - severity: warning - annotations: - summary: "PostgreSQL too many connections (> 80%)" - description: "PostgreSQL has too many connections and the value is {{ $value }}. (instance: {{ $labels.pod }})" -``` - -You can view all the built-in alert rules in **Alerts Tab** of **Prometheus Dashboards**. Run the commands below to open Prometheus Dashboards. - -```bash -# View dashboards list -kbcli dashboard list - -# Open Prometheus Dashboards -kbcli dashboard open kubeblocks-prometheus-server # Here is an example and fill in the actual name based on the above dashboard list -``` - -## Configure IM alert - -The alert message notification of Kubeblocks mainly adopts the AlertManager native capability. After receiving the Prometheus alarm, KubeBlocks performs multiple steps, including deduplication, grouping, silence, suppression, and routing, and finally sends it to the corresponding notification channel. -AlertManager integrates a set of notification channels, such as Email and Slack. Kubeblocks extends new IM class notification channels with AlertManger Webhook. - -This tutorial takes configuring Feishu as the notification channel as an example. - -### Before you start - -To receive alerts, you need to deploy monitoring components and enable cluster monitoring first. Refer to [Monitor database](monitor-database.md) for details. - -### Step 1. Configure alert channels - -Configure the notification channels in advance based on your needs and obtain the necessary information for the following steps. -Taking Feishu as an example, you can obtain the webhook address after creating a custom robot. If the signature verification in the security configuration is enabled, you can obtain the signature key in advance. - -Currently, Feishu custom bot, DingTalk custom bot, WeChat Enterprise custom bot, and Slack are supported. You can refer to the following guides to configure notification channels. - -* [Feishu custom bot](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN) -* [DingTalk custom bot](https://open.dingtalk.com/document/orgapp/custom-robot-access) -* [WeChat Enterprise custom bot](https://developer.work.weixin.qq.com/document/path/91770) -* [Slack](https://api.slack.com/messaging/webhooks) - -:::note - -* Each notification channel has its interface call amount and frequency limits and when the limits are reached, the channel limits traffic and alerts are not sent out. -* The SLA of the service provided by a single channel cannot guarantee the alerts are sent successfully. Therefore, it is recommended to configure multiple notification channels to ensure availability. - -::: - -### Step 2. Configure the receiver - -To improve usability, `kbcli` develops the `alert` subcommand to simplify the receiver configuration. You can set the notification channels and receivers by the `alert` subcommand. This subcommand also supports condition filters, such as cluster names and severity levels. After the configuration succeeds, it takes effect without restarting the service. - -Add an alert receiver. - - ```bash - kbcli alert add-receiver --webhook='xxx' --cluster=xx --severity=xx - ``` - -***Example*** - - The following commands show how to add a receiver to Feishu based on different requirements. - The webhook address below is an example and you need to replace it with the actual address before running the command. - - ```bash - # Signature authentication is disabled - kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' - - # Signature authentication is enabled and sign is used as the value of token - kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=sign' - - # Only receive the alerts from a cluster named pg - kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=pg - - # Only receive the critical alerts from a cluster named pg - kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=pg --severity=critical - ``` - -:::note - -For the detailed command description, run `kbcli alert add-receiver -h`. - -::: - -Run the command below to view the notification configurations. - - ```bash - kbcli alert list-receivers - ``` - -Run the command below to delete the notification channel and receiver if you want to disable the alert function. - - ```bash - kbcli alert delete-receiver - ``` - -## Troubleshooting - -If you cannot receive alert notices, run the commands below to get the logs of AlertManager and AlertManager-Webhook-Adaptor add-ons. - -```bash -# Find the corresponding Pod of AlertManager and get Pod name -kubectl get pods -n kb-system -l 'release=kubeblocks,app=prometheus,component=alertmanager' - -# Search AlertManeger logs -kubectl logs -n kb-system -c prometheus-alertmanager - -# Find the corresponding Pod of AlertManager-Webhook-Adaptor and get Pod name -kubectl get pods -n kb-system -l 'app.kubernetes.io/instance=kubeblocks,app.kubernetes.io/name=alertmanager-webhook-adaptor' - -# Search AlertManager-Webhook-Adaptor logs -kubectl logs -n kb-system -c alertmanager-webhook-adaptor -``` \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-postgresql/observability/monitor-database.md b/docs/user_docs/kubeblocks-for-postgresql/observability/monitor-database.md deleted file mode 100644 index 903d892f9..000000000 --- a/docs/user_docs/kubeblocks-for-postgresql/observability/monitor-database.md +++ /dev/null @@ -1,103 +0,0 @@ ---- -title: Monitor database -description: How to monitor your database -sidebar_position: 1 ---- - -# Observability of KubeBlocks -With the built-in database observability, you can observe the database health status and track and measure your database in real-time to optimize database performance. This section shows you how database observability works with KubeBlocks and how to use the function. - -## Enable database monitor - -***Steps:*** - -1. Install KubeBlocks and the monitoring add-ons are installed by default. - - ```bash - kbcli kubeblocks install - ``` - - If you do not want to enable the monitoring add-ons when installing KubeBlocks, set `--monitor` to cancel the add-on installation. But it is not recommended to disable the monitoring function. - - ```bash - kbcli kubeblocks install --monitor=false - ``` - - If you have installed KubeBlocks without the monitoring add-ons, you can use `kbcli addon` to enable the monitoring add-ons. To ensure the completeness of the monitoring function, it is recommended to enable three monitoring add-ons. - - ```bash - # View all add-ons supported - kbcli addon list - - # Enable prometheus add-on - kbcli addon enable prometheus - - # Enable granfana add-on - kbcli addon enable granfana - - # Enable alertmanager-webhook-adaptor add-on - kbcli addon enable alertmanager-webhook-adaptor - ``` - - :::note - - Refer to [Enable add-ons](./../../installation/enable-add-ons.md) for details. - - ::: - -2. View the Web Console of the monitoring components. - - Run the command below to view the Web Console list of the monitoring components after the components are installed. - ```bash - kbcli dashboard list - > - NAME NAMESPACE PORT CREATED-TIME - kubeblocks-grafana default 3000 Jan 13,2023 10:53 UTC+0800 - kubeblocks-prometheus-alertmanager default 9093 Jan 13,2023 10:53 UTC+0800 - kubeblocks-prometheus-server default 9090 Jan 13,2023 10:53 UTC+0800 - ``` - For the Web Console list returned by the above command, if you want to view the Web Console of a specific monitoring component, run the command below and this command enables the port-forward of your local host and opens the default browser: - ```bash - kbcli dashboard open - ``` -3. Enable the database monitoring function. - - The monitoring function is enabled by default when a database is created. The open-source or customized Exporter is injected after the monitoring function is enabled. This Exporter can be found by Prometheus server automatically and scrape monitoring indicators at regular intervals. - - For a new cluster, run the command below to create a database cluster. - ```bash - # Search the cluster definition - kbcli clusterdefinition list - - # Create a cluster - kbcli cluster create --cluster-definition='xxx' - ``` - - ***Example*** - - ```bash - kbcli cluster create pg-cluster --cluster-definition='postgresql' - ``` - - :::note - - The setting of `monitor` is `true` by default and it is not recommended to disable it. For example, - ```bash - kbcli cluster create mycluster --cluster-definition='postgresql' --monitor=false - ``` - - ::: - - You can change the value to `false` to disable the monitor function if required. - - For the existing cluster, you can update it to enable the monitor function with `update` command. - - ```bash - kbcli cluster update --monitor=true - ``` - - ***Example*** - - ```bash - kbcli cluster update pg-cluster --monitor=true - ``` - -You can view the dashboard of the corresponding cluster via Grafana Web Console. For more detailed information, see [Grafana documentation](https://grafana.com/docs/grafana/latest/dashboards/). \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-redis/backup-and-restore/_category_.yml b/docs/user_docs/kubeblocks-for-redis/backup-and-restore/_category_.yml new file mode 100644 index 000000000..ff724d53b --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/backup-and-restore/_category_.yml @@ -0,0 +1,4 @@ +position: 4 +label: Backup and Restore +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-redis/backup-and-restore/snapshot-backup-and-restore-for-redis.md b/docs/user_docs/kubeblocks-for-redis/backup-and-restore/snapshot-backup-and-restore-for-redis.md new file mode 100644 index 000000000..3c6665326 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/backup-and-restore/snapshot-backup-and-restore-for-redis.md @@ -0,0 +1,127 @@ +--- +title: Snapshot backup and restore for Redis +description: Guide for backup and restore for Redis +keywords: [redis, snapshot, backup, restore] +sidebar_position: 2 +sidebar_label: Snapshot backup and restore +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Snapshot backup and restore for Redis + +This section shows how to use `kbcli` to back up and restore a Redis cluster. + +***Steps:*** + +1. Install KubeBlocks and the snapshot-controller add-on. + + ```bash + kbcli kubeblocks install --set snapshot-controller.enabled=true + ``` + + If you have installed KubeBlocks without enabling the snapshot-controller, run the command below. + + ```bash + kbcli kubeblocks upgrade --set snapshot-controller.enabled=true + ``` + + Since your `kubectl` is already connected to the cluster of cloud Kubernetes service, this command installs the latest version of KubeBlocks in the default namespace `kb-system` in your environment. + + Verify the installation with the following command. + + ```bash + kubectl get pod -n kb-system + ``` + + The pod with `kubeblocks` and `kb-addon-snapshot-controller` is shown. See the information below. + + ```bash + NAME READY STATUS RESTARTS AGE + kubeblocks-5c8b9d76d6-m984n 1/1 Running 0 9m + kb-addon-snapshot-controller-6b4f656c99-zgq7g 1/1 Running 0 9m + ``` + + If the output result does not show `kb-addon-snapshot-controller`, it means the snapshot-controller add-on is not enabled. It may be caused by failing to meet the installable condition of this add-on. Refer to [Enable add-ons](./../../installation/enable-addons.md) to find the environment requirements and then enable the snapshot-controller add-on. + +2. Configure cloud managed Kubernetes environment to support the snapshot function. For ACK and GKE, the snapshot function is enabled by default, you can skip this step. + + + + + The backup is realized by the volume snapshot function, you need to configure EKS to support the snapshot function. + + Configure the storage class of the snapshot (the assigned EBS volume is gp3). + + ```yaml + kubectl create -f - < + + + + Configure the default volumesnapshot class. + + ```yaml + kubectl create -f - < + + +3. Create a snapshot backup. + + ```bash + kbcli cluster backup redis-cluster + ``` + +4. Check the backup. + + ```bash + kbcli cluster list-backups + ``` + +5. Restore to a new cluster. + + Copy the backup name to the clipboard, and restore to the new cluster. + + :::note + + You do not need to specify other parameters for creating a cluster. The restoration automatically reads the parameters of the source cluster, including specification, disk size, etc., and creates a new Redis cluster with the same specifications. + + ::: + + Execute the following command. + + ```bash + kbcli cluster restore redis-new-from-snapshot --backup backup-default-redis-cluster-20230411115450 + ``` diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/_category_.yml b/docs/user_docs/kubeblocks-for-redis/cluster-management/_category_.yml new file mode 100644 index 000000000..200a2feb5 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/_category_.yml @@ -0,0 +1,4 @@ +position: 2 +label: Cluster Management +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/create-and-connect-a-redis-cluster.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/create-and-connect-a-redis-cluster.md new file mode 100644 index 000000000..7937e261f --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/create-and-connect-a-redis-cluster.md @@ -0,0 +1,206 @@ +--- +title: Create and connect to a Redis Cluster +description: How to create and connect to a Redis cluster +keywords: [redis, create a redis cluster, connect to a redis cluster, cluster, redis sentinel] +sidebar_position: 1 +sidebar_label: Create and connect +--- + +# Create and Connect to a Redis cluster + +KuebBlocks for Redis supports Standalone clusters and PrimarySecondary clusters. + +But for your better high-availability experience, KubeBlocks creates a Redis PrimarySecondary by default. + +## Create a Redis cluster + +### Before you start + +* [Install `kbcli`](./../../installation/install-kbcli.md). +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* Make sure the PostgreSQL addon is installed with `kbcli addon list`. + + ```bash + kbcli addon list + > + NAME TYPE STATUS EXTRAS AUTO-INSTALL INSTALLABLE-SELECTOR + ... + redis Helm Enabled true + ... + ``` + +* View all the database types and versions available for creating a cluster. + + ```bash + kbcli clusterversion list + ``` + +### (Recommended) Create a cluster on a tainted node + +In actual scenarios, you are recommended to create a cluster on nodes with taints and customized specifications. + +1. Taint your node. + + :::note + + If you have already some tainted nodes, you can skip this step. + + ::: + + 1. Get Kubernetes nodes. + + ```bash + kubectl get node + ``` + + 2. Place taints on the selected nodes. + + ```bash + kubectl taint nodes =true:NoSchedule + ``` + +2. Create a Redis cluster. + + The cluster creation command is simply `kbcli cluster create`. Use tolerances to deploy it on the tainted node. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + + To create a cluster with specified class, you can use `--set` flag and specify your requirement. + + ```bash + kbcli cluster create redis-cluster --cluster-definition=redis --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --set type=redis,cpu=1,memory=1Gi,replicas=2,storage=10Gi,storageClass= --set type=redis-sentinel,cpu=1,memory=1Gi,replicas=3,storage=1Gi,storageClass= --namespace + ``` + + Or change the corresponding parameters in the YAML file. + + ```bash + kbcli cluster create redis-cluster --cluster-definition=redis --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' --namespace --set-file -< + accessModes: + - ReadWriteOnce + resources: + requests: + cpu: 200m + memory: 500Mi + storage: 30Gi + - name: redis-sentinel + replicas: 3 + componentDefRef: redis-sentinel + volumeClaimTemplates: + - name: data + spec: + storageClassName: + accessModes: + - ReadWriteOnce + resources: + requests: + cpu: 100m + memory: 500Mi + storage: 30Gi + EOF + ``` + +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose a database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this function is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
`kbcli cluster create redis-cluster --cluster-definition redis --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a Redis cluster with default settings. + +```bash +kbcli cluster create redis-cluster --cluster-definition=redis --tolerations '"key=taint1name,value=true,operator=Equal,effect=NoSchedule","key=taint2name,value=true,operator=Equal,effect=NoSchedule"' +``` + +### Create a cluster on a node without taints + +The cluster creation command is simply `kbcli cluster create`. Further, you are recommended to create a cluster with a specified class and customize your cluster settings as demanded. + +To create a cluster with a specified class, you can use the `--set` flag and specify your requirement. + +```bash +kbcli cluster create redis-cluster --cluster-definition=redis --set type=redis,cpu=1,memory=1Gi,replicas=2,storage=10Gi,storageClass= --set type=redis-sentinel,cpu=1,memory=1Gi,replicas=3,storage=1Gi,storageClass= --namespace +``` + +Or you can directly change the corresponding parameters in the YAML file. + + ```bash + kbcli cluster create redis-cluster --cluster-definition=redis --namespace --set-file -< + accessModes: + - ReadWriteOnce + resources: + requests: + cpu: 200m + memory: 500Mi + storage: 30Gi + - name: redis-sentinel + replicas: 3 + componentDefRef: redis-sentinel + volumeClaimTemplates: + - name: data + spec: + storageClassName: + accessModes: + - ReadWriteOnce + resources: + requests: + cpu: 100m + memory: 500Mi + storage: 30Gi + EOF + ``` + +See the table below for detailed descriptions of customizable parameters, setting the `--termination-policy` is necessary, and you are strongly recommended turn on the monitor and enable all logs. + +📎 Table 1. kbcli cluster create flags description + +| Option | Description | +|:-----------------------|:------------------------| +| `--cluster-definition` | It specifies the cluster definition. You can choose the database type. Run `kbcli cd list` to show all available cluster definitions. | +| `--cluster-version` | It specifies the cluster version. Run `kbcli cv list` to show all available cluster versions. If you do not specify a cluster version when creating a cluster, the latest version is applied by default. | +| `--enable-all-logs` | It enables you to view all application logs. When this function is enabled, enabledLogs of component level will be ignored. For logs settings, refer to [Access Logs](./../../observability/access-logs.md). | +| `--help` | It shows the help guide for `kbcli cluster create`. You can also use the abbreviated `-h`. | +| `--monitor` | It is used to enable the monitor function and inject metrics exporter. It is set as true by default. | +| `--node-labels` | It is a node label selector. Its default value is [] and means empty value. If you want set node labels, you can follow the example format:
`kbcli cluster create redis-cluster --cluster-definition=redis --node-labels='"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"'` | +| `--set` | It sets the cluster resource including CPU, memory, replicas, and storage, each set corresponds to a component. For example, `--set cpu=1000m,memory=1Gi,replicas=1,storage=10Gi`. | +| `--set-file` | It uses a yaml file, URL, or stdin to set the cluster resource. | +| `--termination-policy` | It specifies how a cluster is deleted. Set the policy when creating a cluster. There are four available values, namely `DoNotTerminate`, `Halt`, `Delete`, and `WipeOut`. `Delete` is set as the default.
- `DoNotTerminate`: DoNotTerminate blocks the delete operation.
- `Halt`: Halt deletes workload resources such as statefulset, deployment workloads but keeps PVCs.
- `Delete`: Delete is based on Halt and deletes PVCs.
- `WipeOut`: WipeOut is based on Delete and wipes out all volume snapshots and snapshot data from backup storage location. | + +If no flags are used and no information is specified, you create a Redis cluster with default settings. + +```bash +kbcli cluster create redis-cluster --cluster-definition=redis +``` + +## Connect to a Redis Cluster + +```bash +kbcli cluster connect --namespace +``` + +For the detailed database connection guide, refer to [Connect database](./../../connect_database/overview-of-database-connection.md). diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/delete-a-redis-cluster.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/delete-a-redis-cluster.md new file mode 100644 index 000000000..acfca365f --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/delete-a-redis-cluster.md @@ -0,0 +1,53 @@ +--- +title: Delete a Redis Cluster +description: How to delete a Redis Cluster +keywords: [redis, delete a cluster, delete protection] +sidebar_position: 6 +sidebar_label: Delete protection +--- + +# Delete a Redis Cluster + +## Termination policy + +:::note + +The termination policy determines how a cluster is deleted. + +::: + +| **terminationPolicy** | **Deleting Operation** | +|:-- | :-- | +| `DoNotTerminate` | `DoNotTerminate` blocks delete operation. | +| `Halt` | `Halt` deletes workload resources such as statefulset, deployment workloads but keep PVCs. | +| `Delete` | `Delete` deletes workload resources and PVCs but keep backups. | +| `WipeOut` | `WipeOut` deletes workload resources, PVCs and all relevant resources included backups. | + +To check the termination policy, execute the following command. + +```bash +kbcli cluster list +``` + +***Example*** + +```bash +kbcli cluster list redis-cluster +> +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +redis-cluster default redis redis-7.0.6 Delete Running Apr 10,2023 20:27 UTC+0800 +``` + +## Step + +Configure the cluster name and run the command below to delete a specified cluster. + +```bash +kbcli cluster delete +``` + +***Example*** + +```bash +kbcli cluster delete redis-cluster +``` diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/expand-volume.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/expand-volume.md new file mode 100644 index 000000000..cd0b06faa --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/expand-volume.md @@ -0,0 +1,120 @@ +--- +title: Expand volume +description: How to expand the volume of a Redis cluster +keywords: [redis, expand volume] +sidebar_position: 3 +sidebar_label: Expand volume +--- + +# Expand volume + +You can expand the storage volume size of each pod. + +:::note + +Volume expansion triggers a concurrent restart and the leader pod may change after the operation. + +::: + +## Before you start + +Check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. + +```bash +kbcli cluster list +``` + +***Example*** + +```bash +kbcli cluster list redis-cluster +> +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +redis-cluster default redis redis-7.0.6 Delete Running Apr 10,2023 19:00 UTC+0800 +``` + +## Steps + +1. Change configuration. There are 3 ways to apply volume expansion. + + **Option 1**. (**Recommended**) Use kbcli + + Configure the values of `--components`, `--volume-claim-templates`, and `--storage`, and run the command below to expand the volume. + + ```bash + kbcli cluster volume-expand redis-cluster --components="redis" \ + --volume-claim-templates="data" --storage="2Gi" + ``` + + - `--components` describes the component name for volume expansion. + - `--volume-claim-templates` describes the VolumeClaimTemplate names in components. + - `--storage` describes the volume storage size. + + **Option 2**. Create an OpsRequest + + Run the command below to expand the volume of a cluster. + + ```bash + kubectl apply -f - < + ``` + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + redis-cluster default redis redis-7.0.6 Delete VolumeExpanding Apr 10,2023 16:27 UTC+0800 + ``` + + - STATUS=VolumeExpanding: it means the volume expansion is in progress. + - STATUS=Running: it means the volume expansion operation has been applied. diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/handle-an-exception.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/handle-an-exception.md new file mode 100644 index 000000000..f85050838 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/handle-an-exception.md @@ -0,0 +1,42 @@ +--- +title: Handle an exception +description: How to handle an exception in a Redis cluster +keywords: [redis, exception] +sidebar_position: 7 +sidebar_label: Handle an exception +--- + +# Handle an exception + +When there is an exception during your operation, you can perform the following procedure to solve it. + +## Steps + +1. Check the cluster status. Specify the name of the cluster you want to check and run the command below. + + ```bash + kbcli cluster list + ``` + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + ``` + +2. Handle the exception according to the status information. + + | **Status** | **Information** | + | :--- | :--- | + | Abnormal | The cluster can be accessed but exceptions occur in some pods. This might be a mediate status of the operation process and the system recovers automatically without executing any extra operation. Wait until the cluster status is Running. | + | ConditionsError | The cluster is normal but an exception occurs to the condition. It might be caused by configuration loss or exception, which further leads to operation failure. Manual recovery is required. | + | Failed | The cluster cannot be accessed. Check the `status.message` string and get the exception reason. Then manually recover it according to the hints. | + + You can check the cluster's status for more information. + +## Fallback strategies + +If the above operation can not solve the problem, try the following steps: + +- Restart this cluster. If the restart fails, you can delete the pod manually. +- Roll the cluster status back to the status before changes. diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/restart-a-redis-cluster.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/restart-a-redis-cluster.md new file mode 100644 index 000000000..eb7d73803 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/restart-a-redis-cluster.md @@ -0,0 +1,73 @@ +--- +title: Restart a Redis cluster +description: How to restart a Redis cluster +keywords: [redis, restart] +sidebar_position: 4 +sidebar_label: Restart +--- + +# Restart a Redis cluster + +You can restart all pods of the cluster. When an exception occurs in a database, you can try to restart it. + +:::note + +Restarting a Redis cluster triggers a concurrent restart and the leader may change after the cluster restarts. + +::: + +## Steps + +1. Restart a cluster. + + You can use `kbcli` or create an OpsRequest to restart a cluster. + + **Option 1.** (**Recommended**) Use kbcli + + Configure the values of `components` and `ttlSecondsAfterSucceed` and run the command below to restart a specified cluster. + + ```bash + kbcli cluster restart redis-cluster --components="redis" \ + --ttlSecondsAfterSucceed=30 + ``` + + - `components` describes the component name that needs to be restarted. + - `ttlSecondsAfterSucceed` describes the time to live of an OpsRequest job after the restarting succeeds. + + **Option 2.** Create an OpsRequest + + Run the command below to restart a cluster. + + ```bash + kubectl apply -f - < + ``` + + - STATUS=Restarting: it means the cluster restart is in progress. + - STATUS=Running: it means the cluster has been restarted. + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + redis-cluster default redis redis-7.0.x Delete Running Apr 10,2023 19:20 UTC+0800 + ``` diff --git a/docs/user_docs/kubeblocks-for-redis/cluster-management/scale-for-a-redis-cluster.md b/docs/user_docs/kubeblocks-for-redis/cluster-management/scale-for-a-redis-cluster.md new file mode 100644 index 000000000..c7c6ce95b --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/cluster-management/scale-for-a-redis-cluster.md @@ -0,0 +1,301 @@ +--- +title: Scale for a Redis cluster +description: How to scale a Redis cluster, horizontal scaling, vertical scaling +keywords: [redis, horizontal scaling, vertical scaling, scale] +sidebar_position: 2 +sidebar_label: Scale +--- + +# Scale for a Redis cluster + +You can scale Redis DB instances in two ways, vertical scaling and horizontal scaling. + +## Vertical scaling + +You can vertically scale a cluster by changing resource requirements and limits (CPU and storage). For example, if you need to change the resource demand from 1C2G to 2C4G, vertical scaling is what you need. + +:::note + +During the vertical scaling process, a concurrent restart is triggered and the leader pod may change after the restarting. + +::: + +### Before you start + +Run the command below to check whether the cluster STATUS is `Running`. Otherwise, the following operations may fail. + +```bash +kbcli cluster list +``` + +***Example*** + +```bash +kbcli cluster list redis-cluster +> +NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME +redis-cluster default redis redis-7.0.6 Delete Running Apr 10,2023 16:21 UTC+0800 +``` + +### Steps + +1. Change configuration. There are 3 ways to apply vertical scaling. + + **Option 1.** (**Recommended**) Use kbcli + + Configure the parameters `--components`, `--memory`, and `--cpu` and run the command. + + ***Example*** + + ```bash + kbcli cluster vscale redis-cluster \ + --components="redis" \ + --memory="4Gi" --cpu="2" \ + ``` + + - `--components` describes the component name ready for vertical scaling. + - `--memory` describes the requested and limited size of the component memory. + - `--cpu` describes the requested and limited size of the component CPU. + + **Option 2.** Create an OpsRequest + + Apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + + ```bash + kubectl apply -f - < + ``` + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + redis-cluster default redis redis-7.0.6 Delete VerticalScaling Apr 10,2023 16:27 UTC+0800 + ``` + + - STATUS=VerticalScaling: it means the vertical scaling is in progress. + - STATUS=Running: it means the vertical scaling operation has been applied. + - STATUS=Abnormal: it means the vertical scaling is abnormal. The reason may be the normal instances number is less than the total instance number or the leader instance is running properly while others are abnormal. + > To solve the problem, you can check manually to see whether resources are sufficient. If AutoScaling is supported, the system recovers when there are enough resources, otherwise, you can create enough resources and check the result with kubectl describe command. + +## Horizontal scaling + +Horizontal scaling changes the amount of pods. For example, you can apply horizontal scaling to scale up from three pods to five pods. The scaling process includes the backup and restoration of data. + +### Before you start + +- Refer to [Backup and restore for Redis](./../backup-and-restore/snapshot-backup-and-restore-for-redis.md) to make sure the EKS environment is configured properly since the horizontal scaling relies on the backup function. +- Check whether the cluster status is `Running`. Otherwise, the following operations may fail. + + ```bash + kbcli cluster list + ``` + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + redis-cluster default redis redis-7.0.6 Delete Running Apr 10,2023 16:50 UTC+0800 + ``` + +### Steps + +1. Change configuration. There are 3 ways to apply horizontal scaling. + + **Option 1.** (**Recommended**) Use kbcli + + Configure the parameters `--components` and `--replicas`, and run the command. + + ***Example*** + + ```bash + kbcli cluster hscale redis-cluster \ + --components="redis" --replicas=2 + ``` + + - `--components` describes the component name ready for vertical scaling. + - `--replicas` describes the replicas with the specified components. + + **Option 2.** Create an OpsRequest + + Apply an OpsRequest to the specified cluster. Configure the parameters according to your needs. + + ```bash + kubectl apply -f - < + ``` + + ***Example*** + + ```bash + kbcli cluster list redis-cluster + > + NAME NAMESPACE CLUSTER-DEFINITION VERSION TERMINATION-POLICY STATUS CREATED-TIME + redis-cluster default redis redis-7.0.6 Delete HorizontalScaling Apr 10,2023 16:58 UTC+0800 + ``` + + - STATUS=HorizontalScaling: it means horizontal scaling is in progress. + - STATUS=Running: it means horizontal scaling has been applied. + +### Handle the snapshot exception + +If `STATUS=ConditionsError` occurs during the horizontal scaling process, you can find the cause from `cluster.status.condition.message` for troubleshooting. +In the example below, a snapshot exception occurs. + +```bash +Status: + conditions: + - lastTransitionTime: "2023-04-10T18:20:26Z" + message: VolumeSnapshot/redis-cluster-redis-scaling-dbqgp: Failed to set default snapshot + class with error cannot find default snapshot class + reason: ApplyResourcesFailed + status: "False" + type: ApplyResources +``` + +***Reason*** + +This exception occurs because the `VolumeSnapshotClass` is not configured. This exception can be fixed after configuring `VolumeSnapshotClass`, but the horizontal scaling cannot continue to run. It is because the wrong backup (volumesnapshot is generated by backup) and volumesnapshot generated before still exist. Delete these two wrong resources and then KubeBlocks re-generates new resources. + +***Steps:*** + +1. Configure the VolumeSnapshotClass by running the command below. + + ```bash + kubectl create -f - < +``` + +***Example*** + +```bash +kbcli cluster stop redis-cluster +``` + +### Option 2. Create an OpsRequest + +Run the command below to stop a cluster. +```bash +kubectl apply -f - < +``` + +***Example*** + +```bash +kbcli cluster start redis-cluster +``` + +### Option 2. Create an OpsRequest + +Run the command below to start a cluster. + +```yaml +kubectl apply -f - < + Output + + ```bash + template meta: + ConfigSpec: redis-replication-config ComponentName: redis ClusterName: redis-cluster + + Configure Constraint: + Parameter Name: acllog-max-len + Allowed Values: [1-10000] + Scope: Global + Dynamic: true + Type: integer + Description: + ``` + + + + * Allowed Values: It defines the valid value range of this parameter. + * Dynamic: The value of `Dynamic` in `Configure Constraint` defines how the parameter reconfiguration takes effect. There are two different reconfiguration strategies based on the effectiveness type of modified parameters, i.e. **dynamic** and **static**. + * When `Dynamic` is `true`, it means the effectiveness type of parameters is **dynamic** and can be updated online. Follow the instructions in [Reconfigure dynamic parameters](#reconfigure-dynamic-parameters). + * When `Dynamic` is `false`, it means the effectiveness type of parameters is **static** and a pod restarting is required to make reconfiguration effective. Follow the instructions in [Reconfigure static parameters](#reconfigure-static-parameters). + * Description: It describes the parameter definition. + +## Reconfigure dynamic parameters + +The example below reconfigures `acllog-max-len`. + +1. View the current values of `acllog-max-len`. + + ```bash + kbcli cluster connect redis-cluster + ``` + + ```bash + 127.0.0.1:6379> config get parameter acllog-max-len + 1) "acllog-max-len" + 2) "128" + ``` + +2. Adjust the values of `acllog-max-len`. + + ```bash + kbcli cluster configure redis-cluster --component=redis --set=acllog-max-len=256 + ``` + + :::note + + Make sure the value you set is within the Allowed Values of this parameter. If you set a value that does not meet the value range, the system prompts an error. For example, + + ```bash + kbcli cluster configure redis-cluster --set=acllog-max-len=1000000 + > + error: failed to validate updated config: [failed to cue template render configure: [configuration."acllog-max-len": 2 errors in empty disjunction: + configuration."acllog-max-len": conflicting values 128 and 1000000: + 20:43 + 155:16 + configuration."acllog-max-len": invalid value 1000000 (out of bound <=10000): + 20:32 + ] + ] + ``` + + ::: + +3. View the status of the parameter reconfiguration. + + `Status.Progress` and `Status.Status` shows the overall status of the parameter reconfiguration and `Conditions` show the details. + + When the `Status.Status` shows `Succeed`, the reconfiguration is completed. + + ```bash + kbcli cluster describe-ops redis-cluster-reconfiguring-zjztm -n default + ``` + +
+ Output + + ```bash + Spec: + Name: redis-cluster-reconfiguring-zjztm NameSpace: default Cluster: redis-cluster Type: Reconfiguring + + Command: + kbcli cluster configure redis-cluster --components=redis --config-spec=redis-replication-config --config-file=redis.conf --set acllog-max-len=256 --namespace=default + + Status: + Start Time: Apr 17,2023 17:22 UTC+0800 + Duration: 10s + Status: Running + Progress: 0/1 + OBJECT-KEY STATUS DURATION MESSAGE + + Conditions: + LAST-TRANSITION-TIME TYPE REASON STATUS MESSAGE + Apr 17,2023 17:22 UTC+0800 Progressing OpsRequestProgressingStarted True Start to process the OpsRequest: redis-cluster-reconfiguring-zjztm in Cluster: redis-cluster + Apr 17,2023 17:22 UTC+0800 Validated ValidateOpsRequestPassed True OpsRequest: redis-cluster-reconfiguring-zjztm is validated + Apr 17,2023 17:22 UTC+0800 Reconfigure ReconfigureStarted True Start to reconfigure in Cluster: redis-cluster, Component: redis + Apr 17,2023 17:22 UTC+0800 ReconfigureRunning ReconfigureRunning True Reconfiguring in Cluster: redis-cluster, Component: redis, ConfigSpec: redis-replication-config + ``` + +
+ +4. Connect to the database to verify whether the parameters are modified. + + The whole searching process has a 30-second delay since it takes some time for kubelet to synchronize modifications to the volume of the pod. + + ```bash + kbcli cluster connect redis-cluster + ``` + + ```bash + 127.0.0.1:6379> config get parameter acllog-max-len + 1) "acllog-max-len" + 2) "256" + ``` + +## Reconfigure static parameters + +The example below reconfigures `maxclients` and `databases`. + +1. View the current values of `maxclients` and `databases`. + + ```bash + kbcli cluster connect redis-cluster + ``` + + ```bash + 127.0.0.1:6379> config get parameter maxclients databases + 1) "databases" + 2) "16" + 3) "maxclients" + 4) "10000" + ``` + +2. Adjust the values of `maxclients` and `databases`. + + ```bash + kbcli cluster configure redis-cluster --component=redis --set=maxclients=20000,databases=32 + ``` + + :::note + + Make sure the value you set is within the Allowed Values of this parameter. If you set a value that does not meet the value range, the system prompts an error. For example, + + ```bash + kbcli cluster configure redis-cluster --component=redis --set=maxclients=65001 + > + error: failed to validate updated config: [failed to cue template render configure: [configuration.maxclients: 2 errors in empty disjunction: + configuration.maxclients: conflicting values 65000 and 65001: + 100:37 + 155:16 + configuration.maxclients: invalid value 65001 (out of bound <=65000): + 100:26 + ] + ] + ``` + + ::: + +3. View the status of the parameter reconfiguration. + + `Status.Progress` and `Status.Status` shows the overall status of the parameter reconfiguration and `Conditions` show the details. + + When the `Status.Status` shows `Succeed`, the reconfiguration is completed. + + ```bash + kbcli cluster describe-ops redis-cluster-reconfiguring-zrkq7 -n default + ``` + +
+ Output + + ```bash + Spec: + Name: redis-cluster-reconfiguring-zrkq7 NameSpace: default Cluster: redis-cluster Type: Reconfiguring + + Command: + kbcli cluster configure redis-cluster --components=redis --config-spec=redis-replication-config --config-file=redis.conf --set databases=32 --set maxclients=20000 --namespace=default + + Status: + Start Time: Apr 17,2023 17:28 UTC+0800 + Duration: 2s + Status: Running + Progress: 0/1 + OBJECT-KEY STATUS DURATION MESSAGE + + Conditions: + LAST-TRANSITION-TIME TYPE REASON STATUS MESSAGE + Apr 17,2023 17:28 UTC+0800 Progressing OpsRequestProgressingStarted True Start to process the OpsRequest: redis-cluster-reconfiguring-zrkq7 in Cluster: redis-cluster + Apr 17,2023 17:28 UTC+0800 Validated ValidateOpsRequestPassed True OpsRequest: redis-cluster-reconfiguring-zrkq7 is validated + Apr 17,2023 17:28 UTC+0800 Reconfigure ReconfigureStarted True Start to reconfigure in Cluster: redis-cluster, Component: redis + Apr 17,2023 17:28 UTC+0800 ReconfigureMerged ReconfigureMerged True Reconfiguring in Cluster: redis-cluster, Component: redis, ConfigSpec: redis-replication-config, info: updated: map[redis.conf:{"databases":"32","maxclients":"20000"}], added: map[], deleted:map[] + Apr 17,2023 17:28 UTC+0800 ReconfigureRunning ReconfigureRunning True Reconfiguring in Cluster: redis-cluster, Component: redis, ConfigSpec: redis-replication-config + ``` + +
+ +4. Connect to the database to verify whether the parameters are modified. + + The whole searching process has a 30-second delay since it takes some time for kubelete to synchronize modifications to the volume of the pod. + + ```bash + kbcli cluster connect redis-cluster + ``` + + ```bash + 127.0.0.1:6379> config get parameter maxclients databases + 1) "databases" + 2) "32" + 3) "maxclients" + 4) "20000" + ``` + +## View history and compare differences + +After the reconfiguration is completed, you can search the reconfiguration history and compare the parameter differences. + +View the parameter reconfiguration history. + +```bash +kbcli cluster describe-config redis-cluster --component=redis +``` + +
+Output + +```bash +ConfigSpecs Meta: +CONFIG-SPEC-NAME FILE ENABLED TEMPLATE CONSTRAINT RENDERED COMPONENT CLUSTER +redis-replication-config redis.conf true redis7-config-template redis7-config-constraints redis-cluster-redis-redis-replication-config redis redis-cluster + +History modifications: +OPS-NAME CLUSTER COMPONENT CONFIG-SPEC-NAME FILE STATUS POLICY PROGRESS CREATED-TIME VALID-UPDATED +redis-cluster-reconfiguring-zjztm redis-cluster redis redis-replication-config redis.conf Succeed restart 1/1 Apr 17,2023 17:22 UTC+0800 +redis-cluster-reconfiguring-zrkq7 redis-cluster redis redis-replication-config redis.conf Succeed restart 1/1 Apr 17,2023 17:28 UTC+0800 {"redis.conf":"{\"databases\":\"32\",\"maxclients\":\"20000\"}"} +redis-cluster-reconfiguring-mwbnw redis-cluster redis redis-replication-config redis.conf Succeed restart 1/1 Apr 17,2023 17:35 UTC+0800 {"redis.conf":"{\"maxclients\":\"40000\"}"} +``` + +
+ +From the above results, there are three parameter modifications. + +Compare these modifications to view the configured parameters and their different values for different versions. + +```bash +kbcli cluster diff-config redis-cluster-reconfiguring-zrkq7 redis-cluster-reconfiguring-mwbnw +> +DIFF-CONFIG RESULT: + ConfigFile: redis.conf TemplateName: redis-replication-config ComponentName: redis ClusterName: redis-cluster UpdateType: update + +PARAMETERNAME REDIS-CLUSTER-RECONFIGURING-ZRKQ7 REDIS-CLUSTER-RECONFIGURING-MWBNW +maxclients 20000 40000 +``` diff --git a/docs/user_docs/kubeblocks-for-redis/high-availability/_category_.yml b/docs/user_docs/kubeblocks-for-redis/high-availability/_category_.yml new file mode 100644 index 000000000..8ce89efb2 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/high-availability/_category_.yml @@ -0,0 +1,4 @@ +position: 6 +label: High Availability +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/kubeblocks-for-redis/high-availability/high-availability.md b/docs/user_docs/kubeblocks-for-redis/high-availability/high-availability.md new file mode 100644 index 000000000..7973322b1 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-redis/high-availability/high-availability.md @@ -0,0 +1,105 @@ +--- +title: High Availability for Redis +description: High availability for a Redis cluster +keywords: [redis, high availability] +sidebar_position: 1 +--- + +# High availability + +KubeBlocks integrates [the official Redis Sentinel solution](https://redis.io/docs/management/sentinel/) to realize high availability and adopts Noop as the switch policy. + +Redis Sentinel is the high availability solution for a Redis PrimarySecondary, which is recommended by Redis and is also the main-stream solution in the community. + +In the Redis PrimarySecondary provided by KubeBlocks, Sentinel is deployed as an independent component. + +## Before you start + +* [Install KubeBlocks](./../../installation/install-kubeblocks.md). +* [Create a Redis PrimarySecondary](./../cluster-management/create-and-connect-a-redis-cluster.md#create-a-redis-cluster). +* Check the Switch Policy and the role probe. + * Check whether the switch policy is `Noop`. + + ```bash + kubectl get cluster redis-cluster -o yaml + > + spec: + componentSpecs: + - name: redis + componentDefRef: redis + switchPolicy: + type: Noop + ``` + + * Check whether the following role probe parameters exist to verify the role probe is enabled. + + ```bash + kubectl get cd redis -o yaml + > + probes: + roleProbe: + failureThreshold: 3 + periodSeconds: 2 + timeoutSeconds: 1 + ``` + +## Steps + +1. View the initial status of the Redis cluster. + + ```bash + kbcli cluster describe redis-cluster + ``` + + ![Redis cluster original status](../../../img/redis-ha-before.png) + + Currently, `redis-cluster-redis-0` is the primary pod and `redis-cluster-redis-1` is the secondary pod. + +2. Simulate a primary pod exception. + + ```bash + # Enter the primary pod + kubectl exec -it redis-cluster-redis-0 -- bash + + # Execute the debug sleep command to simulate a primary pod exception + root@redis-redis-0:/# redis-cli debug sleep 30 + ``` + +3. Open the Redis Sentinel log to view the failover. + + ```bash + kubectl logs redis-cluster-redis-sentinel-0 + ``` + + In the logs, we can view when a high-availability switch occurs. + + ```bash + 1:X 18 Apr 2023 06:13:17.072 # +switch-master redis-cluster-redis-sentinel redis-cluster-redis-0.redis-cluster-redis-headless.default.svc 6379 redis-cluster-redis-1.redis-cluster-redis-headless.default.svc 6379 + 1:X 18 Apr 2023 06:13:17.074 * +slave slave redis-cluster-redis-0.redis-cluster-redis-headless.default.svc:6379 redis-cluster-redis-0.redis-cluster-redis-headless.default.svc 6379 @ redis-cluster-redis-sentinel redis-cluster-redis-1.redis-cluster-redis-headless.default.svc 6379 + 1:X 18 Apr 2023 06:13:17.077 * Sentinel new configuration saved on disk + ``` + +4. Connect to the Redis cluster to view the primary pod information after the exception simulation. + + ```bash + kbcli cluster connect redis-cluster + ``` + + ```bash + # View the current primary pod + 127.0.0.1:6379> info replication + ``` + + ![Redis info replication](../../../img/redis-ha-info-replication.png) + + From the output, `redis-cluster-redis-1` has been assigned as the secondary's pod. + +5. Describe the cluster and check the instance role. + + ```bash + kbcli cluster describe redis-cluster + ``` + + ![Redis cluster status after HA](./../../../img/redis-ha-after.png) + + After the failover, `redis-cluster-redis-0` becomes the secondary pod and `redis-cluster-redis-1` becomes the primary pod. diff --git a/docs/user_docs/kubeblocks-for-vector-database/_category_.yml b/docs/user_docs/kubeblocks-for-vector-database/_category_.yml new file mode 100644 index 000000000..1e78ae842 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-vector-database/_category_.yml @@ -0,0 +1,4 @@ +position: 8 +label: KubeBlocks for Vector Database +collapsible: true +collapsed: true diff --git a/docs/user_docs/kubeblocks-for-vector-database/chatgpt-retrieval-plugin-with-kubeblocks.md b/docs/user_docs/kubeblocks-for-vector-database/chatgpt-retrieval-plugin-with-kubeblocks.md new file mode 100644 index 000000000..f8a800921 --- /dev/null +++ b/docs/user_docs/kubeblocks-for-vector-database/chatgpt-retrieval-plugin-with-kubeblocks.md @@ -0,0 +1,110 @@ +--- +title: ChatGPT Retrieval Plugin with KubeBlocks +description: Introduction and Installation of ChatGPT Retrieval Plugin on KubeBlocks +keywords: [ChatGPT retrieval plugin] +sidebar_position: 6 +sidebar_label: ChatGPT Retrieval Plugin +--- + +# ChatGPT Retrieval Plugin with KubeBlocks + +## What is ChatGPT Retrieval Plugin + +[ChatGPT Retrieval Plugin](https://github.com/openai/chatgpt-retrieval-plugin) is the official plugin from OpenAI, it provides a flexible solution for semantic search and retrieval of personal or organizational documents using natural language queries. + +The purpose of Plugin is to extend the OpenAI abilities from a public large model to a hybrid model with private data. It manages a private knowledge base to promise the privacy and safety, meanwhile, it can be accessed through a set of query APIs from the remote OpenAI GPT Chatbox. + +The remote Chatbox engages in dialogue with the user, processing their natural language queries and retrieving relevant data from both OpenAI's large model and our private knowledge base, and then combines this information to provide a more accurate and comprehensive answer. + +With the ChatGPT Retrieval Plugin, data privacy and benefits of ChatGPT are balanced. + +## ChatGPT Retrieval Plugin on KubeBlocks + +KubeBlocks makes two major improvements over ChatGPT Retrieval Plugin: + +- KubeBlocks provides a solid vector database for Plugin, and relieves the burden of database management. KubeBlocks supports a wide range of vector databases, such as Postgres, Redis, Milvus, Qdrant and Weaviate. +- KubeBlocks also builds multi-arch images for Plugin, integrates the Plugin as a native Cluster inside, you can configure the APIs, Secrets, Env vars configurable through command line and Helm Charts. + +These improvements achieve a better experience for running your own Plugin. + +## Install ChatGPT Retrieval Plugin on KubeBlocks + +***Before you start*** + +- Make sure you have the Plugin authorities from OpenAI. If don't, you can join the [waiting list here](https://openai.com/waitlist/plugins). +- An OPENAI_API_KEY for the Plugin to call OpenAI embedding APIs. +- [Install kbcli](./../installation/install-kbcli.md). +- [Install KubeBlocks](./../installation/install-kubeblocks.md). + +***Steps:*** + +1. List addons of KubeBlocks. Each vector database is an addon in KubeBlocks. + + ```bash + kbcli addon list + ``` + + This guide shows how to install ChatGPT Retrieval Plugin with Qdrant. If Qdrant is not enabled, enable it with the following command. + + ```bash + kbcli addon enable qdrant + ``` + +2. When enabled successfully, you can check it with `kbcli addon list` and `kubectl get clusterdefinition`. + + ```bash + kbcli addon list + > + NAME TYPE STATUS EXTRAS AUTO-INSTALL AUTO-INSTALLABLE-SELECTOR + qdrant Helm Enabled true + ``` + + ```bash + kubectl get clusterdefinition + > + NAME MAIN-COMPONENT-NAME STATUS AGE + qdrant qdrant Available 6m14s + ``` + +3. Create a Qdrant cluster with `kbcli cluster create --cluster-definition=qdrant`. + + ```bash + kbcli cluster create --cluster-definition=qdrant + > + Warning: cluster version is not specified, use the recently created ClusterVersion qdrant-1.1.0 + Cluster lilac26 created + ``` + + ***Result:*** + + In the above example, a qdrant standalone cluster named `Cluster lilac26` is created. + +4. Install the plugin with Qdrant as datastore with helm. + + ```bash + helm install gptplugin kubeblocks/chatgpt-retrieval-plugin \ + --set datastore.DATASTORE=qdrant \ + --set datastore.QDRANT_COLLECTION=document_chunks \ + --set datastore.QDRANT_URL=http://lilac26-qdrant-headless.default.svc.cluster.local \ + --set datastore.QDRANT_PORT=6333 \ + --set datastore.BEARER_TOKEN=your_bearer_token \ + --set datastore.OPENAI_API_KEY=your_openai_api_key \ + --set website.url=your_website_url + ``` + +5. Check whether the plugin is installed successfully. + + ```bash + kubectl get pods + > + NAME READY STATUS RESTARTS AGE + gptplugin-chatgpt-retrieval-plugin-647d85498d-jd2bj 1/1 Running 0 10m + ``` + +6. Port-forward the Plugin Portal to access it. + + ```bash + kubectl port-forward pod/gptplugin-chatgpt-retrieval-plugin-647d85498d-jd2bj 8081:8080 + ``` + +7. In your web browser, open the plugin portal with the address `http://127.0.0.1:8081/docs`. diff --git a/docs/user_docs/kubeblocks-for-postgresql/observability/_category_.yml b/docs/user_docs/observability/_category_.yml similarity index 80% rename from docs/user_docs/kubeblocks-for-postgresql/observability/_category_.yml rename to docs/user_docs/observability/_category_.yml index 48a71c6da..241be27f2 100644 --- a/docs/user_docs/kubeblocks-for-postgresql/observability/_category_.yml +++ b/docs/user_docs/observability/_category_.yml @@ -1,4 +1,4 @@ -position: 4 +position: 12 label: Observability collapsible: true collapsed: true \ No newline at end of file diff --git a/docs/user_docs/observability/access-logs.md b/docs/user_docs/observability/access-logs.md new file mode 100644 index 000000000..69110b70a --- /dev/null +++ b/docs/user_docs/observability/access-logs.md @@ -0,0 +1,156 @@ +--- +title: Access logs +description: How to access cluster log files +keywords: [access logs] +sidebar_position: 3 +--- + +# Access logs + +The KubeBlocks log enhancement function aims to simplify troubleshooting. With `kbcli`, the command line tool of KubeBlocks, you can view all kinds of logs generated by the database clusters running on KubeBlocks, such as slow logs, error logs, audit logs, and the container running logs (Stdout and Stderr). For Redis, only the running log is supported. + +The KubeBlocks log enhancement function uses methods similar to kubectl exec and kubectl logs to ensure a self-closed loop and lightweight. + +## Before you start + +- The container image supports `tail` and `xargs` commands. +- [Install KubeBlocks](./../installation/install-kubeblocks.md). +- In this guide, we take the MySQL engine as an example, the operation is the same for all database engines. + +## Steps + +1. Enable the log enhancement function. + + * Enable this function when creating a cluster. + + * If you create a cluster by running the `kbcli cluster create` command, add the `--enable-all-logs=true` option to enable the log enhancement function. When this option is `true`, all the log types defined by `spec.componentDefs.logConfigs` in `ClusterDefinition` are enabled automatically. + + ```bash + kbcli cluster create mycluster --cluster-definition='apecloud-mysql' --enable-all-logs=true + ``` + + * Update this cluster if you did not enable it when creating a cluster. + + ```bash + kbcli cluster update mycluster --enable-all-logs=true -n + ``` + + :::note + + The default namespace in which a cluster is created is `default`. If you specify a namespace when creating a cluster, fill in `` with your customized one. + + ::: + +2. View the supported logs. + + Run the `kbcli cluster list-logs` command to view the enabled log types of the target cluster and the log file details. INSTANCE of each node is displayed. + + ***Example*** + + ```bash + kbcli cluster list-logs mycluster + > + INSTANCE LOG-TYPE FILE-PATH SIZE LAST-WRITTEN COMPONENT + mycluster-mysql-0 error /data/mysql/log/mysqld-error.log 6.4K Feb 06, 2023 09:13 (UTC+00:00) mysql + mycluster-mysql-0 general /data/mysql/log/mysqld.log 5.9M Feb 06, 2023 09:13 (UTC+00:00) mysql + mycluster-mysql-0 slow /data/mysql/log/mysqld-slowquery.log 794 Feb 06, 2023 09:13 (UTC+00:00) mysql + ``` + +3. Access the cluster log file. + + Run the `kbcli cluster logs` command to view the details of the target log file generated by the target instance on the target cluster. You can use different options to view the log file details you need. + You can also run `kbcli cluster logs -h` to see the examples and option descriptions. + + ```bash + kbcli cluster logs -h + ``` + +
+ + Output + + ```bash + Access cluster log file + + Examples: + # Return snapshot logs from cluster mycluster with default primary instance (stdout) + kbcli cluster logs mycluster + + # Display only the most recent 20 lines from cluster mycluster with default primary instance (stdout) + kbcli cluster logs --tail=20 mycluster + + # Return snapshot logs from cluster mycluster with specify instance my-instance-0 (stdout) + kbcli cluster logs mycluster --instance my-instance-0 + + # Return snapshot logs from cluster mycluster with specify instance my-instance-0 and specify container + # my-container (stdout) + kbcli cluster logs mycluster --instance my-instance-0 -c my-container + + # Return slow logs from cluster mycluster with default primary instance + kbcli cluster logs mycluster --file-type=slow + + # Begin streaming the slow logs from cluster mycluster with default primary instance + kbcli cluster logs -f mycluster --file-type=slow + + # Return the specify file logs from cluster mycluster with specify instance my-instance-0 + kbcli cluster logs mycluster --instance my-instance-0 --file-path=/var/log/yum.log + + # Return the specify file logs from cluster mycluster with specify instance my-instance-0 and specify + # container my-container + kbcli cluster logs mycluster --instance my-instance-0 -c my-container --file-path=/var/log/yum.log + ``` +
+ +4. (Optional) Troubleshooting. + + The log enhancement function does not affect the core process of KubeBlocks. If a configuration exception occurs, a warning shows to help troubleshoot. + `warning` is recorded in the `event` and `status.Conditions` of the target database cluster. + + View `warning` information. + + - Run `kbcli cluster describe ` to view the status of the target cluster. You can also run `kbcli cluster list events ` to view the event information of the target cluster directly. + + ```bash + kbcli cluster describe mycluster + ``` + + ```bash + kbcli cluster list-events mycluster + ``` + + - Run `kubectl describe cluster ` to view the warning. + + ```bash + kubectl describe cluster mycluster + ``` + + ***Example*** + + ```bash + Status: + Cluster Def Generation: 3 + Components: + Replicasets: + Phase: Running + Conditions: + Last Transition Time: 2022-11-11T03:57:42Z + Message: EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets + Reason: EnableLogsListValidateFail + Status: False + Type: ValidateEnabledLogs + Observed Generation: 2 + Operations: + Horizontal Scalable: + Name: replicasets + Restartable: + replicasets + Vertical Scalable: + replicasets + Phase: Running + Events: + Type Reason Age From Message + ---- ------ ---- ---- ------- + Normal Creating 49s cluster-controller Start Creating in Cluster: release-name-error + Warning EnableLogsListValidateFail 49s cluster-controller EnableLogs of cluster component replicasets has invalid value [errora slowa] which isn't defined in cluster definition component replicasets + Normal Running 36s cluster-controller Cluster: release-name-error is ready, current phase is Running + ``` diff --git a/docs/user_docs/kubeblocks-for-mysql/observability/alert.md b/docs/user_docs/observability/alert.md similarity index 68% rename from docs/user_docs/kubeblocks-for-mysql/observability/alert.md rename to docs/user_docs/observability/alert.md index ce8e37ca0..9d74229a2 100644 --- a/docs/user_docs/kubeblocks-for-mysql/observability/alert.md +++ b/docs/user_docs/observability/alert.md @@ -1,19 +1,27 @@ --- title: Configure IM alert description: How to enable IM alert +keywords: [mysql, alert, alert message] sidebar_position: 2 --- # Configure IM alert -Alerts are mainly used for daily error response to improve system availability. Kubeblocks has a built-in set of common alert rules and integrates multiple notification channels. The alert capability of Kubeblocks can meet the operation and maintenance requirements of production-level online clusters. +Alerts are mainly used for daily error response to improve system availability. KubeBlocks has a built-in set of common alert rules and integrates multiple notification channels. The alert capability of KubeBlocks can meet the operation and maintenance requirements of production-level online clusters. + +:::note + +The alert function is the same for all. + +::: ## Alert rules -KubeBlocks has a set of general built-in alter rules to meet the alert needs of different types of data products and provides an out-of-the-box experience without further configurations. These alert rules provide the best practice for cluster operation and maintenance. These alarm rules further improve alert accuracy and reduce the probability of false negatives and false positives through experience-based smoothing windows, alarm thresholds, alarm levels, and alarm indicators. +The built-in generic alert rules of KubeBlocks meet the needs of various data products and provide an out-of-the-box experience without further configurations. These alert rules provide the best practice for cluster operation and maintenance, which further improve alert accuracy and reduce the probability of false negatives and false positives by experience-based smoothing windows, alert thresholds, alert levels, and alert indicators. + +Taking PostgreSQL as an example, the alert rules have built-in common abnormal events, such as instance down, instance restart, slow query, connection amount, deadlock, and cache hit rate. -Taking PostgreSQL as an example, the alert rules have built-in common abnormal events, such as instance down, instance restart, slow query, connection amount, deadlock, and cache hit rate. -The following example shows PostgreSQL alert rules (refer to [Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/) for syntax). When the amount of active connections exceeds 80% of the threshold and lasts for 2 minutes, Prometheus triggers a warning and sends it to AlertManager. +The following example shows PostgreSQL alert rules (refer to [Prometheus](https://prometheus.io/docs/prometheus/latest/querying/basics/) for syntax). When the amount of active connections exceeds 80% of the threshold and lasts for 2 minutes, Prometheus triggers a warning alert and sends it to AlertManager. ```bash alert: PostgreSQLTooManyConnections @@ -41,21 +49,19 @@ kbcli dashboard open kubeblocks-prometheus-server # Here is an example and fill ## Configure IM alert -The alert message notification of Kubeblocks mainly adopts the AlertManager native capability. After receiving the Prometheus alarm, KubeBlocks performs multiple steps, including deduplication, grouping, silence, suppression, and routing, and finally sends it to the corresponding notification channel. -AlertManager integrates a set of notification channels, such as Email and Slack. Kubeblocks extends new IM class notification channels with AlertManger Webhook. +The alert message notification of KubeBlocks mainly adopts the AlertManager native capability. After receiving the Prometheus alert, KubeBlocks performs steps including deduplication, grouping, silence, suppression, and routing, and finally sends it to the corresponding notification channel. + +AlertManager integrates a set of notification channels, such as Email and Slack. KubeBlocks extends new IM class notification channels with AlertManger Webhook. This tutorial takes configuring Feishu as the notification channel as an example. ### Before you start -To receive alerts, you need to deploy monitoring components and enable cluster monitoring first. Refer to [Monitor database](monitor-database.md) for details. - -### Step 1. Configure alert channels +To receive alerts, you need to deploy monitoring add-ons and enable cluster monitoring first. Refer to [Monitor database](monitor-database.md) for details. -Configure the notification channels in advance based on your needs and obtain the necessary information for the following steps. -Taking Feishu as an example, you can obtain the webhook address after creating a custom robot. If the signature verification in the security configuration is enabled, you can obtain the signature key in advance. +### Configure alert channels -Currently, Feishu custom bot, DingTalk custom bot, WeChat Enterprise custom bot, and Slack are supported. You can refer to the following guides to configure notification channels. +Refer to the following guides to configure your alert channels. * [Feishu custom bot](https://open.feishu.cn/document/ukTMukTMukTM/ucTM5YjL3ETO24yNxkjN) * [DingTalk custom bot](https://open.dingtalk.com/document/orgapp/custom-robot-access) @@ -64,14 +70,14 @@ Currently, Feishu custom bot, DingTalk custom bot, WeChat Enterprise custom bot, :::note -* Each notification channel has its interface call amount and frequency limits and when the limits are reached, the channel will limit traffic and you cannot receive alerts. +* Each notification channel has its interface call amount and frequency limits and when the limits are reached, the channel will limit traffic and you cannot receive alerts. * The SLA of the service provided by a single channel cannot guarantee the alerts are sent successfully. Therefore, it is recommended to configure multiple notification channels to ensure availability. ::: -### Step 2. Configure the receiver +### Configure the receiver -To improve ease of use, `kbcli` develops the `alert` subcommand to simplify the receiver configuration. You can set the notification channels and receivers by the `alert` subcommand. This subcommand also supports condition filters, such as cluster names and severity levels. After the configuration succeeds, it takes effect dynamically without restarting the service. +To improve ease of use, `kbcli` develops the `alert` subcommand to simplify the receiver configuration. You can set the notification channels and receivers by the `alert` subcommand. This subcommand also supports condition filters, such as cluster names and severity levels. After the configuration succeeds, it takes effect dynamically without the service restarting. Add an alert receiver. @@ -93,13 +99,13 @@ Add an alert receiver. kbcli alert add-receiver \ --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=sign' - # Only receive the alerts from a cluster named mysql + # Only receive the alerts from a cluster named mysql-cluster kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=mysql + --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=mysql-cluster - # Only receive the critical alerts from a cluster named mysql + # Only receive the critical alerts from a cluster named mysql-cluster kbcli alert add-receiver \ - --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=mysql --severity=critical + --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo' --cluster=mysql-cluster --severity=critical ``` :::note @@ -108,13 +114,13 @@ For the detailed command description, run `kbcli alert add-receiver -h`. ::: -Run the command below to view the notification configurations. +View the notification configurations. ```bash kbcli alert list-receivers ``` -Run the command below to delete the notification channel and receiver if you want to disable the alert function. +Delete the notification channel and receiver if you want to disable the alert function. ```bash kbcli alert delete-receiver diff --git a/docs/user_docs/observability/monitor-database.md b/docs/user_docs/observability/monitor-database.md new file mode 100644 index 000000000..b78e062ef --- /dev/null +++ b/docs/user_docs/observability/monitor-database.md @@ -0,0 +1,165 @@ +--- +title: Monitor database +description: How to monitor your database +keywords: [monitor database, monitor a cluster, monitor] +sidebar_position: 1 +--- + +# Observability of KubeBlocks + +With the built-in database observability, you can observe the database health status and track and measure your database in real-time to optimize database performance. This section shows you how database observability works with KubeBlocks and how to use the function. + +## Enable database monitor + +***Steps:*** + +1. Enable the monitor function for KubeBlocks. + + When installing KubeBlocks, the monitoring add-ons are installed by default. + + ```bash + kbcli kubeblocks install + ``` + + If it is not `true`, you can set `--monitor` to `true`. + + ```bash + kbcli kubeblocks install --monitor=true + ``` + + If you have installed KubeBlocks without the monitoring add-ons, you can use `kbcli addon` to enable the monitoring add-ons. To ensure the completeness of the monitoring function, it is recommended to enable three monitoring add-ons. + + ```bash + # View all add-ons supported + kbcli addon list + ... + grafana Helm Enabled true + alertmanager-webhook-adaptor Helm Enabled true + prometheus Helm Enabled alertmanager true + ... + # Enable prometheus add-on + kbcli addon enable prometheus + + # Enable granfana add-on + kbcli addon enable granfana + + # Enable alertmanager-webhook-adaptor add-on + kbcli addon enable alertmanager-webhook-adaptor + ``` + +:::note + +Refer to [Enable add-ons](./../installation/enable-addons.md) for details. + +::: + +2. Enable the database monitoring function. + + The monitoring function is enabled by default when a database is created. The open-source or customized Exporter is injected after the monitoring function is enabled. This Exporter can be found by Prometheus server automatically and scrape monitoring indicators at regular intervals. + + - For a new cluster, run the command below to create a database cluster. + + ```bash + # Search the cluster definition + kbcli clusterdefinition list + + # Create a cluster + kbcli cluster create --cluster-definition='xxx' + ``` + + ***Example*** + + ```bash + kbcli cluster create mysql-cluster --cluster-definition='apecloud-mysql' + ``` + + :::note + + The setting of `monitor` is `true` by default and it is not recommended to disable it. In the cluster definition, you can choose any supported database engine, such as PostgreSQL, MongoDB, Redis. + + ```bash + kbcli cluster create mycluster --cluster-definition='apecloud-mysql' --monitor=true + ``` + + ::: + + - For the existing cluster, you can update it to enable the monitor function with `update` command. + + ```bash + kbcli cluster update --monitor=true + ``` + + ***Example*** + + ```bash + kbcli cluster update mysql-cluster --monitor=true + ``` + +You can view the dashboard of the corresponding cluster via Grafana Web Console. For more detailed information, see [Grafana documentation](https://grafana.com/docs/grafana/latest/dashboards/). + +3. View the Web Console of the monitoring components. + + 1. View the Web Console list of the monitoring components after the components are installed. + + ```bash + kbcli dashboard list + > + NAME NAMESPACE PORT CREATED-TIME + kubeblocks-grafana default 3000 Jan 13,2023 10:53 UTC+0800 + kubeblocks-prometheus-alertmanager default 9093 Jan 13,2023 10:53 UTC+0800 + kubeblocks-prometheus-server default 9090 Jan 13,2023 10:53 UTC+0800 + ``` + + 2. Open the Web Console of a specific monitoring add-on listed above, you can copy it from the above list. + + ```bash + kbcli dashboard open + ``` + + ***Example*** + + ```bash + kbcli dashboard open kubeblocks-grafana + ``` + + ***Result*** + + A monitoring page on Grafana website is loaded automatically after the command is executed. + + 3. Click the Dashboard icon on the left bar and two monitoring panels show on the page. + ![Dashboards](./../../img/quick_start_dashboards.png) + + 4. Click **General** -> **MySQL** to monitor the status of the ApeCloud MySQL cluster created by Playground. + ![MySQL_panel](./../../img/quick_start_mysql_panel.png) + +:::note + +The Prometheus add-on uses the local ephemeral storage by default, which might cause data loss when its Pod migrates to other pods. To avoid data loss, it is recommended to follow the steps below to enable PersistentVolume to meet the requirement of data persistence. + +1. Disable the Prometheus add-on. + + ```bash + kbcli addon disable prometheus + ``` + + :::caution + + Disabling the Prometheus add-on might cause the loss of local ephemeral storage. + + ::: + +2. Enable the PersistentVolume. + + PersistentVolumeClaim and PersistentVolume are created after executing this command and these resources require manual cleanup. + + ```bash + kbcli addon enable prometheus --storage 10Gi + ``` + +3. (**Optional**) If you want to stop using the PersistentVolume, execute the command below. + + ```bash + kbcli addon enable prometheus --storage 0Gi + ``` + +::: diff --git a/docs/user_docs/quick-start/try-kubeblocks-functions-on-cloud.md b/docs/user_docs/quick-start/try-kubeblocks-functions-on-cloud.md deleted file mode 100644 index e7b07a637..000000000 --- a/docs/user_docs/quick-start/try-kubeblocks-functions-on-cloud.md +++ /dev/null @@ -1,345 +0,0 @@ ---- -title: Try out basic functions of KubeBlocks on Cloud -description: How to run KubeBlocks on Playground -sidebar_position: 1 -sidebar_label: Try out KubeBlocks on cloud ---- - -import Tabs from '@theme/Tabs'; -import TabItem from '@theme/TabItem'; - -# Try out basic functions of KubeBlocks on Cloud -This guide walks you through the quickest way to get started with KubeBlocks, demonstrating how to easily create a KubeBlocks demo environment (Playground) with simply one `kbcli` command. -With Playground, you can try out KubeBlocks both on your local host (macOS) and on a cloud environment (AWS). - - - - -## Before you start to try KubeBlocks on Cloud (AWS) - -When deploying on the cloud, cloud resources are initialized with the help of the terraform script maintained by ApeCloud. Find the script at [Github repository](https://github.com/apecloud/cloud-provider). - -When deploying a Kubernetes cluster on the cloud, `kbcli` clones the above repository to the local host, calls the terraform commands to initialize the cluster, then deploys KubeBlocks on this cluster. -* Install AWS CLI. Refer to [Installing or updating the latest version of the AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) for details. -* Make sure the following tools are installed on your local host. - * Docker: v20.10.5 (runc ≥ v1.0.0-rc93) or above. For installation details, refer to [Get Docker](https://docs.docker.com/get-docker/). - * `kubectl`: It is used to interact with Kubernetes clusters. For installation details, refer to [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl). - * `kbcli`: It is the command line tool of KubeBlocks and is used for the interaction between Playground and KubeBlocks. Follow the steps below to install `kbcli`. - 1. Install `kbcli`. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - 2. Run `kbcli version` to check the `kbcli` version and make sure `kbcli` is installed successfully. - -## Configure access key - -Configure the Access Key of cloud resources. -For AWS, there are two options. - -**Option 1.** Use `aws configure`. - -Fill in AWS Access Key ID and AWS Secret Access Key and run the command below to configure access permission. - -```bash -aws configure -AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE -AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY -``` - -You can refer to [Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) for detailed information. - -**Option 2.** Use environment variables. - -```bash -export AWS_ACCESS_KEY_ID="anaccesskey" -export AWS_SECRET_ACCESS_KEY="asecretkey" -``` - -## Initialize Playground - -Initialize Playground. - -```bash -kbcli playground init --cloud-provider aws --region cn-northwest-1 -``` - -* `cloud-provider` specifies the cloud provider. -* `region` specifies the region to deploy a Kubernetes cluster. - Frequently used regions are as follows. You can find the full region list on [the official website](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/?nc1=h_ls). - * Americas - - | Region ID | Region name | - | :-- | :-- | - | us-east-1 | Northern Virginia | - | us-east-2 | Ohio | - | us-west-1 | Northern California | - | us-west-2 | Oregon | - - * Asia Pacific - - | Region ID | Region name | - | :-- | :-- | - | ap-east-1 | Hong Kong | - | ap-southeast-1 | Singapore | - | cn-north-1 | Beijing | - | cn-northwest-1 | Ningxia | - -During the initialization, `kbcli` clones [the GitHub repository](https://github.com/apecloud/cloud-provider) to the path `~/.kbcli/playground` and calls the terraform script to create cloud resources. And then `kbcli` deploys KubeBlocks automatically and installs a MySQL cluster. - -After the `kbcli playground init` command is executed, `kbcli` automatically switches the context of the local kubeconfig to the current cluster. Run the command below to view the deployed cluster. - -```bash -# View kbcli version -kbcli version - -# View the cluster list -kbcli cluster list -``` - -:::note - -The initialization lasts about 20 minutes. If the installation fails after a long time, please check your network environment. - -::: - -## Try KubeBlocks with Playground - -You can explore KubeBlocks by [Viewing an ApeCloud MySQL cluster](#view-an-apecloud-mysql-cluster), [Accessing an ApeCloud MySQL Cluster](#access-an-apecloud-mysql-cluster), [Observability](#observability), and [High availability](#high-availability-of-apecloud-mysql). Refer to [Overview](./../introduction/introduction.md) to explore more KubeBlocks features and you can try the full features of KubeBlocks in a standard Kubernetes cluster. - -KubeBlocks supports the complete life cycle management of a database cluster. Go through the following instruction to try basic features of KubeBlocks. -For the full feature set, refer to [KubeBlocks Documentation](./../introduction/introduction.md) for details. - -### View an ApeCloud MySQL cluster - -***Steps:*** - -1. Run the command below to view the database cluster list. - ```bash - kbcli cluster list - ``` - -2. Run `kbcli cluster describe` to view the details of a specified database cluster, such as `STATUS`, `Endpoints`, `Topology`, `Images`, and `Events`. - ```bash - kbcli cluster describe mycluster - ``` - -### Access an ApeCloud MySQL cluster - -**Option 1.** Connect database inside Kubernetes cluster. - -If a database cluster has been created and its status is `Running`, run `kbcli cluster connect` to access a specified database cluster. For example, -```bash -kbcli cluster connect mycluster -``` - -**Option 2.** Connect database outside Kubernetes cluster. - -Get the MySQL client connection example. - -```bash -kbcli cluster connect --show-example --client=cli mycluster -``` - -**Example** - -```bash -kubectl port-forward service/mycluster-mysql 3306:3306 -> -Forwarding from 127.0.0.1:3306 -> 3306 -Forwarding from [::1]:3306 -> 3306 - - -mysql -h 127.0.0.1 -P 3306 -u root -paiImelyt -> -... -Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - -mysql> show databases; -> -+--------------------+ -| Database | -+--------------------+ -| information_schema | -| mydb | -| mysql | -| performance_schema | -| sys | -+--------------------+ -5 rows in set (0.02 sec) -``` - -### Observability - -KubeBlocks has complete observability capabilities. This section demonstrates the monitoring function of KubeBlocks. - -***Steps:*** - -1. Run the command below to view the monitoring page to observe the service running status. - ```bash - kbcli dashboard open kubeblocks-grafana - ``` - - ***Result*** - - A monitoring page on Grafana website is loaded automatically after the command is executed. - -2. Click the Dashboard icon on the left bar and two monitoring panels show on the page. - ![Dashboards](./../../img/quick_start_dashboards.png) -3. Click **General** -> **MySQL** to monitor the status of the ApeCloud MySQL cluster deployed by Playground. - ![MySQL_panel](./../../img/quick_start_mysql_panel.png) - -### High availability of ApeCloud MySQL - -ApeCloud MySQL Paxos Group delivers high availability with RPO=0 and RTO in less than 30 seconds. -This section uses a simple failure simulation to show you the failure recovery capability of ApeCloud MySQL. - -#### Delete ApeCloud MySQL Standalone - -Delete the ApeCloud MySQL Standalone before trying out high availability. -```bash -kbcli cluster delete mycluster -``` - -#### Create an ApeCloud MySQL Paxos Group - -Playground creates an ApeCloud MySQL Standalone by default. You can also use `kbcli` to create a new Paxos Group. The following is an example of creating an ApeCloud MySQL Paxos Group with default configurations. - -```bash -kbcli cluster create --cluster-definition='apecloud-mysql' --set replicas=3 -``` - -#### Simulate leader pod failure recovery - -In this example, we delete the leader pod to simulate a failure. - -***Steps:*** - -1. Run `kbcli cluster describe ` to view the ApeCloud MySQL Paxos group information. View the leader pod name in `Topology`. In this example, the leader pod's name is maple05-mysql-1. - - ```bash - kbcli cluster describe maple05 - > - Name: maple05 Created Time: Jan 27,2023 17:33 UTC+0800 - NAMESPACE CLUSTER-DEFINITION VERSION STATUS TERMINATION-POLICY - default apecloud-mysql ac-mysql-8.0.30 Running WipeOut - - Endpoints: - COMPONENT MODE INTERNAL EXTERNAL - mysql ReadWrite 10.43.29.51:3306 - - Topology: - COMPONENT INSTANCE ROLE STATUS AZ NODE CREATED-TIME - mysql maple05-mysql-1 leader Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - mysql maple05-mysql-2 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - mysql maple05-mysql-0 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - - Resources Allocation: - COMPONENT DEDICATED CPU(REQUEST/LIMIT) MEMORY(REQUEST/LIMIT) STORAGE-SIZE STORAGE-CLASS - mysql false - - Images: - COMPONENT TYPE IMAGE - mysql mysql docker.io/apecloud/wesql-server:8.0.30-5.alpha2.20230105.gd6b8719 - - Events(last 5 warnings, see more:kbcli cluster list-events -n default mycluster): - TIME TYPE REASON OBJECT MESSAGE - ``` -2. Delete the leader pod. - ```bash - kubectl delete pod maple05-mysql-1 - > - pod "maple05-mysql-1" deleted - ``` - -3. Run `kbcli cluster connect maple05` to connect to the ApeCloud MySQL Paxos Group to test its availability. You can find this group can still be accessed within seconds due to our HA strategy. - ```bash - kbcli cluster connect maple05 - > - Connect to instance maple05-mysql-2: out of maple05-mysql-2(leader), maple05-mysql-1(follower), maple05-mysql-0(follower) - Welcome to the MySQL monitor. Commands end with ; or \g. - Your MySQL connection id is 33 - Server version: 8.0.30 WeSQL Server - GPL, Release 5, Revision d6b8719 - - Copyright (c) 2000, 2022, Oracle and/or its affiliates. - - Oracle is a registered trademark of Oracle Corporation and/or its - affiliates. Other names may be trademarks of their respective - owners. - - Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - - mysql> - ``` - -#### Demonstrate availability failure by NON-STOP NYAN CAT (for fun) -The above example uses `kbcli cluster connect` to test availability, in which the changes are not obvious to see. -NON-STOP NYAN CAT is a demo application to observe how the database cluster exceptions affect actual businesses. Animations and real-time key information display provided by NON-STOP NYAN CAT can directly show the availability influences of database services. - -***Steps:*** - -1. Run the command below to install the NYAN CAT demo application. - ```bash - kbcli addon enable nyancat - ``` - - **Result:** - - ``` - addon.extensions.kubeblocks.io/nyancat patched - ``` -2. Check the NYAN CAT add-on status and when its status is `Enabled`, this application is ready. - ```bash - kbcli addon list | grep nyancat - ``` -3. Open the web page. - ```bash - kbcli dashboard open kubeblocks-nyancat - ``` -4. Delete the leader pod and view the influences on the ApeCloud MySQL clusters through the NYAN CAT page. - - ![NYAN CAT](./../../img/quick_start_nyan_cat.png) - -5. Uninstall the NYAN CAT demo application after your trial. - ```bash - kbcli addon disable nyancat - ``` - -## Destroy Playground - -Before destroying Playground, it is recommended to delete the clusters created by KubeBlocks. - -```bash -# View all clusters -kbcli cluster list -A - -# Delete a cluster -# A double-check is required and you can add --auto-approve to check it automatically -kbcli cluster delete - -# Uninstall KubeBlocks -# A double-check is required and you can add --auto-approve to check it automatically -kbcli kubeblocks uninstall --remove-pvcs --remove-pvs -``` - -Run the command below to destroy Playground. - -```bash -kbcli playground destroy --cloud-provider aws --region cn-northwest-1 -``` - -Like the parameters in `kbcli playground init`, use `--cloud-provider` and `--region` to specify the cloud provider and the region. - -:::caution - -`kbcli playground destroy` directly deletes the Kubernetes cluster on the cloud but there might be residual resources in cloud, such as volumes. Please confirm whether there are residual resources after uninstalling and delete them in time to avoid unnecessary fees. - -::: - - - - - -Coming soon! - - - diff --git a/docs/user_docs/quick-start/try-kubeblocks-in-5m-on-local-host.md b/docs/user_docs/quick-start/try-kubeblocks-in-5m-on-local-host.md deleted file mode 100644 index b4d2e32ef..000000000 --- a/docs/user_docs/quick-start/try-kubeblocks-in-5m-on-local-host.md +++ /dev/null @@ -1,283 +0,0 @@ ---- -title: Try out KubeBlocks in 5 minutes on Local Host -description: A quick tour of KubeBlocks in 5 minutes on Local host on Playground -sidebar_position: 1 -sidebar_label: Try out KubeBlocks on local host ---- - - -# Try out KubeBlocks in 5 minutes on Local Host -This guide walks you through the quickest way to get started with KubeBlocks, demonstrating how to easily create a KubeBlocks demo environment (Playground) with simply one `kbcli` command. -With Playground, you can try out KubeBlocks both on your local host (macOS). - - -## Before you start - -Meet the following requirements for smooth operation of Playground and other functions. - -* Minimum system requirements: - * CPU: 4 cores - * RAM: 4 GB - - To check CPU, use `sysctl hw.physicalcpu` command; - - To check memory, use `top -d` command. - -* Make sure the following tools are installed on your local host. - * Docker: v20.10.5 (runc ≥ v1.0.0-rc93) or above. For installation details, refer to [Get Docker](https://docs.docker.com/get-docker/). - * `kubectl`: It is used to interact with Kubernetes clusters. For installation details, refer to [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl). - * `kbcli`: It is the command line tool of KubeBlocks and is used for the interaction between Playground and KubeBlocks. Follow the steps below to install `kbcli`. - 1. Install `kbcli`. - ```bash - curl -fsSL https://www.kubeblocks.io/installer/install_cli.sh | bash - ``` - 2. Run `kbcli version` to check the `kbcli` version and make sure `kbcli` is installed successfully. - -## Initialize Playground - -***Steps:*** - -1. Install Playground. - - ```bash - kbcli playground init - ``` - - This command: - 1. Creates a Kubernetes cluster in the container with [K3d](https://k3d.io/v5.4.6/). - 2. Deploys KubeBlocks in this Kubernetes cluster. - 3. Creates an ApeCloud MySQL Standalone by KubeBlocks. - -2. View the created cluster and when the status is `Running`, this cluster is created successfully. - ```bash - kbcli cluster list - ``` - - **Result:** - - You just created a cluster named `mycluster` in the default namespace. - - You can find the Playground user guide under the installation success tip. View this guide again by running `kbcli playground guide`. - - - -## Try KubeBlocks with Playground - -You can explore KubeBlocks, by [Viewing an ApeCloud MySQL cluster](#view-an-apecloud-mysql-cluster), [Accessing an ApeCloud MySQL cluster](#access-an-apecloud-mysql-cluster), [Observability](#observability), and [High availability](#high-availability-of-apecloud-mysql). Refer to [Overview](./../introduction/introduction.md) to explore detailed KubeBlocks features and you can try all the features of KubeBlocks in a standard Kubernetes cluster. - -KubeBlocks supports the complete life cycle management of a database cluster. Go through the following instructions to try basic features of KubeBlocks. - -:::note - -The local host does not support volume expansion, backup, and restore functions. - -::: - -### View an ApeCloud MySQL cluster - -***Steps:*** - -1. View the database cluster list. - ```bash - kbcli cluster list - ``` - -2. View the details of a specified database cluster and get information like `STATUS`, `Endpoints`, `Topology`, `Images`, and `Events`. - ```bash - kbcli cluster describe mycluster - ``` - -### Access an ApeCloud MySQL cluster - -**Option 1.** Connect database inside Kubernetes cluster. - -If a database cluster has been created and its status is `Running`, run `kbcli cluster connect` to access a specified database cluster. For example, - -```bash -kbcli cluster connect mycluster -``` - -**Option 2.** Connect database outside Kubernetes cluster. - -Get the MySQL client connection example. - -```bash -kbcli cluster connect --show-example --client=cli mycluster -``` - -**Example** - -```bash -kubectl port-forward service/mycluster-mysql 3306:3306 -> -Forwarding from 127.0.0.1:3306 -> 3306 -Forwarding from [::1]:3306 -> 3306 - - -mysql -h 127.0.0.1 -P 3306 -u root -paiImelyt -> -... -Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - -mysql> show databases; -> -+--------------------+ -| Database | -+--------------------+ -| information_schema | -| mydb | -| mysql | -| performance_schema | -| sys | -+--------------------+ -5 rows in set (0.02 sec) -``` - -### Observability - -KubeBlocks supports complete observability capabilities. This section demonstrates the monitoring function of KubeBlocks. - -***Steps:*** - -1. View the monitoring page to observe the service running status. - ```bash - kbcli dashboard open kubeblocks-grafana - ``` - - **Result** - - A monitoring page on Grafana website is loaded automatically after the command is executed. - -2. Click the Dashboard icon on the left bar and monitoring panels show on the page. - ![Dashboards](./../../img/quick_start_dashboards.png) -3. Click **General** -> **MySQL** to monitor the status of the ApeCloud MySQL cluster deployed by Playground. - ![MySQL_panel](./../../img/quick_start_mysql_panel.png) - -### High availability of ApeCloud MySQL - -ApeCloud MySQL Paxos group delivers high availability with RPO=0 and RTO in less than 30 seconds. -This guide shows a simple failure simulation to show you the failure recovery capability of ApeCloud MySQL. - -#### Delete ApeCloud MySQL Standalone - -Delete the ApeCloud MySQL Standalone before trying out high availability. -```bash -kbcli cluster delete mycluster -``` - -#### Create an ApeCloud MySQL Paxos group - -Playground creates an ApeCloud MySQL standalone by default. You can also use `kbcli` to create a new Paxos group. The following is an example of creating an ApeCloud MySQL Paxos group with default configurations. - -```bash -kbcli cluster create --cluster-definition='apecloud-mysql' --set replicas=3 -``` - -#### Simulate leader pod failure recovery - -In this example, delete the leader pod to simulate a failure. - -***Steps:*** - -1. Run `kbcli cluster describe` to view the ApeCloud MySQL Paxos group information. View the leader pod name in `Topology`. In this example, the leader pod's name is maple05-mysql-1. - ```bash - kbcli cluster describe maple05 - > - Name: maple05 Created Time: Jan 27,2023 17:33 UTC+0800 - NAMESPACE CLUSTER-DEFINITION VERSION STATUS TERMINATION-POLICY - default apecloud-mysql ac-mysql-8.0.30 Running WipeOut - - Endpoints: - COMPONENT MODE INTERNAL EXTERNAL - mysql ReadWrite 10.43.29.51:3306 - - Topology: - COMPONENT INSTANCE ROLE STATUS AZ NODE CREATED-TIME - mysql maple05-mysql-1 leader Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - mysql maple05-mysql-2 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - mysql maple05-mysql-0 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 - - Resources Allocation: - COMPONENT DEDICATED CPU(REQUEST/LIMIT) MEMORY(REQUEST/LIMIT) STORAGE-SIZE STORAGE-CLASS - mysql false - - Images: - COMPONENT TYPE IMAGE - mysql mysql docker.io/apecloud/wesql-server:8.0.30-5.alpha2.20230105.gd6b8719 - - Events(last 5 warnings, see more:kbcli cluster list-events -n default mycluster): - TIME TYPE REASON OBJECT MESSAGE - ``` -2. Delete the leader pod. - ```bash - kubectl delete pod maple05-mysql-1 - > - pod "maple05-mysql-1" deleted - ``` - -3. Run `kbcli cluster connect maple05` to connect to the ApeCloud MySQL Paxos group to test its availability. You can find this group can still be accessed within seconds due to our HA strategy. - ```bash - kbcli cluster connect maple05 - > - Connect to instance maple05-mysql-2: out of maple05-mysql-2(leader), maple05-mysql-1(follower), maple05-mysql-0(follower) - Welcome to the MySQL monitor. Commands end with ; or \g. - Your MySQL connection id is 33 - Server version: 8.0.30 WeSQL Server - GPL, Release 5, Revision d6b8719 - - Copyright (c) 2000, 2022, Oracle and/or its affiliates. - - Oracle is a registered trademark of Oracle Corporation and/or its - affiliates. Other names may be trademarks of their respective - owners. - - Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. - - mysql> - ``` - -#### Demonstrate availability failure by NON-STOP NYAN CAT (for fun) -The above example uses `kbcli cluster connect` to test availability, in which the changes are not obvious to see. -NON-STOP NYAN CAT is a demo application to observe how the database cluster exceptions affect actual businesses. Animations and real-time key information display provided by NON-STOP NYAN CAT can directly show the availability influences of database services. - -***Steps:*** - -1. Run the command below to install the NYAN CAT demo application. - ```bash - kbcli addon enable nyancat - ``` - - **Result:** - - ``` - addon.extensions.kubeblocks.io/nyancat patched - ``` -2. Check the NYAN CAT add-on status and when its status is `Enabled`, this application is ready. - ```bash - kbcli addon list | grep nyancat - ``` -3. Open the web page. - ```bash - kbcli dashboard open kubeblocks-nyancat - ``` -4. Delete the leader pod and view the influences on the ApeCloud MySQL cluster through the NYAN CAT page. - - ![NYAN CAT](./../../img/quick_start_nyan_cat.png) - -5. Uninstall the NYAN CAT demo application after your trial. - ```bash - kbcli addon disable nyancat - ``` - -## Destroy Playground - -Destroying Playground cleans up relevant component services and data: - -* Delete all KubeBlocks database clusters. -* Uninstall KubeBlocks. -* Delete the local Kubernetes clusters created by K3d. - -Destroy Playground. -```bash -kbcli playground destroy -``` - diff --git a/docs/user_docs/quick-start/try-kubeblocks-on-cloud.md b/docs/user_docs/quick-start/try-kubeblocks-on-cloud.md new file mode 100644 index 000000000..d69e8ecf2 --- /dev/null +++ b/docs/user_docs/quick-start/try-kubeblocks-on-cloud.md @@ -0,0 +1,481 @@ +--- +title: Try out KubeBlocks on Cloud +description: How to run Playground on Cloud +keywords: [Playground, try out, cloud] +sidebar_position: 1 +sidebar_label: Try out KubeBlocks on cloud +--- + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + +# Try out KubeBlocks on Cloud + +This guide walks you through the quickest way to get started with KubeBlocks on cloud, demonstrating how to create a demo environment (Playground) with one command. + +## Preparation + +When deploying KubeBlocks on the cloud, cloud resources are initialized with the help of [the terraform script](https://github.com/apecloud/cloud-provider). `kbcli` downloads the script and stores it locally, then calls the terraform commands to initialize a fully-managed Kubernetes cluster and deploy KubeBlocks on this cluster. + + + + +### Before you start to try KubeBlocks on AWS + +Make sure you have all the followings prepared. + +* [Install AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html) +* [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +* [Install `kbcli`](./../installation/install-kbcli.md) + +### Configure access key + +**Option 1.** Use `aws configure`. + +Fill in an access key and run the command below to authenticate the requests. + +```bash +aws configure +AWS Access Key ID [None]: AKIAIOSFODNN7EXAMPLE +AWS Secret Access Key [None]: wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY +``` + +You can refer to [Quick configuration with aws configure](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-quickstart.html#cli-configure-quickstart-config) for detailed information. + +**Option 2.** Use environment variables. + +```bash +export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE" +export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" +``` + +### Initialize Playground + +```bash +kbcli playground init --cloud-provider aws --region us-west-2 +``` + +* `cloud-provider` specifies the cloud provider. +* `region` specifies the region to deploy a Kubernetes cluster. + You can find the region list on [the official website](https://aws.amazon.com/about-aws/global-infrastructure/regional-product-services/?nc1=h_ls). + +During the initialization, `kbcli` clones [the GitHub repository](https://github.com/apecloud/cloud-provider) to the directory `~/.kbcli/playground`, installs KubeBlocks, and creates a MySQL cluster. After executing the `kbcli playground init` command, kbcli automatically switches the current context of kubeconfig to the new Kubernetes cluster. +Run the command below to view the created cluster. + +```bash +# View kbcli version +kbcli version + +# View the cluster list +kbcli cluster list +``` + +:::note + +The initialization lasts about 20 minutes. If the installation fails after a long time, please check your network. + +::: + + + + +### Before you start to try KubeBlocks on GCP + +Make sure you have all the followings prepared. + +* Google Cloud account +* [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +* [Install `kbcli`](./../installation/install-kbcli.md) + +### Configure GCP environment + +***Steps:*** + +1. Install Google Cloud SDK. + + ```bash + # macOS brew install + brew install --cask google-cloud-sdk + + # windows + choco install gcloudsdk + ``` + +2. Initialize GCP. + + ```bash + gcloud init + ``` + +3. Log in to GCP. + + ```bash + gcloud auth application-default login + ``` + +4. Configure GOOGLE_PROJECT environment variables,```kbcli playground``` creates GKE cluster in the project. + + ```bash + export GOOGLE_PROJECT= + ``` + +### Initialize Playground + +The following command deploys a GKE service in the region `us-central1` on GCP, and installs KubeBlocks. + +```bash +kbcli playground init --cloud-provider gcp --region us-central1 +``` + +* `cloud-provider` specifies the cloud provider. +* `region` specifies the region to deploy a Kubernetes cluster. + +During the initialization, `kbcli` clones [the GitHub repository](https://github.com/apecloud/cloud-provider) to the directory `~/.kbcli/playground`, installs KubeBlocks, and creates a MySQL cluster. After executing the `kbcli playground init` command, kbcli automatically switches the current context of kubeconfig to the new Kubernetes cluster. +Run the command below to view the created cluster. + +```bash +# View kbcli version +kbcli version + +# View the cluster list +kbcli cluster list +``` + +:::note + +The initialization takes about 20 minutes. If the installation fails after a long time, please check your network. + +::: + + + + +### Before you start to try KubeBlocks on Tencent Cloud + +Make sure you have all the followings prepared. + +* Tencent Cloud account +* [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +* [Install `kbcli`](./../installation/install-kbcli.md) + +### Configure TKE environment + +***Steps:*** + +1. Log in to Tencent Cloud. +2. Go to [Tencent Kubernetes Engine (TKE)](https://console.cloud.tencent.com/tke2) to grant resource operation permission to your account before using the container service. +3. Go to [API Console](https://console.cloud.tencent.com/cam/overview) -> **Access Key** -> **API Keys** and click **Create Key** to create a pair of Secret ID and Secret Key. +4. Add the Secret ID and Secret Key to the environment variables. + + ```bash + export TENCENTCLOUD_SECRET_ID=YOUR_SECRET_ID + export TENCENTCLOUD_SECRET_KEY=YOUR_SECRET_KEY + ``` + +### Initialize Playground + +The following command deploys a Kubernetes service in the region `ap-chengdu` on Tencent Cloud and installs KubeBlocks. + +```bash +kbcli playground init --cloud-provider tencentcloud --region ap-chengdu +``` + +* `cloud-provider` specifies the cloud provider. +* `region` specifies the region to deploy a Kubernetes cluster. + +During the initialization, `kbcli` clones [the GitHub repository](https://github.com/apecloud/cloud-provider) to the directory `~/.kbcli/playground`, installs KubeBlocks, and creates a MySQL cluster. After executing the `kbcli playground init` command, kbcli automatically switches the current context of kubeconfig to the new Kubernetes cluster. +Run the command below to view the created cluster. + +```bash +# View kbcli version +kbcli version + +# View the cluster list +kbcli cluster list +``` + +:::note + +The initialization takes about 20 minutes. If the installation fails after a long time, please check your network. + +::: + + + + +### Before you start to try KubeBlocks on Alibaba Cloud + +Make sure you have all the followings prepared. + +* Alibaba Cloud account. +* [Install kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl) +* [Install `kbcli`](./../installation/install-kbcli.md) + +### Configure ACK environment + +***Steps:*** + +1. Log in to Alibaba Cloud. +2. Follow the instructions in [Quick start for first-time users](https://www.alibabacloud.com/help/en/container-service-for-kubernetes/latest/quick-start-for-first-time-users) to check whether you have activated Alibaba Cloud Container Service for Kubernetes (ACK) and assigned roles. +3. Click [AliyunOOSLifecycleHook4CSRole](https://ram.console.aliyun.com/role/authorize?spm=5176.2020520152.0.0.5b4716ddI6QevL&request=%7B%22ReturnUrl%22%3A%22https%3A%2F%2Fram.console.aliyun.com%22%2C%22Services%22%3A%5B%7B%22Roles%22%3A%5B%7B%22RoleName%22%3A%22AliyunOOSLifecycleHook4CSRole%22%2C%22TemplateId%22%3A%22AliyunOOSLifecycleHook4CSRole%22%7D%5D%2C%22Service%22%3A%22OOS%22%7D%5D%7D) and click **Agree to Authorization** to create an AliyunOOSLifecycleHook4CSRole role. + + This operation grant permissions to access Operation Orchestration Service (OOS) and to access the resources in other cloud products since creating and managing a node pool is required for creating an ACK cluster. + + Refer to [Scale a node pool](https://www.alibabacloud.com/help/zh/container-service-for-kubernetes/latest/scale-up-and-down-node-pools) for details. +4. Create an AccessKey ID and the corresponding AccessKey secret. + + 1. Go to [Alibaba Cloud Management Console](https://homenew.console.aliyun.com/home/dashboard/ProductAndService). Hover the pointer over your account console and click **AccessKey Management**. + 2. Click **Create AccessKey** to create the AccessKey ID and the corresponding AccessKey secret. + 3. Add the AccessKey ID and AccessKey secret to the environment variable to configure identity authorization information. + + ```bash + export ALICLOUD_ACCESS_KEY="************" + export ALICLOUD_SECRET_KEY="************" + ``` + + :::note + + Refer to [Create an AccessKey pair](https://www.alibabacloud.com/help/en/resource-access-management/latest/accesskey-pairs-create-an-accesskey-pair-for-a-ram-user) for details. + + ::: + +### Initialize Playground + +The following command deploys an ACK cluster in the region `cn-hangzhou` on Alibaba Cloud, and installs KubeBlocks. + +```bash +kbcli playground init --cloud-provider alicloud --region cn-hangzhou +``` + +* `cloud-provider` specifies the cloud provider. +* `region` specifies the region to deploy a Kubernetes cluster. + +During the initialization, `kbcli` clones [the GitHub repository](https://github.com/apecloud/cloud-provider) to the directory `~/.kbcli/playground`, installs KubeBlocks, and creates a MySQL cluster. After executing the `kbcli playground init` command, kbcli automatically switches the current context of kubeconfig to the new Kubernetes cluster. +Run the command below to view the created cluster. + +```bash +# View kbcli version +kbcli version + +# View the cluster list +kbcli cluster list +``` + +:::note + +The initialization takes about 20 minutes. If the installation fails after a long time, please check your network. + +::: + + + + +## Try KubeBlocks with Playground +Go through the following instructions to try basic features of KubeBlocks. + +### Describe a MySQL cluster + +***Steps:*** + +1. View the database cluster list. + + ```bash + kbcli cluster list + ``` + +2. View the details of a specified database cluster, such as `STATUS`, `Endpoints`, `Topology`, `Images`, and `Events`. + + ```bash + kbcli cluster describe mycluster + ``` + +### Access a MySQL cluster + +**Option 1.** Connect database from container network. + +Wait until the status of this cluster is `Running`, then run `kbcli cluster connect` to access a specified database cluster. For example, + +```bash +kbcli cluster connect mycluster +``` + +**Option 2.** Connect database remotely. + +***Steps:*** + +1. Get Credentials. + ```bash + kbcli cluster connect --show-example --client=cli mycluster + ``` +2. Run `port-forward`. + + ```bash + kubectl port-forward service/mycluster-mysql 3306:3306 + > + Forwarding from 127.0.0.1:3306 -> 3306 + Forwarding from [::1]:3306 -> 3306 + ``` + +3. Open another terminal tab to connect the database cluster. + + ```bash + mysql -h 127.0.0.1 -P 3306 -u root -paiImelyt + > + ... + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + mysql> show databases; + > + +--------------------+ + | Database | + +--------------------+ + | information_schema | + | mydb | + | mysql | + | performance_schema | + | sys | + +--------------------+ + 5 rows in set (0.02 sec) + ``` + +### Observe a MySQL cluster + +KubeBlocks has complete observability capabilities. This section demonstrates the monitoring function of KubeBlocks. + +***Steps:*** + +1. Open the grafana dashboard. + + ```bash + kbcli dashboard open kubeblocks-grafana + ``` + + ***Result*** + + A monitoring page on Grafana website is loaded automatically after the command is executed. + +2. Click the Dashboard icon on the left bar and two monitoring panels show on the page. + ![Dashboards](./../../img/quick_start_dashboards.png) +3. Click **General** -> **MySQL** to monitor the status of the MySQL cluster created by Playground. + ![MySQL_panel](./../../img/quick_start_mysql_panel.png) + +### High availability of MySQL + +This guide shows a simple failure simulation to show you the failure recovery capability of MySQL. + +#### Delete the Standalone MySQL cluster + +Delete the Standalone MySQL cluster before trying out high availability. + +```bash +kbcli cluster delete mycluster +``` + +#### Create a Raft MySQL cluster + +You can use `kbcli` to create a Raft MySQL cluster. The following is an example of creating a Raft MySQL cluster with default configurations. + +```bash +kbcli cluster create --cluster-definition='apecloud-mysql' --set replicas=3 +``` + +#### Simulate leader pod failure recovery + +In this example, delete the leader pod to simulate a failure. + +***Steps:*** + +1. Make sure the newly created cluster is `Running`. + + ```bash + kbcli cluster list + ``` + +2. Find the leader pod name in `Topology`. In this example, the leader pod's name is maple05-mysql-1. + + ```bash + kbcli cluster describe maple05 + > + Name: maple05 Created Time: Jan 27,2023 17:33 UTC+0800 + NAMESPACE CLUSTER-DEFINITION VERSION STATUS TERMINATION-POLICY + default apecloud-mysql ac-mysql-8.0.30 Running WipeOut + + Endpoints: + COMPONENT MODE INTERNAL EXTERNAL + mysql ReadWrite 10.43.29.51:3306 + + Topology: + COMPONENT INSTANCE ROLE STATUS AZ NODE CREATED-TIME + mysql maple05-mysql-1 leader Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + mysql maple05-mysql-2 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + mysql maple05-mysql-0 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + + Resources Allocation: + COMPONENT DEDICATED CPU(REQUEST/LIMIT) MEMORY(REQUEST/LIMIT) STORAGE-SIZE STORAGE-CLASS + mysql false + + Images: + COMPONENT TYPE IMAGE + mysql mysql docker.io/apecloud/wesql-server:8.0.30-5.alpha2.20230105.gd6b8719 + + Events(last 5 warnings, see more:kbcli cluster list-events -n default mycluster): + TIME TYPE REASON OBJECT MESSAGE + ``` + +3. Delete the leader pod. + + ```bash + kubectl delete pod maple05-mysql-1 + > + pod "maple05-mysql-1" deleted + ``` + +4. Connect to the Raft MySQL cluster. It can be accessed within seconds. + + ```bash + kbcli cluster connect maple05 + > + Connect to instance maple05-mysql-2: out of maple05-mysql-2(leader), maple05-mysql-1(follower), maple05-mysql-0(follower) + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 33 + Server version: 8.0.30 WeSQL Server - GPL, Release 5, Revision d6b8719 + + Copyright (c) 2000, 2022, Oracle and/or its affiliates. + + Oracle is a registered trademark of Oracle Corporation and/or its + affiliates. Other names may be trademarks of their respective + owners. + + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + mysql> + ``` + +## Destroy Playground + +1. Before destroying Playground, it is recommended to delete the database clusters created by KubeBlocks. + + ```bash + # View all clusters + kbcli cluster list -A + + # Delete a cluster + # A double-check is required or you can add --auto-approve to skip it + kbcli cluster delete + + # Uninstall KubeBlocks + # A double-check is required or you can add --auto-approve to skip it + kbcli kubeblocks uninstall --remove-pvcs --remove-pvs + ``` + +2. Destroy Playground. + + ```bash + kbcli playground destroy + ``` + +:::caution + +`kbcli playground destroy` deletes the Kubernetes cluster on the cloud, but there might be residual resources on the cloud, such as volumes and snapshots. Please delete them in time to avoid unexpected costs. + +::: diff --git a/docs/user_docs/quick-start/try-kubeblocks-on-your-laptop.md b/docs/user_docs/quick-start/try-kubeblocks-on-your-laptop.md new file mode 100644 index 000000000..0d09f5b48 --- /dev/null +++ b/docs/user_docs/quick-start/try-kubeblocks-on-your-laptop.md @@ -0,0 +1,300 @@ +--- +title: Try out KubeBlocks in 5 minutes on laptop +description: A quick tour of KubeBlocks Playground +keywords: [Playground, try out, laptop,] +sidebar_position: 1 +sidebar_label: Try out KubeBlocks on laptop +--- + +# Try out KubeBlocks in 5 minutes on laptop + +This guide walks you through the quickest way to get started with KubeBlocks, demonstrating how to create a demo environment (Playground) with one command. + +## Before you start + +Meet the following requirements for a smooth user experience: + +* Minimum system requirements: + * CPU: 4C, use `sysctl hw.physicalcpu` command to check CPU; + * RAM: 4G, use `top -d` command to check memory. + +* Make sure the following tools are installed on your laptop: + * [Docker](https://docs.docker.com/get-docker/): v20.10.5 (runc ≥ v1.0.0-rc93) or above; + * [kubectl](https://kubernetes.io/docs/tasks/tools/#kubectl): it is used to interact with Kubernetes clusters; + * [kbcli](./../installation/install-kbcli.md): it is used for the interaction between Playground and KubeBlocks. + +## Initialize Playground + +***Steps:*** + +1. Install Playground. + + ```bash + kbcli playground init + ``` + + This command: + 1. Creates a Kubernetes cluster in the container with [K3d](https://k3d.io/v5.4.6/). + 2. Deploys KubeBlocks in the K3d cluster. + 3. Creates a standalone MySQL cluster. + +2. Check the MySQL cluster repeatedly until the status becomes `Running`. + + ```bash + kbcli cluster list + ``` + + **Result:** + + You just created a cluster named `mycluster` in the default namespace. You can find the user guide under the installation success tip. View this guide again by running `kbcli playground init -h`. + +## Try KubeBlocks with Playground + +You can explore KubeBlocks, by referring to [Describe a MySQL cluster](#describe-a-mysql-cluster), [Access a MySQL cluster](#access-a-mysql-cluster), [Observe a MySQL cluster](#observe-a-mysql-cluster), and [High availability](#high-availability-of-mysql). Go through the following instructions to try basic features of KubeBlocks. + +:::note + +Playground does not support volume expansion, backup, and restore functions. + +::: + +### Describe a MySQL cluster + +***Steps:*** + +1. View the database cluster list. + + ```bash + kbcli cluster list + ``` + +2. View the details of a specified database cluster, such as `STATUS`, `Endpoints`, `Topology`, `Images`, and `Events`. + + ```bash + kbcli cluster describe mycluster + ``` + +### Access a MySQL cluster + +**Option 1.** Connect database from container network. + +Wait until the status of this cluster is `Running`, then run `kbcli cluster connect` to access a specified database cluster. For example, + +```bash +kbcli cluster connect mycluster +``` + +**Option 2.** Connect database from host network. + +***Steps:*** + +1. Get Credentials. + ```bash + kbcli cluster connect --show-example --client=cli mycluster + ``` +3. Run `port-forward`. + + ```bash + kubectl port-forward service/mycluster-mysql 3306:3306 + > + Forwarding from 127.0.0.1:3306 -> 3306 + Forwarding from [::1]:3306 -> 3306 + +3. Open another terminal tab to connect the database cluster. + + ```bash + mysql -h 127.0.0.1 -P 3306 -u root -paiImelyt + > + ... + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + mysql> show databases; + > + +--------------------+ + | Database | + +--------------------+ + | information_schema | + | mydb | + | mysql | + | performance_schema | + | sys | + +--------------------+ + 5 rows in set (0.02 sec) + ``` + +### Observe a MySQL cluster + +KubeBlocks supports complete observability capabilities. This section demonstrates the monitoring function of KubeBlocks. + +***Steps:*** + +1. Open the grafana dashboard. + + ```bash + kbcli dashboard open kubeblocks-grafana + ``` + + **Result:** + + A monitoring page on Grafana website is loaded automatically after the command is executed. + +2. Click the Dashboard icon on the left bar and monitoring panels show on the page. + ![Dashboards](./../../img/quick_start_dashboards.png) +3. Click **General** -> **MySQL** to monitor the status of the MySQL cluster. + ![MySQL_panel](./../../img/quick_start_mysql_panel.png) + +### High availability of MySQL + +This guide shows a simple failure simulation to show you the failure recovery capability of MySQL. + +#### Delete the Standalone MySQL cluster + +Delete the Standalone MySQL cluster before trying out high availability. + +```bash +kbcli cluster delete mycluster +``` + +#### Create a Raft MySQL cluster + +You can use `kbcli` to create a Raft MySQL cluster. The following is an example of creating a Raft MySQL cluster with default configurations. + +```bash +kbcli cluster create --cluster-definition='apecloud-mysql' --set replicas=3 +``` + +#### Simulate leader pod failure recovery + +In this example, delete the leader pod to simulate a failure. + +***Steps:*** + +1. Make sure the newly created cluster is `Running`. + + ```bash + kbcli cluster list + ``` + +2. Find the leader pod name in `Topology`. In this example, the leader pod's name is maple05-mysql-1. + + ```bash + kbcli cluster describe maple05 + > + Name: maple05 Created Time: Jan 27,2023 17:33 UTC+0800 + NAMESPACE CLUSTER-DEFINITION VERSION STATUS TERMINATION-POLICY + default apecloud-mysql ac-mysql-8.0.30 Running WipeOut + + Endpoints: + COMPONENT MODE INTERNAL EXTERNAL + mysql ReadWrite 10.43.29.51:3306 + + Topology: + COMPONENT INSTANCE ROLE STATUS AZ NODE CREATED-TIME + mysql maple05-mysql-1 leader Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + mysql maple05-mysql-2 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + mysql maple05-mysql-0 follower Running k3d-kubeblocks-playground-server-0/172.20.0.3 Jan 30,2023 17:33 UTC+0800 + + Resources Allocation: + COMPONENT DEDICATED CPU(REQUEST/LIMIT) MEMORY(REQUEST/LIMIT) STORAGE-SIZE STORAGE-CLASS + mysql false + + Images: + COMPONENT TYPE IMAGE + mysql mysql docker.io/apecloud/wesql-server:8.0.30-5.alpha2.20230105.gd6b8719 + + Events(last 5 warnings, see more:kbcli cluster list-events -n default mycluster): + TIME TYPE REASON OBJECT MESSAGE + ``` + +3. Delete the leader pod. + + ```bash + kubectl delete pod maple05-mysql-1 + > + pod "maple05-mysql-1" deleted + ``` + +4. Connect to the Raft MySQL cluster. It can be accessed within seconds. + + ```bash + kbcli cluster connect maple05 + > + Connect to instance maple05-mysql-2: out of maple05-mysql-2(leader), maple05-mysql-1(follower), maple05-mysql-0(follower) + Welcome to the MySQL monitor. Commands end with ; or \g. + Your MySQL connection id is 33 + Server version: 8.0.30 WeSQL Server - GPL, Release 5, Revision d6b8719 + + Copyright (c) 2000, 2022, Oracle and/or its affiliates. + + Oracle is a registered trademark of Oracle Corporation and/or its + affiliates. Other names may be trademarks of their respective + owners. + + Type 'help;' or '\h' for help. Type '\c' to clear the current input statement. + + mysql> + ``` + +#### Demonstrate availability failure by NON-STOP NYAN CAT (for fun) + +The above example uses `kbcli cluster connect` to test availability, in which the changes are not obvious to see. + +NON-STOP NYAN CAT is a demo application to observe how the database cluster exceptions affect actual businesses. Animations and real-time key information display provided by NON-STOP NYAN CAT can directly show the availability influences of database services. + +***Steps:*** + +1. Install the NYAN CAT demo application. + + ```bash + kbcli addon enable nyancat + ``` + +
+ + Expected output + + ```bash + addon.extensions.kubeblocks.io/nyancat enabled + ``` + +
+ +2. Check the NYAN CAT add-on status and when its status is `Enabled`, this application is ready. + + ```bash + kbcli addon list | grep nyancat + ``` + +3. Open the web page. + + ```bash + kbcli dashboard open kubeblocks-nyancat + ``` + +4. Open another terminal tab and delete the leader pod. Then view the influences on the Raft MySQL cluster through the NYAN CAT page. + + ```bash + kubectl delete pod maple05-mysql-1 + ``` + + ![NYAN CAT](./../../img/quick_start_nyan_cat.png) + +5. Uninstall the NYAN CAT demo application after your trial. + + ```bash + kbcli addon disable nyancat + ``` + +## Destroy Playground + +Destroying Playground cleans up resources and data: + +* Delete all KubeBlocks database clusters. +* Uninstall KubeBlocks. +* Delete the Kubernetes cluster created by K3d. + +Destroy Playground. + +```bash +kbcli playground destroy +``` diff --git a/docs/user_docs/resource-scheduling/resource-scheduling.md b/docs/user_docs/resource-scheduling/resource-scheduling.md index ff3c65d5d..339d55c7e 100644 --- a/docs/user_docs/resource-scheduling/resource-scheduling.md +++ b/docs/user_docs/resource-scheduling/resource-scheduling.md @@ -1,12 +1,14 @@ --- title: Configure pod affinity for database clusters -description: How to configure pods affinity for database clusters +description: How to configure pod affinity for database clusters +keywords: [pod affinity] sidebar_position: 1 --- # Configure pod affinity for database clusters -Affinity controls the selection logic of pod allocation on nodes. By a reasonable allocation of Kubernetes pods on different nodes, the business availability, resource usage rate, and stability are improved. +Affinity controls the selection logic of pod allocation on nodes. By a reasonable allocation of Kubernetes pods on different nodes, the business availability, resource usage rate, and stability are improved. + Affinity and toleration can be set by `kbcli` or the CR YAML file of the cluster. `kbcli` only supports the cluster-level configuration and the CR YAML file supports both the cluster-level and component-level configurations. ## Option 1. Use kbcli @@ -58,9 +60,9 @@ Options: ## Option 2. Use a YAML file -You can configure pod affinity and toleration in either the spec of a cluster or the spec of a component. +You can configure pod affinity and toleration in either the spec of a cluster or the spec of a component. -The cluster-level configuration is used as the default configuration of all components; if the pod affinity configuration exists in a component, the component-level configuration will take effect and cover the default cluster-level configuration. +The cluster-level configuration is used as the default configuration of all components; if the pod affinity configuration exists in a component, the component-level configuration will take effect and cover the default cluster-level configuration. ```yaml spec: @@ -69,7 +71,7 @@ spec: topologyKeys: - kubernetes.io/hostname nodeLabels: - - topology.kubernetes.io/zone: us-east-1a + topology.kubernetes.io/zone: us-east-1a tenancy: sharedNode tolerations: - key: EngineType @@ -131,7 +133,7 @@ kbcli cluster create --topology-keys kubernetes.io/hostname --pod-anti-affinity ### Deploy pods in specified nodes -You can specify a node label to deploy a cluster on the specified node. +You can specify a node label to deploy a cluster on the specified node. The example below creates and sets a cluster to be deployed on the node with an available zone label of `topology.kubernetes.io/zone=us-east-1a`. ```bash @@ -160,4 +162,4 @@ kbcli cluster create --tenancy=DedicatedNode This command will be performed successfully based on the prerequisite that you have added taints for these nodes. Otherwise, the business that is not managed by KubeBlocks can still be deployed on these nodes. -::: \ No newline at end of file +::: diff --git a/docs/user_docs/user-management/_category_.yml b/docs/user_docs/user-management/_category_.yml new file mode 100644 index 000000000..ffba66443 --- /dev/null +++ b/docs/user_docs/user-management/_category_.yml @@ -0,0 +1,4 @@ +position: 11 +label: User Management +collapsible: true +collapsed: true \ No newline at end of file diff --git a/docs/user_docs/user-management/manage_user_accounts.md b/docs/user_docs/user-management/manage_user_accounts.md new file mode 100644 index 000000000..8c49bf6b8 --- /dev/null +++ b/docs/user_docs/user-management/manage_user_accounts.md @@ -0,0 +1,74 @@ +--- +title: Manage user accounts +description: How to manage user accounts +keywords: [user account] +sidebar_position: 1 +sidebar_label: Manage user accounts +--- + +# Manage user accounts + +KubeBlocks offers a variety of services to enhance the usability, availability, and observability of database clusters. Different components require user accounts with different permissions to create connections. + +***Steps*** + +- Create a user account + + ```bash + kbcli cluster create-account --name --password + ``` + +- Grant a role to a user + + ```bash + kbcli cluster grant-role --name --role + ``` + + KubeBlocks provides three role levels of permission. + + - Superuser: with all permissions. + - ReadWrite: read and write. + - ReadOnly: read only. + + For different database engines, the detailed permission are varied. Check the table below. + + | Role | MySQL | PostgreSQL | Redis | + | :------ | :------- | :------ | :----- | + | Superuser | GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, RELOAD, SHUTDOWN, PROCESS, FILE, REFERENCES, INDEX, ALTER, SHOW DATABASES, SUPER, CREATE TEMPORARY TABLES, LOCK TABLES, EXECUTE, REPLICATION SLAVE, REPLICATION CLIENT, CREATE VIEW, SHOW VIEW, CREATE ROUTINE, ALTER ROUTINE, CREATE USER, EVENT, TRIGGER, CREATE TABLESPACE, CREATE ROLE, DROP ROLE ON * a user | ALTER USER WITH SUPERUSER | +@ALL allkeys| + | ReadWrite | GRANT SELECT, INSERT, DELETE ON * TO a user | GRANT pg_write_all_data TO a user | -@ALL +@Write +@READ allkeys | + | ReadOnly | GRANT SELECT, SHOW VIEW ON * TO a user | GRANT pg_read_all_data TO a user | -@ALL +@READ allkeys | + +- Check role level of a user account + + ```bash + kbcli cluster describe-account --name + ``` + +- Revoke role from a user account + + ```bash + kbcli cluster revoke-role --name --role + ``` + +- List all user accounts + + ```bash + kbcli cluster list-accounts + ``` + + :::note + + For security reasons, the `list-accounts` command does not show all accounts. Accounts with high privilege such as operational accounts and superuser accounts that meet certain rules are hidden. Refer to the table below to view the hidden accounts. + + ::: + + | Database | Hidden Accounts | + | :--- | :--- | + | MySQL | root
kb*
Localhost = '' | + | postGre | Postgres
kb* | + +- Delete a user account + + ```bash + kbcli cluster delete-account --name + ``` diff --git a/examples/loadbalancer/nginx.yaml b/examples/loadbalancer/nginx.yaml deleted file mode 100644 index a0f851a8f..000000000 --- a/examples/loadbalancer/nginx.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - nodeSelector: - kubernetes.io/hostname: ip-192-168-17-47.cn-north-1.compute.internal - containers: - - name: nginx - image: nginx - imagePullPolicy: IfNotPresent - ports: - - containerPort: 80 - name: web diff --git a/externalapis/preflight/v1beta2/groupversion_info.go b/externalapis/preflight/v1beta2/groupversion_info.go index 2157d11e6..6feadac23 100644 --- a/externalapis/preflight/v1beta2/groupversion_info.go +++ b/externalapis/preflight/v1beta2/groupversion_info.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/externalapis/preflight/v1beta2/hostpreflight_types.go b/externalapis/preflight/v1beta2/hostpreflight_types.go index 2aff73568..982cfcb63 100644 --- a/externalapis/preflight/v1beta2/hostpreflight_types.go +++ b/externalapis/preflight/v1beta2/hostpreflight_types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -39,9 +39,9 @@ type HostPreflightStatus struct { troubleshoot.HostPreflightStatus `json:",inline"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:storageversion // HostPreflight is the Schema for the hostpreflights API type HostPreflight struct { @@ -52,7 +52,7 @@ type HostPreflight struct { Status HostPreflightStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // HostPreflightList contains a list of HostPreflight type HostPreflightList struct { diff --git a/externalapis/preflight/v1beta2/preflight_types.go b/externalapis/preflight/v1beta2/preflight_types.go index 4e8995463..683ce5c80 100644 --- a/externalapis/preflight/v1beta2/preflight_types.go +++ b/externalapis/preflight/v1beta2/preflight_types.go @@ -1,9 +1,12 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + 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. @@ -36,9 +39,9 @@ type PreflightStatus struct { troubleshoot.PreflightStatus `json:",inline"` } -//+kubebuilder:object:root=true -//+kubebuilder:subresource:status -//+kubebuilder:storageversion +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:storageversion // Preflight is the Schema for the preflights API type Preflight struct { @@ -49,7 +52,7 @@ type Preflight struct { Status PreflightStatus `json:"status,omitempty"` } -//+kubebuilder:object:root=true +// +kubebuilder:object:root=true // PreflightList contains a list of Preflight type PreflightList struct { diff --git a/externalapis/preflight/v1beta2/type.go b/externalapis/preflight/v1beta2/type.go index 896d0a2ee..5781b515a 100644 --- a/externalapis/preflight/v1beta2/type.go +++ b/externalapis/preflight/v1beta2/type.go @@ -1,9 +1,12 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + 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. @@ -13,7 +16,11 @@ limitations under the License. package v1beta2 -import troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +import ( + v1 "k8s.io/api/core/v1" + + troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) // ExtendCollect defines extended data collector for k8s cluster type ExtendCollect struct { @@ -21,68 +28,101 @@ type ExtendCollect struct { // ClusterAccessAnalyze analyzes the accessibility of target type ClusterAccessAnalyze struct { - // analyzeMeta is defined in troubleshoot.sh + // AnalyzeMeta is defined in troubleshoot.sh troubleshoot.AnalyzeMeta `json:",inline"` - // outcomes are expected user defined results. + // Outcomes are expected user defined results. // +kubebuilder:validation:Required Outcomes []*troubleshoot.Outcome `json:"outcomes"` } type ExtendAnalyze struct { - // clusterAccess is to determine the accessibility of target k8s cluster + // ClusterAccess is to determine the accessibility of target k8s cluster // +optional ClusterAccess *ClusterAccessAnalyze `json:"clusterAccess,omitempty"` + // StorageClass is to determine the correctness of target storage class + // +optional + StorageClass *KBStorageClassAnalyze `json:"storageClass,omitempty"` + // Taint is to Determine the matching between the taint and toleration + // +optional + Taint *KBTaintAnalyze `json:"taint,omitempty"` } type HostUtility struct { - // hostCollectorMeta is defined in troubleshoot.sh + // HostCollectorMeta is defined in troubleshoot.sh troubleshoot.HostCollectorMeta `json:",inline"` - // utilityName indicates the utility which will be checked in local host + // UtilityName indicates the utility which will be checked in local host // +kubebuilder:validation:Required UtilityName string `json:"utilityName"` } type ClusterRegion struct { - // hostCollectorMeta is defined in troubleshoot.sh + // HostCollectorMeta is defined in troubleshoot.sh troubleshoot.HostCollectorMeta `json:",inline"` - // providerName denotes which cloud provider target k8s located on + // ProviderName denotes the cloud provider target k8s located on // +kubebuilder:validation:Required ProviderName string `json:"providerName"` } type ExtendHostCollect struct { - // hostUtility is to collect the data of target utility. + // HostUtility is to collect the data of target utility. // +optional HostUtility *HostUtility `json:"hostUtility,omitempty"` - // ClusterRegion is to collect the data of target k8s + // ClusterRegion is region of target k8s // +optional ClusterRegion *ClusterRegion `json:"clusterRegion,omitempty"` } type HostUtilityAnalyze struct { - // hostCollectorMeta is defined in troubleshoot.sh + // HostCollectorMeta is defined in troubleshoot.sh troubleshoot.AnalyzeMeta `json:",inline"` - // collectorName indicates which collect data will be analyzed + // CollectorName indicates the collected data to be analyzed // +optional CollectorName string `json:"collectorName,omitempty"` - // outcomes are expected user defined results + // Outcomes are expected user defined results // +kubebuilder:validation:Required Outcomes []*troubleshoot.Outcome `json:"outcomes"` } type ClusterRegionAnalyze struct { - // analyzeMeta is defined in troubleshoot.sh + // AnalyzeMeta is defined in troubleshoot.sh troubleshoot.AnalyzeMeta `json:",inline"` - // outcomes are expected user defined results. + // Outcomes are expected user defined results. // +kubebuilder:validation:Required Outcomes []*troubleshoot.Outcome `json:"outcomes"` - // regionNames is a set of expected region names + // RegionNames is a set of expected region names // +kubebuilder:validation:Required RegionNames []string `json:"regionNames"` } +// KBStorageClassAnalyze replaces default storageClassAnalyze in preflight +type KBStorageClassAnalyze struct { + // AnalyzeMeta is defined in troubleshoot.sh + troubleshoot.AnalyzeMeta `json:",inline"` + // Outcomes are expected user defined results. + // +kubebuilder:validation:Required + Outcomes []*troubleshoot.Outcome `json:"outcomes"` + // StorageClassType is StorageClass type + // +kubebuilder:validation:Required + StorageClassType string `json:"storageClassType"` + // Provisioner is the provisioner of StorageClass + // +optional + Provisioner string `json:"provisioner,omitempty"` +} + +// KBTaintAnalyze matches the analysis of taints with TolerationsMap +type KBTaintAnalyze struct { + // AnalyzeMeta is defined in troubleshoot.sh + troubleshoot.AnalyzeMeta `json:",inline"` + // Outcomes are expected user defined results. + // +kubebuilder:validation:Required + Outcomes []*troubleshoot.Outcome `json:"outcomes"` + // Tolerations are toleration configuration passed by kbcli + // +optional + TolerationsMap map[string][]v1.Toleration `json:"tolerations"` +} + type ExtendHostAnalyze struct { - // hostUtility is to analyze the presence of target utility. + // HostUtility is to analyze the presence of target utility // +optional HostUtility *HostUtilityAnalyze `json:"hostUtility,omitempty"` // ClusterRegion is to validate the regionName of target k8s cluster diff --git a/go.mod b/go.mod index ce66694f2..193d928d5 100644 --- a/go.mod +++ b/go.mod @@ -1,17 +1,20 @@ module github.com/apecloud/kubeblocks -go 1.19 +go 1.20 require ( cuelang.org/go v0.4.3 github.com/DATA-DOG/go-sqlmock v1.5.0 github.com/Masterminds/semver/v3 v3.2.0 github.com/Masterminds/sprig/v3 v3.2.3 + github.com/Shopify/sarama v1.30.0 github.com/StudioSol/set v1.0.0 - github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 + github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 github.com/authzed/controller-idioms v0.7.0 github.com/bhmj/jsonslice v1.1.2 github.com/briandowns/spinner v1.23.0 + github.com/cenkalti/backoff/v4 v4.2.0 + github.com/chaos-mesh/chaos-mesh/api v0.0.0-20230423031423-0b31a519b502 github.com/clbanning/mxj/v2 v2.5.7 github.com/containerd/stargz-snapshotter/estargz v0.13.0 github.com/containers/common v0.49.1 @@ -39,13 +42,15 @@ require ( github.com/hashicorp/terraform-exec v0.18.0 github.com/jackc/pgx/v5 v5.2.0 github.com/jedib0t/go-pretty/v6 v6.4.4 + github.com/json-iterator/go v1.1.12 github.com/k3d-io/k3d/v5 v5.4.4 + github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0 github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0 github.com/leaanthony/debme v1.2.1 github.com/manifoldco/promptui v0.9.0 github.com/mitchellh/mapstructure v1.5.1-0.20220423185008-bf980b35cac4 - github.com/onsi/ginkgo/v2 v2.7.0 - github.com/onsi/gomega v1.25.0 + github.com/onsi/ginkgo/v2 v2.9.1 + github.com/onsi/gomega v1.27.4 github.com/opencontainers/image-spec v1.1.0-rc2 github.com/pingcap/go-tpc v1.0.9 github.com/pkg/errors v0.9.1 @@ -53,6 +58,7 @@ require ( github.com/redis/go-redis/v9 v9.0.1 github.com/replicatedhq/termui/v3 v3.1.1-0.20200811145416-f40076d26851 github.com/replicatedhq/troubleshoot v0.57.0 + github.com/russross/blackfriday/v2 v2.1.0 github.com/sethvargo/go-password v0.2.0 github.com/shirou/gopsutil/v3 v3.23.1 github.com/sirupsen/logrus v1.9.0 @@ -64,6 +70,7 @@ require ( github.com/sykesm/zap-logfmt v0.0.4 github.com/valyala/fasthttp v1.41.0 github.com/vmware-tanzu/velero v1.10.1 + github.com/xdg-go/scram v1.1.1 go.etcd.io/etcd/client/v3 v3.5.6 go.etcd.io/etcd/server/v3 v3.5.6 go.mongodb.org/mongo-driver v1.11.1 @@ -71,8 +78,10 @@ require ( go.uber.org/zap v1.24.0 golang.org/x/crypto v0.5.0 golang.org/x/exp v0.0.0-20221028150844-83b7d23a625f - golang.org/x/net v0.7.0 + golang.org/x/net v0.8.0 + golang.org/x/oauth2 v0.4.0 golang.org/x/sync v0.1.0 + golang.org/x/text v0.8.0 google.golang.org/grpc v1.52.0 google.golang.org/protobuf v1.28.1 gopkg.in/inf.v0 v0.9.1 @@ -85,11 +94,13 @@ require ( k8s.io/client-go v0.26.1 k8s.io/component-base v0.26.1 k8s.io/cri-api v0.25.0 - k8s.io/klog/v2 v2.90.0 - k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 + k8s.io/gengo v0.0.0-20220913193501-391367153a38 + k8s.io/klog v1.0.0 + k8s.io/klog/v2 v2.90.1 + k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a k8s.io/kubectl v0.26.0 k8s.io/metrics v0.26.0 - k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 + k8s.io/utils v0.0.0-20230209194617-a36077c30491 sigs.k8s.io/controller-runtime v0.14.4 sigs.k8s.io/kustomize/kyaml v0.13.9 sigs.k8s.io/yaml v1.3.0 @@ -115,6 +126,8 @@ require ( github.com/PuerkitoBio/purell v1.1.1 // indirect github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 // indirect github.com/acomagu/bufpipe v1.0.3 // indirect + github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 // indirect + github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a // indirect github.com/andybalholm/brotli v1.0.4 // indirect github.com/antlr/antlr4/runtime/Go/antlr v1.4.10 // indirect github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect @@ -125,14 +138,13 @@ require ( github.com/blang/semver/v4 v4.0.0 // indirect github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f // indirect github.com/cenkalti/backoff v2.2.1+incompatible // indirect - github.com/cenkalti/backoff/v4 v4.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/chai2010/gettext-go v1.0.2 // indirect github.com/chzyer/readline v1.5.1 // indirect - github.com/cloudflare/circl v1.1.0 // indirect + github.com/cloudflare/circl v1.3.3 // indirect github.com/cockroachdb/apd/v2 v2.0.1 // indirect github.com/containerd/cgroups v1.0.4 // indirect - github.com/containerd/containerd v1.6.15 // indirect + github.com/containerd/containerd v1.6.18 // indirect github.com/containers/image/v5 v5.24.0 // indirect github.com/containers/libtrust v0.0.0-20230121012942-c1716e8a8d01 // indirect github.com/containers/ocicrypt v1.1.7 // indirect @@ -142,15 +154,19 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/cyphar/filepath-securejoin v0.2.3 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/daviddengcn/go-colortext v1.0.0 // indirect github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dimchansky/utfbom v1.1.1 // indirect github.com/distribution/distribution/v3 v3.0.0-20221208165359-362910506bc2 // indirect - github.com/docker/distribution v2.8.1+incompatible // indirect + github.com/docker/distribution v2.8.2+incompatible // indirect github.com/docker/docker-credential-helpers v0.7.0 // indirect github.com/docker/go v1.5.1-1.0.20160303222718-d30aec9fd63c // indirect github.com/docker/go-metrics v0.0.1 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect + github.com/eapache/go-resiliency v1.2.0 // indirect + github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 // indirect + github.com/eapache/queue v1.1.0 // indirect github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/emicklei/proto v1.6.15 // indirect github.com/emirpasic/gods v1.18.1 // indirect @@ -169,11 +185,12 @@ require ( github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-openapi/errors v0.20.3 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/jsonreference v0.20.0 // indirect + github.com/go-openapi/jsonpointer v0.19.6 // indirect + github.com/go-openapi/jsonreference v0.20.1 // indirect github.com/go-openapi/strfmt v0.21.3 // indirect github.com/go-openapi/swag v0.22.3 // indirect github.com/go-redis/redis/v7 v7.4.1 // indirect + github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect github.com/go-test/deep v1.0.8 // indirect github.com/gobwas/glob v0.2.3 // indirect github.com/godbus/dbus/v5 v5.1.0 // indirect @@ -181,7 +198,7 @@ require ( github.com/golang-jwt/jwt/v4 v4.4.2 // indirect github.com/golang/glog v1.0.0 // 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.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/goodhosts/hostsfile v0.1.1 // indirect github.com/google/btree v1.0.1 // indirect @@ -189,6 +206,7 @@ require ( github.com/google/gnostic v0.6.9 // indirect github.com/google/go-intervals v0.0.2 // indirect github.com/google/gofuzz v1.2.0 // indirect + github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.2.1 // indirect github.com/googleapis/gax-go/v2 v2.7.0 // indirect @@ -204,9 +222,10 @@ require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-getter v1.6.2 // indirect + github.com/hashicorp/go-getter v1.7.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-safetemp v1.0.0 // indirect + github.com/hashicorp/go-uuid v1.0.2 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/hashicorp/hcl v1.0.0 // indirect github.com/hashicorp/terraform-json v0.15.0 // indirect @@ -218,13 +237,17 @@ require ( github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect github.com/jackc/puddle/v2 v2.1.2 // indirect github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.0.0 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.2 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/jedib0t/go-pretty v4.3.0+incompatible // indirect github.com/jhump/protoreflect v1.13.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/jmoiron/sqlx v1.3.5 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/josharian/intern v1.0.0 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/kevinburke/ssh_config v1.2.0 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/klauspost/pgzip v1.2.6-0.20220930104621-17e8dac29df8 // indirect @@ -234,6 +257,7 @@ require ( github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect github.com/lib/pq v1.10.7 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect + github.com/lithammer/dedent v1.1.0 // indirect github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e // indirect github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/magiconair/properties v1.8.7 // indirect @@ -249,7 +273,7 @@ require ( github.com/mistifyio/go-zfs/v3 v3.0.0 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/mitchellh/go-testing-interface v1.14.0 // indirect + github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/moby/locker v1.0.1 // indirect @@ -264,6 +288,7 @@ require ( github.com/morikuni/aec v1.0.0 // indirect github.com/mpvl/unique v0.0.0-20150818121801-cbe035fff7de // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect github.com/nsf/termbox-go v0.0.0-20190121233118-02980233997d // indirect github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/tablewriter v0.0.5 // indirect @@ -275,6 +300,7 @@ require ( github.com/ostreedev/ostree-go v0.0.0-20210805093236-719684c64e4f // indirect github.com/pelletier/go-toml/v2 v2.0.6 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect + github.com/pierrec/lz4 v2.6.1+incompatible // indirect github.com/pjbgf/sha1cd v0.2.3 // indirect github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/prometheus/client_golang v1.14.0 // indirect @@ -283,9 +309,10 @@ require ( github.com/prometheus/procfs v0.8.0 // indirect github.com/prometheus/statsd_exporter v0.22.3 // indirect github.com/protocolbuffers/txtpbfmt v0.0.0-20201118171849-f6a6b3f636fc // indirect + github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 // indirect github.com/rivo/uniseg v0.4.3 // indirect + github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rubenv/sql-migrate v1.2.0 // indirect - github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/savsgio/gotils v0.0.0-20220530130905-52f3993e8d6d // indirect github.com/segmentio/ksuid v1.0.4 // indirect github.com/sergi/go-diff v1.2.0 // indirect @@ -311,7 +338,6 @@ require ( github.com/vbatts/tar-split v0.11.2 // indirect github.com/xanzy/ssh-agent v0.3.3 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect - github.com/xdg-go/scram v1.1.1 // indirect github.com/xdg-go/stringprep v1.0.3 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -342,14 +368,12 @@ require ( go.uber.org/atomic v1.10.0 // indirect go.uber.org/multierr v1.8.0 // indirect go4.org/intern v0.0.0-20211027215823-ae77deb06f29 // indirect - go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/oauth2 v0.4.0 // indirect - golang.org/x/sys v0.5.0 // indirect - golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 // indirect + golang.org/x/mod v0.9.0 // indirect + golang.org/x/sys v0.6.0 // indirect + golang.org/x/term v0.6.0 // indirect golang.org/x/time v0.3.0 // indirect - golang.org/x/tools v0.4.0 // indirect + golang.org/x/tools v0.7.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect gomodules.xyz/jsonpatch/v2 v2.2.0 // indirect google.golang.org/api v0.107.0 // indirect @@ -361,19 +385,17 @@ require ( gopkg.in/yaml.v3 v3.0.1 // indirect inet.af/netaddr v0.0.0-20211027220019-c74959edd3b6 // indirect k8s.io/apiserver v0.26.1 // indirect + k8s.io/component-helpers v0.26.0 // indirect oras.land/oras-go v1.2.2 // indirect periph.io/x/host/v3 v3.8.0 // indirect - sigs.k8s.io/json v0.0.0-20220713155537-f223a00ba0e2 // indirect + sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/kustomize/api v0.12.1 // indirect + sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 // indirect sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect ) replace ( - // github.com/google/certificate-transparency-go => github.com/google/certificate-transparency-go v1.1.3 - // github.com/coreos/etcd => github.com/coreos/etcd v3.5.5+incompatible - github.com/hashicorp/terraform => github.com/apecloud/terraform v1.3.0-20220927 github.com/spf13/afero => github.com/spf13/afero v1.2.2 go.opentelemetry.io/otel => go.opentelemetry.io/otel v1.10.0 go.opentelemetry.io/otel/trace => go.opentelemetry.io/otel/trace v1.10.0 -// go.etcd.io/etcd/v3 => go.etcd.io/etcd/v3 v3.5.5+incompatible ) diff --git a/go.sum b/go.sum index 1989c2b82..8a44138fe 100644 --- a/go.sum +++ b/go.sum @@ -20,35 +20,173 @@ cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmW 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/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= +cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= +cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= +cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= +cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= +cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= cloud.google.com/go v0.105.0 h1:DNtEKRBAAzeS4KyIory52wWHuClNaXJ5x1F7xa4q+5Y= cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= +cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= +cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= +cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= +cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= +cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= +cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= +cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= +cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= +cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= +cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= +cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= +cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= +cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= +cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= +cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= +cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= 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.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= 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.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= +cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= +cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= +cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= +cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= +cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= +cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= +cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= +cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= +cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= +cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= +cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= +cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= +cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= +cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= cloud.google.com/go/compute v1.14.0 h1:hfm2+FfxVmnRlh6LpB7cg1ZNU+5edAHmW679JePztk0= cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= +cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= +cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= +cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= +cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= +cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= +cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= +cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= +cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= +cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= +cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= +cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= +cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= +cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= 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/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= +cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= +cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= +cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= +cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= +cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= +cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= +cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= +cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= +cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= +cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk= +cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= +cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= +cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= +cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= +cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= +cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= +cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= +cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= +cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= +cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= +cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= cloud.google.com/go/iam v0.8.0 h1:E2osAkZzxI/+8pZcxVLcDtAQx/u+hZXVryUaYQ5O0Kk= cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= +cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= +cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= +cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= +cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= cloud.google.com/go/longrunning v0.3.0 h1:NjljC+FYPV3uh5/OwWT6pVU+doBqMg2x/rZlE+CamDs= +cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= +cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= +cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= +cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= +cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= +cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= +cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= +cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= +cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= +cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= +cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= +cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= +cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= +cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= +cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= +cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= +cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= +cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= +cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= +cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= +cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= +cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= +cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= +cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= +cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= +cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= +cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= +cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= +cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= +cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= +cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= +cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= +cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= +cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= +cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= +cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= +cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= +cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= +cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= +cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= +cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= +cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= +cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= +cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= +cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= +cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= cloud.google.com/go/storage v1.27.0 h1:YOO045NZI9RKfCj1c5A/ZtuuENUc8OAW+gHdGnDgyMQ= cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= +cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= +cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= +cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= +cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= +cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= +cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= +cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= +cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= +cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= +cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= +cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= contrib.go.opencensus.io/exporter/prometheus v0.4.1 h1:oObVeKo2NxpdF/fIfrPsNj6K0Prg0R0mHM+uANlYMiM= contrib.go.opencensus.io/exporter/prometheus v0.4.1/go.mod h1:t9wvfitlUjGXG2IXAZsuFq26mDGid/JwCEXp+gTG/9U= cuelang.org/go v0.4.3 h1:W3oBBjDTm7+IZfCKZAmC8uDG0eYfJL4Pp/xbbCMKaVo= @@ -160,7 +298,9 @@ github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdko github.com/Shopify/logrus-bugsnag v0.0.0-20170309145241-6dbc35f2c30d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d h1:UrqY+r/OJnIp5u0s1SbQ8dVfLCZJsnvazdBP5hS4iRs= github.com/Shopify/logrus-bugsnag v0.0.0-20171204204709-577dee27f20d/go.mod h1:HI8ITrYtUY+O+ZhtlqUnD8+KwNPOyugEhfP9fdUIaEQ= +github.com/Shopify/sarama v1.30.0 h1:TOZL6r37xJBDEMLx4yjB77jxbZYXPaDow08TSK6vIL0= github.com/Shopify/sarama v1.30.0/go.mod h1:zujlQQx1kzHsh4jfV1USnptCQrHAEZ2Hk8fTKCulPVs= +github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae h1:ePgznFqEG1v3AjMklnK8H7BSc++FDSo7xfK9K7Af+0Y= github.com/Shopify/toxiproxy/v2 v2.1.6-0.20210914104332-15ea381dcdae/go.mod h1:/cvHQkZ1fst0EmZnA5dFtiQdWCNCFYzb+uE2vqVgvx0= github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg= github.com/StudioSol/set v1.0.0 h1:G27J71la+Da08WidabBkoRrvPLTa4cdCn0RjvyJ5WKQ= @@ -170,11 +310,15 @@ github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ github.com/agrea/ptr v0.0.0-20180711073057-77a518d99b7b h1:WMhlIaJkDgEQSVJQM06YV+cYUl1r5OY5//ijMXJNqtA= github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412 h1:vOVO0ypMfTt6tZacyI0kp+iCZb1XSNiYDqnzBWYgfe4= github.com/ahmetalpbalkan/go-cursor v0.0.0-20131010032410-8136607ea412/go.mod h1:AI9hp1tkp10pAlK5TCwL+7yWbRgtDm9jhToq6qij2xs= +github.com/ahmetb/gen-crd-api-reference-docs v0.3.0 h1:+XfOU14S4bGuwyvCijJwhhBIjYN+YXS18jrCY2EzJaY= +github.com/ahmetb/gen-crd-api-reference-docs v0.3.0/go.mod h1:TdjdkYhlOifCQWPs1UdTma97kQQMozf5h26hTuG70u8= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 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-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a h1:E/8AP5dFtMhl5KPJz66Kt9G0n+7Sn41Fy1wv9/jHOrc= +github.com/alecthomas/units v0.0.0-20210927113745-59d0afb8317a/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= github.com/alexflint/go-filemutex v0.0.0-20171022225611-72bdc8eae2ae/go.mod h1:CgnQgUtFrFz9mxFNtED3jI5tLDjKlOM+oUF/sTk6ps0= github.com/andybalholm/brotli v1.0.0/go.mod h1:loMXtMfwqflxFJPmdbJO0a3KNoPuLBgiu3qAvBg8x/Y= github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= @@ -200,7 +344,6 @@ github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:W github.com/authzed/controller-idioms v0.7.0 h1:HhNMUBb8hJzYqY3mhen3B2AC5nsIem3fBe0tC/AAOHo= github.com/authzed/controller-idioms v0.7.0/go.mod h1:0B/PmqCguKv8b3azSMF+HdyKpKr2o3UAZ5eo12Ze8Fo= github.com/aws/aws-sdk-go v1.15.11/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZoCYDt7FT0= -github.com/aws/aws-sdk-go v1.15.78/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3ATZkfNZeM= github.com/aws/aws-sdk-go v1.44.122 h1:p6mw01WBaNpbdP2xrisz5tIkcNwzj/HysobNoaAHjgo= github.com/aws/aws-sdk-go v1.44.122/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= @@ -245,6 +388,7 @@ github.com/bugsnag/osext v0.0.0-20130617224835-0dd3f918b21b/go.mod h1:obH5gd0Bsq github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0 h1:nvj0OLI3YqYXer/kZD8Ri1aaunCxIEsOst1BVJswV0o= github.com/bugsnag/panicwrap v0.0.0-20151223152923-e2c28503fcd0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/bxcodec/faker v2.0.1+incompatible h1:P0KUpUw5w6WJXwrPfv35oc91i4d8nf40Nwln+M/+faA= github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f h1:tRk+aBit+q3oqnj/1mF5HHhP2yxJM2lSa0afOJxQ3nE= github.com/c9s/goprocinfo v0.0.0-20170724085704-0010a05ce49f/go.mod h1:uEyr4WpAH4hio6LFriaPkL938XnrvLpNPmQHBdrmbIE= github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= @@ -264,6 +408,8 @@ github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chai2010/gettext-go v1.0.2 h1:1Lwwip6Q2QGsAdl/ZKPCwTe9fe0CjlUbqj5bFNSjIRk= github.com/chai2010/gettext-go v1.0.2/go.mod h1:y+wnP2cHYaVj19NZhYKAwEMH2CI1gNHeQQ+5AjwawxA= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20230423031423-0b31a519b502 h1:dlu7F5rX2PA4laECDbFXwtDKktUK31lcC09wU70L3QY= +github.com/chaos-mesh/chaos-mesh/api v0.0.0-20230423031423-0b31a519b502/go.mod h1:5qllHIhMkPEWjIimDum42JtMj0P1Tn9x91XUceuPNjY= github.com/checkpoint-restore/go-criu/v4 v4.1.0/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw= github.com/checkpoint-restore/go-criu/v5 v5.0.0/go.mod h1:cfwC0EG7HMUenopBsUf9d89JlCLQIfgVcNsNN0t6T2M= github.com/checkpoint-restore/go-criu/v5 v5.3.0/go.mod h1:E/eQpaFtUKGOOSEBZgmKAcn+zUUwWxqcaKZlF54wK8E= @@ -289,8 +435,9 @@ github.com/clbanning/mxj/v2 v2.5.7/go.mod h1:hNiWqW14h+kc+MdF9C6/YoRfjEJoR3ou6tn github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004 h1:lkAMpLVBDaj17e85keuznYcH5rqI438v41pKcBl4ZxQ= github.com/cloudflare/cfssl v0.0.0-20180223231731-4e2dcbde5004/go.mod h1:yMWuSON2oQp+43nFtAV/uvKQIFpSPerB57DCt9t8sSA= -github.com/cloudflare/circl v1.1.0 h1:bZgT/A+cikZnKIwn7xL2OBj012Bmvho/o6RpRvv3GKY= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs= +github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 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/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= @@ -346,8 +493,8 @@ github.com/containerd/containerd v1.5.0-beta.4/go.mod h1:GmdgZd2zA2GYIBZ0w09Zvgq github.com/containerd/containerd v1.5.0-rc.0/go.mod h1:V/IXoMqNGgBlabz3tHD2TWDoTJseu1FGOKuoA4nNb2s= github.com/containerd/containerd v1.5.1/go.mod h1:0DOxVqwDy2iZvrZp2JUx/E+hS0UNTVn7dJnIOwtYR4g= github.com/containerd/containerd v1.5.7/go.mod h1:gyvv6+ugqY25TiXxcZC3L5yOeYgEw0QMhscqVp1AR9c= -github.com/containerd/containerd v1.6.15 h1:4wWexxzLNHNE46aIETc6ge4TofO550v+BlLoANrbses= -github.com/containerd/containerd v1.6.15/go.mod h1:U2NnBPIhzJDm59xF7xB2MMHnKtggpZ+phKg8o2TKj2c= +github.com/containerd/containerd v1.6.18 h1:qZbsLvmyu+Vlty0/Ex5xc0z2YtKpIsb5n45mAMI+2Ns= +github.com/containerd/containerd v1.6.18/go.mod h1:1RdCUu95+gc2v9t3IL+zIlpClSmew7/0YS8O5eQZrOw= github.com/containerd/continuity v0.0.0-20190426062206-aaeac12a7ffc/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20190815185530-f2a389ac0a02/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= github.com/containerd/continuity v0.0.0-20191127005431-f65d91d395eb/go.mod h1:GL3xCUCBDV3CZiTSEKksMWbLE66hEyuu9qyDOOqM47Y= @@ -458,6 +605,8 @@ github.com/dapr/kit v0.0.3/go.mod h1:+vh2UIRT0KzFm5YJWfj7az4XVSdodys1OCz1WzNe1Eo 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/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/daviddengcn/go-colortext v1.0.0 h1:ANqDyC0ys6qCSvuEK7l3g5RaehL/Xck9EX8ATG8oKsE= +github.com/daviddengcn/go-colortext v1.0.0/go.mod h1:zDqEI5NVUop5QPpVJUxE9UO10hRnmkD5G4Pmri9+m4c= github.com/denis-tingajkin/go-header v0.3.1/go.mod h1:sq/2IxMhaZX+RRcgHfCRx/m0M5na0fBt4/CRe7Lrji0= github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/denisenkom/go-mssqldb v0.9.0/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= @@ -478,8 +627,8 @@ github.com/docker/cli v20.10.24+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hH github.com/docker/distribution v0.0.0-20190905152932-14b96e55d84c/go.mod h1:0+TTO4EOBfRPhZXAeF1Vu+W3hHZ8eLp8PgKVZlcvtFY= github.com/docker/distribution v2.7.1-0.20190205005809-0d3efadf0154+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/distribution v2.7.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= -github.com/docker/distribution v2.8.1+incompatible h1:Q50tZOPR6T/hjNsyc9g8/syEs6bk8XXApsHjKukMl68= -github.com/docker/distribution v2.8.1+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= +github.com/docker/distribution v2.8.2+incompatible h1:T3de5rq0dB1j30rp0sA2rER+m322EBzniBPB6ZIzuh8= +github.com/docker/distribution v2.8.2+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= github.com/docker/docker v1.4.2-0.20190924003213-a8608b5b67c7/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/docker v20.10.24+incompatible h1:Ugvxm7a8+Gz6vqQYQQ2W7GYq5EUPaAiuPgIfVyI3dYE= github.com/docker/docker v20.10.24+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= @@ -509,8 +658,11 @@ github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dvsekhvalnov/jose2go v0.0.0-20170216131308-f21a8cedbbae/go.mod h1:7BvyPhdbLxMXIYTFPLsyJRFMsKmOZnQmzh6Gb+uquuM= github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/eapache/go-resiliency v1.2.0 h1:v7g92e/KSN71Rq7vSThKaWIq68fL4YHvWyiUKorFR1Q= github.com/eapache/go-resiliency v1.2.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21 h1:YEetp8/yCZMuEPMUDHG0CW/brkkEp8mzqk2+ODEitlw= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0 h1:YOEu7KNc61ntiQlcEeUIoDTJ2o8mQznoNvUhiigpIqc= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= github.com/elazarl/goproxy v0.0.0-20180725130230-947c36da3153/go.mod h1:/Zj4wYkgs4iZTTu3o/KG3Itv/qCCa8VVMlb3i9OVuzc= github.com/elazarl/goproxy v0.0.0-20191011121108-aa519ddbe484 h1:pEtiCjIXx3RvGjlUJuCNxNOw0MNblyR9Wi+vJGBFh+8= @@ -556,6 +708,7 @@ github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/flowstack/go-jsonschema v0.1.1/go.mod h1:yL7fNggx1o8rm9RlgXv7hTBWxdBM0rVwpMwimd3F3N0= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= +github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= @@ -625,13 +778,13 @@ github.com/go-openapi/errors v0.20.3/go.mod h1:Z3FlZ4I8jEGxjUK+bugx3on2mIAk4txuA github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0= github.com/go-openapi/jsonpointer v0.19.2/go.mod h1:3akKfEdA7DF1sugOqz1dVQHBcuDBPKZGEoHC/NkiQRg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= -github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.19.6 h1:eCs3fxoIi3Wh6vtgmLTOjdhSpiqphQ+DaPn38N2ZdrE= +github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs= github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg= github.com/go-openapi/jsonreference v0.19.2/go.mod h1:jMjeRr2HHw6nAVajTXJ4eiUwohSTlpa0o73RUL1owJc= github.com/go-openapi/jsonreference v0.19.3/go.mod h1:rjx6GuL8TTa9VaixXglHmQmIL98+wF9xc8zWvFonSJ8= -github.com/go-openapi/jsonreference v0.20.0 h1:MYlu0sBgChmCfJxxUKZ8g1cPWFOB37YSZqewK7OKeyA= -github.com/go-openapi/jsonreference v0.20.0/go.mod h1:Ag74Ico3lPc+zR+qjn4XBUmXymS4zJbYVCZmcgkasdo= +github.com/go-openapi/jsonreference v0.20.1 h1:FBLnyygC4/IZZr893oiomc9XaghoveYTrLC1F86HID8= +github.com/go-openapi/jsonreference v0.20.1/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k= github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc= github.com/go-openapi/spec v0.19.3/go.mod h1:FpwSN1ksY1eteniUU7X0N/BgJ7a4WvBFVA8Lj9mJglo= github.com/go-openapi/strfmt v0.21.3 h1:xwhj5X6CjXEZZHMWy1zKJxvW9AfHC9pkyUjLvHtKG7o= @@ -653,6 +806,7 @@ github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LB github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= 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 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/go-test/deep v1.0.8 h1:TDsG77qcSprGbC6vTN8OuXp5g+J+b5Pcguhf7Zt61VM= github.com/go-test/deep v1.0.8/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= @@ -736,9 +890,11 @@ github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw 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.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= -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.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golangci/check v0.0.0-20180506172741-cfe4005ccda2/go.mod h1:k9Qvh+8juN+UKMCS/3jFtGICgW8O96FVaZsaxdzDkR4= @@ -756,6 +912,11 @@ github.com/golangci/misspell v0.0.0-20180809174111-950f5d19e770/go.mod h1:dEbvlS github.com/golangci/prealloc v0.0.0-20180630174525-215b22d4de21/go.mod h1:tf5+bzsHdTM0bsB7+8mt0GUMvjCgwLpTapNZHU8AajI= github.com/golangci/revgrep v0.0.0-20180526074752-d9c87f5ffaf0/go.mod h1:qOQCunEYvmd/TLamH+7LlVccLvUH5kZNhbCgTHoBbp4= github.com/golangci/unconvert v0.0.0-20180507085042-28b1c447d1f4/go.mod h1:Izgrg8RkN3rCIMLGE9CyYmU9pY2Jer6DgANEnZ/L/cQ= +github.com/golangplus/bytes v0.0.0-20160111154220-45c989fe5450/go.mod h1:Bk6SMAONeMXrxql8uvOKuAZSu8aM5RUGv+1C6IJaEho= +github.com/golangplus/bytes v1.0.0/go.mod h1:AdRaCFwmc/00ZzELMWb01soso6W1R/++O1XL80yAn+A= +github.com/golangplus/fmt v1.0.0/go.mod h1:zpM0OfbMCjPtd2qkTD/jX2MgiFCqklhSUFyDW44gVQE= +github.com/golangplus/testing v1.0.0 h1:+ZeeiKZENNOMkTTELoSySazi+XaEhVO0mb+eanrSEUQ= +github.com/golangplus/testing v1.0.0/go.mod h1:ZDreixUV3YzhoVraIDyOzHrr76p6NUh6k/pPg/Q3gYA= github.com/gomodule/redigo v1.8.2 h1:H5XSIre1MB5NbPYFp+i1NBbb5qN1W8Y8YAQoAYbkm8k= github.com/goodhosts/hostsfile v0.1.1 h1:SqRUTFOshOCon0ZSXDrW1bkKZvs4+5pRgYFWySdaLno= github.com/goodhosts/hostsfile v0.1.1/go.mod h1:lXcUP8xO4WR5vvuQ3F/N0bMQoclOtYKEEUnyY2jTusY= @@ -797,6 +958,7 @@ github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXi 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 h1:d8MncMlErDFTwQGBK1xhv026j9kqhvw1Qv9IbWT1VLQ= +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-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -808,6 +970,10 @@ github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLe 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 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= +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/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= @@ -817,13 +983,24 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= +github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= github.com/googleapis/enterprise-certificate-proxy v0.2.1 h1:RY7tHKZcRlk788d5WSo/e83gOyyy742E8GSs771ySpg= github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= 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.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= +github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= +github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= +github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= +github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= +github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= +github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= github.com/googleapis/gax-go/v2 v2.7.0 h1:IcsPKeInNvYi7eqSaDjiZqDDKu5rsmunY0Y1YupQSSQ= github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= github.com/googleapis/gnostic v0.4.1/go.mod h1:LRhVm6pbyptWbWbuZ38d1eyptfvIytN3ir6b65WBswg= +github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= github.com/gookit/color v1.2.5/go.mod h1:AhIE+pS6D4Ql0SQWbBeXPHw7gY0/sjHoA4s/n1KB7xg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/handlers v0.0.0-20150720190736-60c7bfde3e33/go.mod h1:Qkdc/uu4tH4g6mTK6auzZ766c4CA0Ng8+o/OAirnOIQ= @@ -874,8 +1051,8 @@ github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtng 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-getter v1.6.2 h1:7jX7xcB+uVCliddZgeKyNxv0xoT7qL5KDtH7rU4IqIk= -github.com/hashicorp/go-getter v1.6.2/go.mod h1:IZCrswsZPeWv9IkVnLElzRU/gz/QPi6pZHn4tv6vbwA= +github.com/hashicorp/go-getter v1.7.0 h1:bzrYP+qu/gMrL1au7/aDvkoOVGUJpeKBgbqRHACAFDY= +github.com/hashicorp/go-getter v1.7.0/go.mod h1:W7TalhMmbPmsSMdNjD0ZskARur/9GJ17cfHTRtXV744= github.com/hashicorp/go-hclog v1.3.1 h1:vDwF1DFNZhntP4DAjuTpOw3uEgMUpXh1pB5fW9DqHpo= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= @@ -891,8 +1068,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.2 h1:cfejS+Tpcp13yd5nYHWDI6qVCny6wyX2Mt5SGur2IGE= github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= -github.com/hashicorp/go-version v1.1.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= @@ -943,11 +1120,17 @@ github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.0.0 h1:J7uCkflzTEhUZ64xqKnkDxq3kzc96ajM1Gli5ktUem8= github.com/jcmturner/gofork v1.0.0/go.mod h1:MK8+TM0La+2rjBD4jE12Kj1pCCxK7d2LK/UM3ncEo0o= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.2 h1:6ZIM6b/JJN0X8UM43ZOM6Z4SJzla+a/u7scXFJzodkA= github.com/jcmturner/gokrb5/v8 v8.4.2/go.mod h1:sb+Xq/fTY5yktf/VxLsE3wlfPqQjp0aWNYyvBVK62bc= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jedib0t/go-pretty v4.3.0+incompatible h1:CGs8AVhEKg/n9YbUenWmNStRW2PHJzaeDodcfvRAbIo= github.com/jedib0t/go-pretty v4.3.0+incompatible/go.mod h1:XemHduiw8R651AF9Pt4FwCTKeG3oo7hrHJAoznj9nag= @@ -1011,12 +1194,12 @@ github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.11.13/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.0/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47etQ6QUkpK6sk= github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= +github.com/klauspost/compress v1.15.11/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.15.12/go.mod h1:QPwzmACJjUTFsnSHH934V6woptycfrDDJnH7hvFVbGM= github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.15.15/go.mod h1:ZcK2JAFqKOpnBlxcLsJzYfrS9X1akm9fHZNnD9+Vo/4= @@ -1039,6 +1222,8 @@ github.com/kr/pty v1.1.5/go.mod h1:9r2w37qlBe7rQ6e1fg1S/9xpWHSnaqNdHD3WcMdbPDA= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0 h1:OYDCOjVcx/5wNzlZ/At8otRibUlw0T6R0xOD31f32bw= +github.com/kubernetes-csi/external-snapshotter/client/v3 v3.0.0/go.mod h1:Q7VUue/CIrKbtpBdF04a1yjGGgsMaDws1HUxtjzgnEY= github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0 h1:nHHjmvjitIiyPlUHk/ofpgvBcNcawJLtf4PYHORLjAA= github.com/kubernetes-csi/external-snapshotter/client/v4 v4.2.0/go.mod h1:YBCo4DoEeDndqvAn6eeu0vWM7QdXmHEeI9cFWplmBys= github.com/kubernetes-csi/external-snapshotter/client/v6 v6.2.0 h1:cMM5AB37e9aRGjErygVT6EuBPB6s5a+l95OPERmSlVM= @@ -1064,6 +1249,8 @@ github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhnIaL+V+BEER86oLrvS+kWobKpbJuye0= github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo= +github.com/lithammer/dedent v1.1.0 h1:VNzHMVCBNG1j0fh3OrsFRkVUwStdDArbgBWoPAffktY= +github.com/lithammer/dedent v1.1.0/go.mod h1:jrXYCQtgg0nJiN+StA2KgR7w6CiQNv9Fd/Z9BP0jIOc= github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4= github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e h1:hz4quJkaJWDo+xW+G6wTF6d6/95QvJ+o2D0+bB/tJ1U= github.com/longhorn/go-iscsi-helper v0.0.0-20210330030558-49a327fb024e/go.mod h1:9z/y9glKmWEdV50tjlUPxFwi1goQfIrrsoZbnMyIZbY= @@ -1159,8 +1346,8 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= -github.com/mitchellh/go-testing-interface v1.14.0 h1:/x0XQ6h+3U3nAyk1yx+bHPURrKa9sVVvYbuqZ7pIAtI= -github.com/mitchellh/go-testing-interface v1.14.0/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= +github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= +github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= @@ -1214,6 +1401,7 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= github.com/nakabonne/nestif v0.3.0/go.mod h1:dI314BppzXjJ4HsCnbo7XzrJHPszZsjnk5wEBSYHI2c= github.com/natefinch/atomic v1.0.1 h1:ZPYKxkqQOx3KZ+RsbnP/YsgvxWQPGxjC0oBt2AhwV0A= @@ -1245,8 +1433,8 @@ github.com/onsi/ginkgo v1.13.0/go.mod h1:+REjRxOmWfHCjfv9TTWB1jD1Frx4XydAD3zm1ls github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 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/v2 v2.7.0 h1:/XxtEV3I3Eif/HobnVx9YmJgk8ENdRsuUmM+fLCFNow= -github.com/onsi/ginkgo/v2 v2.7.0/go.mod h1:yjiuMwPokqY1XauOgju45q3sJt6VzQ/Fict1LFVcsAo= +github.com/onsi/ginkgo/v2 v2.9.1 h1:zie5Ly042PD3bsCvsSOPvRnFwyo3rKe64TJlD6nu0mk= +github.com/onsi/ginkgo/v2 v2.9.1/go.mod h1:FEcmzVcCHl+4o9bQZVab+4dC9+j+91t2FHSzmGAPfuo= github.com/onsi/gomega v0.0.0-20151007035656-2152b45fa28a/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= @@ -1256,8 +1444,8 @@ github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoT github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.3/go.mod h1:V9xEwhxec5O8UDM77eCW8vLymOMltsqPVYWrpDsH8xc= github.com/onsi/gomega v1.16.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= -github.com/onsi/gomega v1.25.0 h1:Vw7br2PCDYijJHSfBOWhov+8cAnUf8MfMaIOV323l6Y= -github.com/onsi/gomega v1.25.0/go.mod h1:r+zV744Re+DiYCIPRlYOTxn0YkOLcAnW8k1xXdMPGhM= +github.com/onsi/gomega v1.27.4 h1:Z2AnStgsdSayCMDiCU42qIz+HLqEPcgiOCXjAU/w+8E= +github.com/onsi/gomega v1.27.4/go.mod h1:riYq/GJKh8hhoM01HN6Vmuy93AarCXCBGpvFDK3q3fQ= github.com/opencontainers/go-digest v0.0.0-20170106003457-a6d0ee40d420/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v0.0.0-20180430190053-c9281466c8b2/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= github.com/opencontainers/go-digest v1.0.0-rc1/go.mod h1:cMLVZDEM3+U2I4VmLI6N8jQYUd2OVphdqWwCJHrFt2s= @@ -1385,6 +1573,7 @@ github.com/quasilyte/go-consistent v0.0.0-20190521200055-c6f3937de18c/go.mod h1: github.com/quasilyte/go-ruleguard v0.2.0/go.mod h1:2RT/tf0Ce0UDj5y243iWKosQogJd8+1G3Rs2fxmlYnw= github.com/quasilyte/regex/syntax v0.0.0-20200407221936-30656e2c4a95/go.mod h1:rlzQ04UMyJXu/aOvhd8qT+hvDrFpiwqp8MRXDY9szc0= github.com/rabbitmq/amqp091-go v1.1.0/go.mod h1:ogQDLSOACsLPsIq0NpbtiifNZi2YOz0VTJ0kHRghqbM= +github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475 h1:N/ElC8H3+5XpJzTSTfLsJV/mx9Q9g7kxmchpfZyxgzM= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/redis/go-redis/v9 v9.0.1 h1:L1B0L2Y7dQMnKxwfzSwemceGlQwVUsqJ1kjkdaoNhts= github.com/redis/go-redis/v9 v9.0.1/go.mod h1:/xDTe9EF1LM61hek62Poq2nzQSGj0xSrEtEHbBQevps= @@ -1395,6 +1584,8 @@ github.com/replicatedhq/troubleshoot v0.57.0/go.mod h1:R5VdixzaBXfWLbP9mcLuZKs/b github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3 h1:utMvzDsuh3suAEnhH0RdHmoPbU648o6CvXxTx4SBMOw= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= +github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= 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= @@ -1553,7 +1744,7 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tommy-muehle/go-mnd v1.3.1-0.20200224220436-e6f9a994e8fa/go.mod h1:dSUh0FtTP8VhvkL1S+gUR1OKd9ZnSaozuI6r3m6wOig= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= -github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= +github.com/ulikunitz/xz v0.5.10/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/ultraware/funlen v0.0.3/go.mod h1:Dp4UiAus7Wdb9KUZsYWZEWiRzGuM2kXM1lPbfaF6xhA= @@ -1731,8 +1922,8 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296 h1:QJ/xcIANMLApehfgPCHnfK1hZiaMmbaTVmPv7DAoTbo= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230221090011-e4bae7ad2296/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181009213950-7c1a557ab941/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= @@ -1804,8 +1995,9 @@ 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/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180811021610-c39426892332/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1861,6 +2053,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +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-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= @@ -1869,13 +2062,22 @@ golang.org/x/net v0.0.0-20210917221730-978cfadd31cf/go.mod h1:9nx3DQGgdP8bBQD5qx golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 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-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= +golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220826154423-83b083e8dc8b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/net v0.0.0-20220906165146-f3363e06e74c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ= +golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 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-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -1889,8 +2091,19 @@ golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/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-20211104180415-d3ed0bb246c8/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-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= +golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= +golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= +golang.org/x/oauth2 v0.1.0/go.mod h1:G9FE4dLTsbXUu90h/Pf85g4w1D+SSAgR+q46nJZ8M4A= golang.org/x/oauth2 v0.4.0 h1:NF0gk8LVPg1Ml7SSbGyySuoxdsXitj7TvgvuRxIMc/M= golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1904,7 +2117,9 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ 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-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -2000,40 +2215,57 @@ golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/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-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-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-20210809222454-d867a43fc93e/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-20210906170528-6f6e22806c34/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-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211210111614-af8b64212486/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-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220517195934-5e4e11fc645e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/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-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220825204002-c680a09ffe64/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0 h1:n2a8QNdAb0sZNpU9R1ALUXBbY+w51fCQDN+7EdxNBsY= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.6.0 h1:clScbb1cHjoCkyRbWwBEUZ5H/tIFu5TAXIqaZD0Gcjw= +golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 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.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -2045,8 +2277,8 @@ 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.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68= +golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/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= @@ -2139,15 +2371,21 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f 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/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.4.0 h1:7mTAgkunk3fr4GAloyyCasadO6h9zSsQZbwvcaIciV4= -golang.org/x/tools v0.4.0/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4= +golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= 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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= gomodules.xyz/jsonpatch/v2 v2.2.0 h1:4pT439QV83L+G9FkcCriY6EkpcK6r6bK+A5FBUMI7qY= @@ -2175,6 +2413,33 @@ google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjR 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.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8= +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.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= +google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= +google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= +google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= +google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= +google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= +google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= +google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= +google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= +google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= +google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= +google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= +google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= +google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= +google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= +google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= +google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= google.golang.org/api v0.107.0 h1:I2SlFjD8ZWabaIFOfeEDg3pf0BHJDh6iYQ1ic3Yu/UU= google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= @@ -2230,10 +2495,69 @@ google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6D 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-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= 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-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= +google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= google.golang.org/genproto v0.0.0-20220107163113-42d7afdf6368/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= +google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= +google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= +google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= +google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= +google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= +google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= +google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= +google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= +google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= +google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= +google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= +google.golang.org/genproto v0.0.0-20221025140454-527a21cfbd71/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf h1:/JqRexUvugu6JURQ0O7RfV1EnvgrOxUV4tSjuAv0Sr0= google.golang.org/genproto v0.0.0-20230104163317-caabf589fcbf/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= google.golang.org/grpc v0.0.0-20160317175043-d3ddb4469d5a/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= @@ -2260,14 +2584,27 @@ google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA5 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 v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= google.golang.org/grpc v1.41.0/go.mod h1:U3l9uK9J0sini8mHphKoXyaqDA/8VyGnDee1zzIUK6k= google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= +google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= +google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc v1.52.0 h1:kd48UiU7EHsV4rnLyOJRuP/Il/UHE7gdDAQ+SZI7nZk= google.golang.org/grpc v1.52.0/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= +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-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= @@ -2387,6 +2724,8 @@ k8s.io/component-base v0.20.4/go.mod h1:t4p9EdiagbVCJKrQ1RsA5/V4rFQNDfRlevJajlGw k8s.io/component-base v0.20.6/go.mod h1:6f1MPBAeI+mvuts3sIdtpjljHWBQ2cIy38oBIWMYnrM= k8s.io/component-base v0.26.1 h1:4ahudpeQXHZL5kko+iDHqLj/FSGAEUnSVO0EBbgDd+4= k8s.io/component-base v0.26.1/go.mod h1:VHrLR0b58oC035w6YQiBSbtsf0ThuSwXP+p5dD/kAWU= +k8s.io/component-helpers v0.26.0 h1:KNgwqs3EUdK0HLfW4GhnbD+q/Zl9U021VfIU7qoVYFk= +k8s.io/component-helpers v0.26.0/go.mod h1:jHN01qS/Jdj95WCbTe9S2VZ9yxpxXNY488WjF+yW4fo= k8s.io/cri-api v0.17.3/go.mod h1:X1sbHmuXhwaHs9xxYffLqJogVsnI+f6cPRcgPel7ywM= k8s.io/cri-api v0.20.1/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= k8s.io/cri-api v0.20.4/go.mod h1:2JRbKt+BFLTjtrILYVqQK5jqhI+XNdF6UiGMgczeBCI= @@ -2396,16 +2735,22 @@ k8s.io/cri-api v0.25.0/go.mod h1:J1rAyQkSJ2Q6I+aBMOVgg2/cbbebso6FNa0UagiR0kc= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200428234225-8167cfdcfc14/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20201203183100-97869a43a9d9/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/gengo v0.0.0-20220913193501-391367153a38 h1:yGN2TZt9XIl5wrcYaFtVMqzP2GIzX5gIcOObCZCuDeA= +k8s.io/gengo v0.0.0-20220913193501-391367153a38/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= +k8s.io/klog v0.2.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= +k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= +k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= -k8s.io/klog/v2 v2.90.0 h1:VkTxIV/FjRXn1fgNNcKGM8cfmL1Z33ZjXRTVxKCoF5M= -k8s.io/klog/v2 v2.90.0/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= +k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw= +k8s.io/klog/v2 v2.90.1/go.mod h1:y1WjHnz7Dj687irZUWR/WLkLc5N1YHtjLdmgWjndZn0= k8s.io/kube-aggregator v0.19.12 h1:OwyNUe/7/gxzEnaLd3sC9Yrpx0fZAERzvFslX5Qq5g8= k8s.io/kube-openapi v0.0.0-20200805222855-6aeccd4b50c6/go.mod h1:UuqjUnNftUyPE5H64/qeyjQoUZhGpeFDVdxjTeEVN2o= k8s.io/kube-openapi v0.0.0-20201113171705-d219536bb9fd/go.mod h1:WOJ3KddDSol4tAGcJo0Tvi+dK12EcqSLqcWsryKMpfM= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280 h1:+70TFaan3hfJzs+7VK2o+OGxg8HsuBr/5f6tVAjDu6E= -k8s.io/kube-openapi v0.0.0-20221012153701-172d655c2280/go.mod h1:+Axhij7bCpeqhklhUTe3xmOn6bWxolyZEeyaFpjGtl4= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a h1:gmovKNur38vgoWfGtP5QOGNOA7ki4n6qNYoFAgMlNvg= +k8s.io/kube-openapi v0.0.0-20230308215209-15aac26d736a/go.mod h1:y5VtZWM9sHHc2ZodIH/6SHzXj+TPU5USoA8lcIeKEKY= k8s.io/kubectl v0.26.0 h1:xmrzoKR9CyNdzxBmXV7jW9Ln8WMrwRK6hGbbf69o4T0= k8s.io/kubectl v0.26.0/go.mod h1:eInP0b+U9XUJWSYeU9XZnTA+cVYuWyl3iYPGtru0qhQ= k8s.io/kubernetes v1.13.0/go.mod h1:ocZa8+6APFNC2tX1DZASIbocyYT5jHzqFVsY5aoB7Jk= @@ -2413,8 +2758,8 @@ k8s.io/metrics v0.26.0 h1:U/NzZHKDrIVGL93AUMRkqqXjOah3wGvjSnKmG/5NVCs= k8s.io/metrics v0.26.0/go.mod h1:cf5MlG4ZgWaEFZrR9+sOImhZ2ICMpIdNurA+D8snIs8= k8s.io/utils v0.0.0-20200729134348-d5654de09c73/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= k8s.io/utils v0.0.0-20201110183641-67b214c5f920/go.mod h1:jPW/WVKK9YHAvNhRxK0md/EJ228hCsBRufyofKtW8HA= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448 h1:KTgPnR10d5zhztWptI952TNtt/4u5h3IzDXkdIMuo2Y= -k8s.io/utils v0.0.0-20221128185143-99ec85e7a448/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/utils v0.0.0-20230209194617-a36077c30491 h1:r0BAOLElQnnFhE/ApUsg3iHdVYYPBjNSSOMowRZxxsY= +k8s.io/utils v0.0.0-20230209194617-a36077c30491/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= mvdan.cc/gofumpt v0.0.0-20200709182408-4fd085cb6d5f/go.mod h1:9VQ397fNXEnF84t90W4r4TRCQK+pg9f8ugVfyj+S26w= mvdan.cc/interfacer v0.0.0-20180901003855-c20040233aed/go.mod h1:Xkxe497xwlCKkIaQYRfC7CSLworTXY9RMqwhhCm+8Nc= mvdan.cc/lint v0.0.0-20170908181259-adc824a0674b/go.mod h1:2odslEg/xrtNQqCYg2/jCoyKnw3vv5biOc3JnIcYfL4= @@ -2430,10 +2775,12 @@ sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.14/go.mod h1:LEScyz sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.0.15/go.mod h1:LEScyzhFmoF5pso/YSeBstl57mOzx9xlU9n85RGrDQg= sigs.k8s.io/controller-runtime v0.14.4 h1:Kd/Qgx5pd2XUL08eOV2vwIq3L9GhIbJ5Nxengbd4/0M= sigs.k8s.io/controller-runtime v0.14.4/go.mod h1:WqIdsAY6JBsjfc/CqO0CORmNtoCtE4S6qbPc9s68h+0= -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-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= +sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/kustomize/api v0.12.1 h1:7YM7gW3kYBwtKvoY216ZzY+8hM+lV53LUayghNRJ0vM= sigs.k8s.io/kustomize/api v0.12.1/go.mod h1:y3JUhimkZkR6sbLNwfJHxvo1TCLwuwm14sCYnkH6S1s= +sigs.k8s.io/kustomize/kustomize/v4 v4.5.7 h1:cDW6AVMl6t/SLuQaezMET8hgnadZGIAr8tUrxFVOrpg= +sigs.k8s.io/kustomize/kustomize/v4 v4.5.7/go.mod h1:VSNKEH9D9d9bLiWEGbS6Xbg/Ih0tgQalmPvntzRxZ/Q= sigs.k8s.io/kustomize/kyaml v0.13.9 h1:Qz53EAaFFANyNgyOEJbT/yoIHygK40/ZcvU3rgry2Tk= sigs.k8s.io/kustomize/kyaml v0.13.9/go.mod h1:QsRbD0/KcU+wdk0/L0fIp2KLnohkVzs6fQ85/nOXac4= sigs.k8s.io/structured-merge-diff/v4 v4.0.1/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= diff --git a/hack/boilerplate.cue.txt b/hack/boilerplate.cue.txt index 4500eda5d..be951210c 100644 --- a/hack/boilerplate.cue.txt +++ b/hack/boilerplate.cue.txt @@ -1,14 +1,17 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . diff --git a/hack/boilerplate.go.txt b/hack/boilerplate.go.txt index 53620698c..8b1b64f82 100644 --- a/hack/boilerplate.go.txt +++ b/hack/boilerplate.go.txt @@ -1,16 +1,19 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ diff --git a/internal/cli/engine/types.go b/hack/boilerplate_apache2.go.txt similarity index 92% rename from internal/cli/engine/types.go rename to hack/boilerplate_apache2.go.txt index 15ae743d8..e424cc895 100644 --- a/internal/cli/engine/types.go +++ b/hack/boilerplate_apache2.go.txt @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -13,5 +13,3 @@ 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 engine diff --git a/hack/docgen/api/gen-api-doc-config.json b/hack/docgen/api/gen-api-doc-config.json new file mode 100644 index 000000000..ed8c2a561 --- /dev/null +++ b/hack/docgen/api/gen-api-doc-config.json @@ -0,0 +1,35 @@ +{ + "hideMemberFields": [ + "TypeMeta" + ], + "hideTypePatterns": [ + "ParseError$", + "List$" + ], + "externalPackages": [ + { + "typeMatchPrefix": "^k8s\\.io/(api|apimachinery/pkg/apis)/", + "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}" + }, + { + "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/api/", + "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-core" + }, + { + "typeMatchPrefix": "^k8s\\.io/apiextensions-apiserver/pkg/apis/apiextensions", + "docsURLTemplate": "https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.25/#{{lower .TypeIdentifier}}-{{arrIndex .PackageSegments -1}}-{{arrIndex .PackageSegments -2}}-k8s-io" + }, + { + "typeMatchPrefix": "^k8s\\.io/apimachinery/pkg/util", + "docsURLTemplate": "https://pkg.go.dev/k8s.io/apimachinery/pkg/util/{{arrIndex .PackageSegments -1}}#{{.TypeIdentifier}}" + } + ], + "typeDisplayNamePrefixOverrides": { + "k8s.io/api/": "Kubernetes ", + "k8s.io/apimachinery/pkg/apis/": "Kubernetes ", + "k8s.io/apimachinery/pkg/api/": "Kubernetes ", + "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/": "Kubernetes api extensions ", + "k8s.io/apimachinery/pkg/util/": "Kubernetes api utils " + }, + "markdownDisabled": false +} diff --git a/hack/docgen/api/generate.sh b/hack/docgen/api/generate.sh new file mode 100755 index 000000000..eecab3c3b --- /dev/null +++ b/hack/docgen/api/generate.sh @@ -0,0 +1,29 @@ +#!/bin/bash +#Copyright (C) 2022-2023 ApeCloud Co., Ltd +# +#This file is part of KubeBlocks project +# +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. +# +#This program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see + + +set -o errexit +set -o nounset +set -o pipefail + +SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )" +PROJECT_DIR=${SCRIPT_DIR%/*/*/*} + +echo "> Generate API docs from $PROJECT_DIR" +go generate "$PROJECT_DIR/apis/..." + diff --git a/hack/docgen/api/main.go b/hack/docgen/api/main.go new file mode 100644 index 000000000..a60a20368 --- /dev/null +++ b/hack/docgen/api/main.go @@ -0,0 +1,706 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +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. +*/ + +/* +Fork from https://github.com/ahmetb/gen-crd-api-reference-docs +*/ + +package main + +import ( + "bytes" + "encoding/json" + "flag" + "fmt" + "html/template" + "io" + "net/http" + "os" + "path/filepath" + "reflect" + "regexp" + "sort" + "strconv" + "strings" + texttemplate "text/template" + "time" + "unicode" + + "golang.org/x/text/cases" + "golang.org/x/text/language" + + "github.com/pkg/errors" + "github.com/russross/blackfriday/v2" + "k8s.io/gengo/parser" + "k8s.io/gengo/types" + "k8s.io/klog" +) + +var ( + flConfig = flag.String("config", "", "path to config file") + flAPIDir = flag.String("api-dir", "", "api directory (or import path), point this to pkg/apis") + flTemplateDir = flag.String("template-dir", "template", "path to template/ dir") + + flHTTPAddr = flag.String("http-addr", "", "start an HTTP server on specified addr to view the result (e.g. :8080)") + flOutFile = flag.String("out-file", "", "path to output file to save the result") + + apiOrder = map[string]int{"cluster": 1, "backup": 2, "add-on": 3} +) + +const ( + docCommentForceIncludes = "// +gencrdrefdocs:force" +) + +type generatorConfig struct { + // HiddenMemberFields hides fields with specified names on all types. + HiddenMemberFields []string `json:"hideMemberFields"` + + // HideTypePatterns hides types matching the specified patterns from the + // output. + HideTypePatterns []string `json:"hideTypePatterns"` + + // ExternalPackages lists recognized external package references and how to + // link to them. + ExternalPackages []externalPackage `json:"externalPackages"` + + // TypeDisplayNamePrefixOverrides is a mapping of how to override displayed + // name for types with certain prefixes with what value. + TypeDisplayNamePrefixOverrides map[string]string `json:"typeDisplayNamePrefixOverrides"` + + // MarkdownDisabled controls markdown rendering for comment lines. + MarkdownDisabled bool `json:"markdownDisabled"` +} + +type externalPackage struct { + TypeMatchPrefix string `json:"typeMatchPrefix"` + DocsURLTemplate string `json:"docsURLTemplate"` +} + +type apiPackage struct { + apiGroup string + apiVersion string + GoPackages []*types.Package + Types []*types.Type // because multiple 'types.Package's can add types to an apiVersion + Constants []*types.Type +} + +func (v *apiPackage) identifier() string { return fmt.Sprintf("%s/%s", v.apiGroup, v.apiVersion) } + +func init() { + klog.InitFlags(nil) + err := flag.Set("alsologtostderr", "true") + if err != nil { + return + } + flag.Parse() + + if *flConfig == "" { + panic("-config not specified") + } + if *flAPIDir == "" { + panic("-api-dir not specified") + } + if *flHTTPAddr == "" && *flOutFile == "" { + panic("-out-file or -http-addr must be specified") + } + if *flHTTPAddr != "" && *flOutFile != "" { + panic("only -out-file or -http-addr can be specified") + } + if err := resolveTemplateDir(*flTemplateDir); err != nil { + panic(err) + } + +} + +func resolveTemplateDir(dir string) error { + path, err := filepath.Abs(dir) + if err != nil { + return err + } + if fi, err := os.Stat(path); err != nil { + return errors.Wrapf(err, "cannot read the %s directory", path) + } else if !fi.IsDir() { + return errors.Errorf("%s path is not a directory", path) + } + return nil +} + +func main() { + defer klog.Flush() + + f, err := os.Open(*flConfig) + if err != nil { + klog.Fatalf("failed to open config file: %+v", err) + } + d := json.NewDecoder(f) + d.DisallowUnknownFields() + var config generatorConfig + if err := d.Decode(&config); err != nil { + klog.Fatalf("failed to parse config file: %+v", err) + } + + klog.Infof("parsing go packages in directory %s", *flAPIDir) + pkgs, err := parseAPIPackages() + if err != nil { + klog.Fatal(err) + } + if len(pkgs) == 0 { + klog.Fatalf("no API packages found in %s", *flAPIDir) + } + + apiPackages, err := combineAPIPackages(pkgs) + if err != nil { + klog.Fatal(err) + } + + mkOutput := func() (string, error) { + var b bytes.Buffer + err := render(&b, apiPackages, config) + if err != nil { + return "", errors.Wrap(err, "failed to render the result") + } + + // remove trailing whitespace from each html line for markdown renderers + s := regexp.MustCompile(`(?m)^\s+`).ReplaceAllString(b.String(), "") + return s, nil + } + + if *flOutFile != "" { + dir := filepath.Dir(*flOutFile) + if err := os.MkdirAll(dir, 0755); err != nil { + klog.Fatalf("failed to create dir %s: %v", dir, err) + } + s, err := mkOutput() + if err != nil { + klog.Fatalf("failed: %+v", err) + } + if err := os.WriteFile(*flOutFile, []byte(s), 0644); err != nil { + klog.Fatalf("failed to write to out file: %v", err) + } + klog.Infof("written to %s", *flOutFile) + } + + if *flHTTPAddr != "" { + h := func(w http.ResponseWriter, r *http.Request) { + now := time.Now() + defer func() { klog.Infof("request took %v", time.Since(now)) }() + s, err := mkOutput() + if err != nil { + _, _ = fmt.Fprintf(w, "error: %+v", err) + klog.Warningf("failed: %+v", err) + } + if _, err := fmt.Fprint(w, s); err != nil { + klog.Warningf("response write error: %v", err) + } + } + http.HandleFunc("/", h) + klog.Infof("server listening at %s", *flHTTPAddr) + klog.Fatal(http.ListenAndServe(*flHTTPAddr, nil)) + } +} + +// groupName extracts the "//+groupName" meta-comment from the specified +// package's comments, or returns empty string if it cannot be found. +func groupName(pkg *types.Package) string { + m := types.ExtractCommentTags("+", pkg.Comments) + v := m["groupName"] + if len(v) == 1 { + return v[0] + } + return "" +} + +func parseAPIPackages() ([]*types.Package, error) { + b := parser.New() + // the following will silently fail (turn on -v=4 to see logs) + if err := b.AddDirRecursive(*flAPIDir); err != nil { + return nil, err + } + scan, err := b.FindTypes() + if err != nil { + return nil, errors.Wrap(err, "failed to parse pkgs and types") + } + var pkgNames []string + for p := range scan { + pkg := scan[p] + klog.V(3).Infof("trying package=%v groupName=%s", p, groupName(pkg)) + + // Do not pick up packages that are in vendor/ as API packages. (This + // happened in knative/eventing-sources/vendor/..., where a package + // matched the pattern, but it didn't have a compatible import path). + if isVendorPackage(pkg) { + klog.V(3).Infof("package=%v coming from vendor/, ignoring.", p) + continue + } + + if groupName(pkg) != "" && len(pkg.Types) > 0 || containsString(pkg.DocComments, docCommentForceIncludes) { + klog.V(3).Infof("package=%v has groupName and has types", p) + pkgNames = append(pkgNames, p) + } + } + sort.Strings(pkgNames) + var pkgs []*types.Package + for _, p := range pkgNames { + klog.Infof("using package=%s", p) + pkgs = append(pkgs, scan[p]) + } + return pkgs, nil +} + +func containsString(sl []string, str string) bool { + for _, s := range sl { + if str == s { + return true + } + } + return false +} + +// combineAPIPackages groups the Go packages by the they +// offer, and combines the types in them. +func combineAPIPackages(pkgs []*types.Package) ([]*apiPackage, error) { + pkgMap := make(map[string]*apiPackage) + var pkgIds []string + + flattenTypes := func(typeMap map[string]*types.Type) []*types.Type { + typeList := make([]*types.Type, 0, len(typeMap)) + + for _, t := range typeMap { + typeList = append(typeList, t) + } + + return typeList + } + + for _, pkg := range pkgs { + apiGroup, apiVersion, err := apiVersionForPackage(pkg) + if err != nil { + return nil, errors.Wrapf(err, "could not get apiVersion for package %s", pkg.Path) + } + + id := fmt.Sprintf("%s/%s", apiGroup, apiVersion) + v, ok := pkgMap[id] + if !ok { + pkgMap[id] = &apiPackage{ + apiGroup: apiGroup, + apiVersion: apiVersion, + Types: flattenTypes(pkg.Types), + Constants: flattenTypes(pkg.Constants), + GoPackages: []*types.Package{pkg}, + } + pkgIds = append(pkgIds, id) + } else { + v.Types = append(v.Types, flattenTypes(pkg.Types)...) + v.Constants = append(v.Constants, flattenTypes(pkg.Constants)...) + v.GoPackages = append(v.GoPackages, pkg) + } + } + + sort.Strings(pkgIds) + + out := make([]*apiPackage, 0, len(pkgMap)) + for _, id := range pkgIds { + out = append(out, pkgMap[id]) + } + return out, nil +} + +// isVendorPackage determines if package is coming from vendor/ dir. +func isVendorPackage(pkg *types.Package) bool { + vendorPattern := string(os.PathSeparator) + "vendor" + string(os.PathSeparator) + return strings.Contains(pkg.SourcePath, vendorPattern) +} + +func findTypeReferences(pkgs []*apiPackage) map[*types.Type][]*types.Type { + m := make(map[*types.Type][]*types.Type) + for _, pkg := range pkgs { + for _, typ := range pkg.Types { + for _, member := range typ.Members { + t := member.Type + t = tryDereference(t) + m[t] = append(m[t], typ) + } + } + } + return m +} + +func isExportedType(t *types.Type) bool { + return strings.Contains(strings.Join(t.SecondClosestCommentLines, "\n"), "+genclient") || strings.Contains(strings.Join(t.SecondClosestCommentLines, "\n"), "+kubebuilder:object:root=true") +} + +func fieldName(m types.Member) string { + v := reflect.StructTag(m.Tags).Get("json") + v = strings.TrimSuffix(v, ",omitempty") + v = strings.TrimSuffix(v, ",inline") + if v != "" { + return v + } + return m.Name +} + +func fieldEmbedded(m types.Member) bool { + return strings.Contains(reflect.StructTag(m.Tags).Get("json"), ",inline") +} + +func isLocalType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) bool { + t = tryDereference(t) + _, ok := typePkgMap[t] + return ok +} + +func renderComments(s []string, markdown bool) string { + s = filterCommentTags(s) + doc := strings.Join(s, "\n") + + if markdown { + doc = string(blackfriday.Run([]byte(doc))) + doc = strings.ReplaceAll(doc, "\n", string(template.HTML("
"))) + doc = strings.ReplaceAll(doc, "{", string(template.HTML("{"))) + doc = strings.ReplaceAll(doc, "}", string(template.HTML("}"))) + return doc + } + return nl2br(doc) +} + +func safe(s string) template.HTML { return template.HTML(s) } + +func toTitle(s string) string { return cases.Title(language.English).String(s) } + +func nl2br(s string) string { + return strings.ReplaceAll(s, "\n\n", string(template.HTML("

"))) +} + +func hiddenMember(m types.Member, c generatorConfig) bool { + for _, v := range c.HiddenMemberFields { + if m.Name == v { + return true + } + } + return false +} + +func typeIdentifier(t *types.Type) string { + t = tryDereference(t) + return t.Name.String() // {PackagePath.Name} +} + +// apiGroupForType looks up apiGroup for the given type +func apiGroupForType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) string { + t = tryDereference(t) + + v := typePkgMap[t] + if v == nil { + klog.Warningf("WARNING: cannot read apiVersion for %s from type=>pkg map", t.Name.String()) + return "" + } + + return v.identifier() +} + +// anchorIDForLocalType returns the #anchor string for the local type +func anchorIDForLocalType(t *types.Type, typePkgMap map[*types.Type]*apiPackage) string { + return fmt.Sprintf("%s.%s", apiGroupForType(t, typePkgMap), t.Name.Name) +} + +// linkForType returns an anchor to the type if it can be generated. returns +// empty string if it is not a local type or unrecognized external type. +func linkForType(t *types.Type, c generatorConfig, typePkgMap map[*types.Type]*apiPackage) (string, error) { + t = tryDereference(t) // dereference kind=Pointer + + if isLocalType(t, typePkgMap) { + return "#" + anchorIDForLocalType(t, typePkgMap), nil + } + + var arrIndex = func(a []string, i int) string { + return a[(len(a)+i)%len(a)] + } + + // types like k8s.io/apimachinery/pkg/apis/meta/v1.ObjectMeta, + // k8s.io/api/core/v1.Container, k8s.io/api/autoscaling/v1.CrossVersionObjectReference, + // github.com/knative/build/pkg/apis/build/v1alpha1.BuildSpec + if t.Kind == types.Struct || t.Kind == types.Pointer || t.Kind == types.Interface || t.Kind == types.Alias { + id := typeIdentifier(t) // gives {{ImportPath.Identifier}} for type + segments := strings.Split(t.Name.Package, "/") // to parse [meta, v1] from "k8s.io/apimachinery/pkg/apis/meta/v1" + + for _, v := range c.ExternalPackages { + r, err := regexp.Compile(v.TypeMatchPrefix) + if err != nil { + return "", errors.Wrapf(err, "pattern %q failed to compile", v.TypeMatchPrefix) + } + if r.MatchString(id) { + tpl, err := texttemplate.New("").Funcs(map[string]interface{}{ + "lower": strings.ToLower, + "arrIndex": arrIndex, + }).Parse(v.DocsURLTemplate) + if err != nil { + return "", errors.Wrap(err, "docs URL template failed to parse") + } + + var b bytes.Buffer + if err := tpl. + Execute(&b, map[string]interface{}{ + "TypeIdentifier": t.Name.Name, + "PackagePath": t.Name.Package, + "PackageSegments": segments, + }); err != nil { + return "", errors.Wrap(err, "docs url template execution error") + } + return b.String(), nil + } + } + klog.Warningf("not found external link source for type %v", t.Name) + } + return "", nil +} + +// tryDereference returns the underlying type when t is a pointer, map, or slice. +func tryDereference(t *types.Type) *types.Type { + for t.Elem != nil { + t = t.Elem + } + return t +} + +// finalUnderlyingTypeOf walks the type hierarchy for t and returns +// its base type (i.e. the type that has no further underlying type). +func finalUnderlyingTypeOf(t *types.Type) *types.Type { + for { + if t.Underlying == nil { + return t + } + + t = t.Underlying + } +} + +func typeDisplayName(t *types.Type, c generatorConfig, typePkgMap map[*types.Type]*apiPackage) string { + s := typeIdentifier(t) + + if isLocalType(t, typePkgMap) { + s = tryDereference(t).Name.Name + } + + if t.Kind == types.Pointer { + s = strings.TrimLeft(s, "*") + } + + switch t.Kind { + case types.Struct, + types.Interface, + types.Alias, + types.Pointer, + types.Slice, + types.Builtin: + // noop + case types.Map: + // return original name + return t.Name.Name + case types.DeclarationOf: + // For constants, we want to display the value + // rather than the name of the constant, since the + // value is what users will need to write into YAML + // specs. + if t.ConstValue != nil { + u := finalUnderlyingTypeOf(t) + // Quote string constants to make it clear to the documentation reader. + if u.Kind == types.Builtin && u.Name.Name == "string" { + return strconv.Quote(*t.ConstValue) + } + + return *t.ConstValue + } + klog.Fatalf("type %s is a non-const declaration, which is unhandled", t.Name) + default: + klog.Fatalf("type %s has kind=%v which is unhandled", t.Name, t.Kind) + } + + // substitute prefix, if registered + for prefix, replacement := range c.TypeDisplayNamePrefixOverrides { + if strings.HasPrefix(s, prefix) { + s = strings.Replace(s, prefix, replacement, 1) + } + } + + if t.Kind == types.Slice { + s = "[]" + s + } + + return s +} + +func hideType(t *types.Type, c generatorConfig) bool { + for _, pattern := range c.HideTypePatterns { + if regexp.MustCompile(pattern).MatchString(t.Name.String()) { + return true + } + } + if !isExportedType(t) && unicode.IsLower(rune(t.Name.Name[0])) { + // types that start with lowercase + return true + } + return false +} + +func typeReferences(t *types.Type, c generatorConfig, references map[*types.Type][]*types.Type) []*types.Type { + var out []*types.Type + m := make(map[*types.Type]struct{}) + for _, ref := range references[t] { + if !hideType(ref, c) { + m[ref] = struct{}{} + } + } + for k := range m { + out = append(out, k) + } + sortTypes(out) + return out +} + +func sortTypes(typs []*types.Type) []*types.Type { + sort.Slice(typs, func(i, j int) bool { + t1, t2 := typs[i], typs[j] + if isExportedType(t1) && !isExportedType(t2) { + return true + } else if !isExportedType(t1) && isExportedType(t2) { + return false + } + return t1.Name.String() < t2.Name.String() + }) + return typs +} + +func visibleTypes(in []*types.Type, c generatorConfig) []*types.Type { + var out []*types.Type + for _, t := range in { + if !hideType(t, c) { + out = append(out, t) + } + } + return out +} + +func filterCommentTags(comments []string) []string { + var out []string + for _, v := range comments { + if !strings.HasPrefix(strings.TrimSpace(v), "+") { + out = append(out, v) + } + } + return out +} + +func isOptionalMember(m types.Member) bool { + tags := types.ExtractCommentTags("+", m.CommentLines) + _, ok := tags["optional"] + return ok +} + +func apiVersionForPackage(pkg *types.Package) (string, string, error) { + group := groupName(pkg) + version := pkg.Name // assumes basename (i.e. "v1" in "core/v1") is apiVersion + r := `^v\d+((alpha|beta|api|stable)[a-z0-9]+)?$` + if !regexp.MustCompile(r).MatchString(version) { + return "", "", errors.Errorf("cannot infer kubernetes apiVersion of go package %s (basename %q doesn't match expected pattern %s that's used to determine apiVersion)", pkg.Path, version, r) + } + return group, version, nil +} + +// extractTypeToPackageMap creates a *types.Type map to apiPackage +func extractTypeToPackageMap(pkgs []*apiPackage) map[*types.Type]*apiPackage { + out := make(map[*types.Type]*apiPackage) + for _, ap := range pkgs { + for _, t := range ap.Types { + out[t] = ap + } + for _, t := range ap.Constants { + out[t] = ap + } + } + return out +} + +// constantsOfType finds all the constants in pkg that have the +// same underlying type as t. This is intended for use by enum +// type validation, where users need to specify one of a specific +// set of constant values for a field. +func constantsOfType(t *types.Type, pkg *apiPackage) []*types.Type { + var constants []*types.Type + + for _, c := range pkg.Constants { + if c.Underlying == t { + constants = append(constants, c) + } + } + + return sortTypes(constants) +} + +func getAPIOrder(filename string) int { + if order, ok := apiOrder[filename]; ok { + return order + } + return 1000 +} + +func render(w io.Writer, pkgs []*apiPackage, config generatorConfig) error { + references := findTypeReferences(pkgs) + typePkgMap := extractTypeToPackageMap(pkgs) + + t, err := template.New("").Funcs(map[string]interface{}{ + "isExportedType": isExportedType, + "fieldName": fieldName, + "fieldEmbedded": fieldEmbedded, + "typeIdentifier": typeIdentifier, + "typeDisplayName": func(t *types.Type) string { return typeDisplayName(t, config, typePkgMap) }, + "visibleTypes": func(t []*types.Type) []*types.Type { return visibleTypes(t, config) }, + "renderComments": func(s []string) string { return renderComments(s, !config.MarkdownDisabled) }, + "packageDisplayName": func(p *apiPackage) string { return p.identifier() }, + "apiGroup": func(t *types.Type) string { return apiGroupForType(t, typePkgMap) }, + "packageAnchorID": func(p *apiPackage) string { + return strings.ReplaceAll(p.identifier(), " ", "") + }, + "linkForType": func(t *types.Type) string { + v, err := linkForType(t, config, typePkgMap) + if err != nil { + klog.Fatal(errors.Wrapf(err, "error getting link for type=%s", t.Name)) + return "" + } + return v + }, + "anchorIDForType": func(t *types.Type) string { return anchorIDForLocalType(t, typePkgMap) }, + "safe": safe, + "toTitle": toTitle, + "sortedTypes": sortTypes, + "typeReferences": func(t *types.Type) []*types.Type { return typeReferences(t, config, references) }, + "hiddenMember": func(m types.Member) bool { return hiddenMember(m, config) }, + "isLocalType": isLocalType, + "isOptionalMember": isOptionalMember, + "constantsOfType": func(t *types.Type) []*types.Type { return constantsOfType(t, typePkgMap[t]) }, + }).ParseGlob(filepath.Join(*flTemplateDir, "*.tpl")) + if err != nil { + return errors.Wrap(err, "parse error") + } + + apiName := strings.Split(filepath.Base(*flOutFile), ".")[0] + filerOrder := getAPIOrder(apiName) + + return errors.Wrap(t.ExecuteTemplate(w, "packages", map[string]interface{}{ + "packages": pkgs, + "apiName": apiName, + "filerOrder": filerOrder, + }), "template execution error") +} diff --git a/hack/docgen/api/template/members.tpl b/hack/docgen/api/template/members.tpl new file mode 100644 index 000000000..a529c6716 --- /dev/null +++ b/hack/docgen/api/template/members.tpl @@ -0,0 +1,48 @@ +{{ define "members" }} + +{{ range .Members }} +{{ if not (hiddenMember .)}} + + + {{ fieldName . }}
+ + {{ if linkForType .Type }} + + {{ typeDisplayName .Type }} + + {{ else }} + {{ typeDisplayName .Type }} + {{ end }} + + + + {{ if fieldEmbedded . }} +

+ (Members of {{ fieldName . }} are embedded into this type.) +

+ {{ end}} + + {{ if isOptionalMember .}} + (Optional) + {{ end }} + + {{ safe (renderComments .CommentLines) }} + + {{ if and (eq (.Type.Name.Name) "ObjectMeta") }} + Refer to the Kubernetes API documentation for the fields of the + metadata field. + {{ end }} + + {{ if or (eq (fieldName .) "spec") }} +
+
+ + {{ template "members" .Type }} +
+ {{ end }} + + +{{ end }} +{{ end }} + +{{ end }} diff --git a/hack/docgen/api/template/pkg.tpl b/hack/docgen/api/template/pkg.tpl new file mode 100644 index 000000000..e5a22d228 --- /dev/null +++ b/hack/docgen/api/template/pkg.tpl @@ -0,0 +1,58 @@ +{{ define "packages" }} + +--- +title: {{ .apiName | toTitle }} API Reference +description: {{ .apiName | toTitle }} API Reference +keywords: [{{ .apiName }}, api] +sidebar_position: {{ .filerOrder }} +sidebar_label: {{ .apiName | toTitle }} +--- +
+ +{{ with .packages}} + +

Packages:

+ +{{ end}} + +{{ range .packages }} +

+ {{- packageDisplayName . -}} +

+ + {{ with (index .GoPackages 0 )}} + {{ with .DocComments }} +
+ {{ safe (renderComments .) }} +
+ {{ end }} + {{ end }} + + Resource Types: +
    + {{- range (visibleTypes (sortedTypes .Types)) -}} + {{ if isExportedType . -}} +
  • + {{ typeDisplayName . }} +
  • + {{- end }} + {{- end -}} +
+ + {{ range (visibleTypes (sortedTypes .Types))}} + {{ template "type" . }} + {{ end }} +
+{{ end }} + +

+ Generated with gen-crd-api-reference-docs +

+ +{{ end }} diff --git a/hack/docgen/api/template/placeholder.go b/hack/docgen/api/template/placeholder.go new file mode 100644 index 000000000..4d1847392 --- /dev/null +++ b/hack/docgen/api/template/placeholder.go @@ -0,0 +1,2 @@ +// Package template Placeholder file to make Go vendor this directory properly. +package template diff --git a/hack/docgen/api/template/type.tpl b/hack/docgen/api/template/type.tpl new file mode 100644 index 000000000..2ff1cd044 --- /dev/null +++ b/hack/docgen/api/template/type.tpl @@ -0,0 +1,79 @@ +{{ define "type" }} + +

+ {{- .Name.Name }} + {{ if eq .Kind "Alias" }}({{.Underlying}} alias){{ end -}} +

+{{ with (typeReferences .) }} +

+ (Appears on: + {{- $prev := "" -}} + {{- range . -}} + {{- if $prev -}}, {{ end -}} + {{- $prev = . -}} + {{ typeDisplayName . }} + {{- end -}} + ) +

+{{ end }} + +
+ {{ safe (renderComments .CommentLines) }} +
+ +{{ with (constantsOfType .) }} + + + + + + + + + {{- range . -}} + + {{- /* + renderComments implicitly creates a

element, so we + add one to the display name as well to make the contents + of the two cells align evenly. + */ -}} +

+ + + {{- end -}} + +
ValueDescription

{{ typeDisplayName . }}

{{ safe (renderComments .CommentLines) }}
+{{ end }} + +{{ if .Members }} + + + + + + + + + {{ if isExportedType . }} + + + + + + + + + {{ end }} + {{ template "members" .}} + +
FieldDescription
+ apiVersion
+ string
+ {{apiGroup .}} +
+ kind
+ string +
{{.Name.Name}}
+{{ end }} + +{{ end }} diff --git a/hack/docgen/cli/main.go b/hack/docgen/cli/main.go index 10bbe10a3..6118a7cec 100644 --- a/hack/docgen/cli/main.go +++ b/hack/docgen/cli/main.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package main diff --git a/hack/install_cli.ps1 b/hack/install_cli.ps1 index b742c426c..29befbfb8 100644 --- a/hack/install_cli.ps1 +++ b/hack/install_cli.ps1 @@ -1,3 +1,6 @@ +param ( + [string]$v +) # kbcli filename $CLI_FILENAME = "kbcli" @@ -10,8 +13,6 @@ $GITLAB_REPO = "85948" $GITLAB = "https://jihulab.com/api/v4/projects" $COUNTRY_CODE = "" -Import-Module Microsoft.PowerShell.Utility - function getCountryCode() { return (Invoke-WebRequest -Uri "https://ifconfig.io/country_code" -UseBasicParsing | Select-Object -ExpandProperty Content).Trim() } @@ -63,7 +64,7 @@ function getLatestRelease { $webClient = New-Object System.Net.WebClient $isDownLoaded = $False $Data = -$timeout = New-TimeSpan -Seconds 60 + function downloadFile { param ( $LATEST_RELEASE_TAG @@ -74,9 +75,8 @@ function downloadFile { $DOWNLOAD_BASE = "$GITLAB/$GITLAB_REPO/packages/generic/kubeblocks" } $DOWNLOAD_URL = "${DOWNLOAD_BASE}/${LATEST_RELEASE_TAG}/${CLI_ARTIFACT}" + # Check the Resource - # Write-Host DOWNLOAD_URL = $DOWNLOAD_URL - $webRequest = [System.Net.HttpWebRequest]::Create($DOWNLOAD_URL) $webRequest.Method = "HEAD" try { @@ -86,6 +86,7 @@ function downloadFile { Write-Host "Resource not found." exit 1 } + # Create the temp directory $CLI_TMP_ROOT = New-Item -ItemType Directory -Path (Join-Path $env:TEMP "kbcli-install-$(Get-Date -Format 'yyyyMMddHHmmss')") $Global:ARTIFACT_TMP_FILE = Join-Path $CLI_TMP_ROOT $CLI_ARTIFACT @@ -107,10 +108,10 @@ function downloadFile { $timer.Interval = 500 Register-ObjectEvent -InputObject $timer -EventName Elapsed -SourceIdentifier "TimerElapsed" -Action { - $precent = $Global:Data.SourceArgs.ProgressPercentage + $percent = $Global:Data.SourceArgs.ProgressPercentage $totalBytes = $Global:Data.SourceArgs.TotalBytesToReceive $receivedBytes = $Global:Data.SourceArgs.BytesReceived - if ($precent -ne $null) { + if ($percent -ne $null) { $downloadProgress = [Math]::Round(($receivedBytes / $totalBytes) * 100, 2) $status = "Downloaded {0} of {1} bytes" -f $receivedBytes, $totalBytes Write-Progress -Activity "Downloading kbcli..." -Status $status -PercentComplete $downloadProgress @@ -191,15 +192,15 @@ checkExistingCLI $COUNTRY_CODE = getCountryCode $ret_val -if (-not $args) { +if (-not $v) { Write-Host "Getting the latest kbcli ..." $ret_val = getLatestRelease } -elseif ($args[0] -match "^v.*$") { - $ret_val = $args[0] +elseif ($v -match "^v.*$") { + $ret_val = $v } else { - $ret_val = "v" + $args[0] + $ret_val = "v" + $v } $CLI_TMP_ROOT = downloadFile $ret_val @@ -213,6 +214,4 @@ try { } cleanup -installCompleted - - +installCompleted \ No newline at end of file diff --git a/hack/install_cli.sh b/hack/install_cli.sh index 47497dcba..348b1ad6d 100755 --- a/hack/install_cli.sh +++ b/hack/install_cli.sh @@ -1,17 +1,20 @@ #!/usr/bin/env bash -# Copyright ApeCloud, Inc. +#Copyright (C) 2022-2023 ApeCloud Co., Ltd # -# 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 +#This file is part of KubeBlocks project # -# http://www.apache.org/licenses/LICENSE-2.0 +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. # -# 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 program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . : ${CLI_INSTALL_DIR:="/usr/local/bin"} : ${CLI_BREW_INSTALL_DIR:="/opt/homebrew/bin"} diff --git a/hack/install_cli_docker.sh b/hack/install_cli_docker.sh index 5ac423c55..0e9a20c0e 100755 --- a/hack/install_cli_docker.sh +++ b/hack/install_cli_docker.sh @@ -1,17 +1,20 @@ #!/usr/bin/env bash -# Copyright ApeCloud, Inc. +#Copyright (C) 2022-2023 ApeCloud Co., Ltd # -# 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 +#This file is part of KubeBlocks project # -# http://www.apache.org/licenses/LICENSE-2.0 +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. # -# 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 program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. +# +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . : ${CLI_INSTALL_DIR:="/usr/local/bin"} : ${CLI_BREW_INSTALL_DIR:="/opt/homebrew/bin"} diff --git a/hack/license/header-check.sh b/hack/license/header-check.sh index 40086345e..2d1081191 100755 --- a/hack/license/header-check.sh +++ b/hack/license/header-check.sh @@ -1,23 +1,20 @@ #!/bin/bash -# Copyright ApeCloud, Inc. +#Copyright (C) 2022-2023 ApeCloud Co., Ltd # -# 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 +#This file is part of KubeBlocks project # -# http://www.apache.org/licenses/LICENSE-2.0 +#This program is free software: you can redistribute it and/or modify +#it under the terms of the GNU Affero General Public License as published by +#the Free Software Foundation, either version 3 of the License, or +#(at your option) any later version. # -# 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. - -# Easy & Dumb header check for CI jobs, currently checks ".go" files only. +#This program is distributed in the hope that it will be useful +#but WITHOUT ANY WARRANTY; without even the implied warranty of +#MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +#GNU Affero General Public License for more details. # -# This will be called by the CI system (with no args) to perform checking and -# fail the job if headers are not correctly set. It can also be called with the -# 'fix' argument to automatically add headers to the missing files. +#You should have received a copy of the GNU Affero General Public License +#along with this program. If not, see . # # Check if headers are fine: # $ ./hack/header-check.sh @@ -29,10 +26,12 @@ set -e -o pipefail # Initialize vars ERR=false FAIL=false +EXCLUDES_DIRS="vendor/\|apis/\|tools/\|externalapis/\|cmd/probe/internal/component/\|internal/cli/cmd/plugin/download" +APACHE2_DIRS="apis/\|externalapis/" -for file in $(git ls-files | grep '\.cue\|\.go$' | grep -v vendor/); do +for file in $(git ls-files | grep '\.cue\|\.go$' | grep -v ${EXCLUDES_DIRS}); do echo -n "Header check: $file... " - if [[ -z $(cat ${file} | grep "Copyright ApeCloud, Inc.\|Code generated by") ]]; then + if [[ -z $(cat ${file} | grep "Copyright (C) 2022-2023 ApeCloud Co., Ltd\|Code generated by") ]]; then ERR=true fi if [ $ERR == true ]; then @@ -52,5 +51,28 @@ for file in $(git ls-files | grep '\.cue\|\.go$' | grep -v vendor/); do fi done + +for file in $(git ls-files | grep '\.go$' | grep ${APACHE2_DIRS}); do + echo -n "Header check: $file... " + if [[ -z $(cat ${file} | grep "Copyright (C) 2022-2023 ApeCloud Co., Ltd\|Code generated by") ]]; then + ERR=true + fi + if [ $ERR == true ]; then + if [[ $# -gt 0 && $1 =~ [[:upper:]fix] ]]; then + ext="${file##*.}" + cat ./hack/boilerplate_apache2."${ext}".txt "${file}" > "${file}".new + mv "${file}".new "${file}" + echo "$(tput -T xterm setaf 3)FIXING$(tput -T xterm sgr0)" + ERR=false + else + echo "$(tput -T xterm setaf 1)FAIL$(tput -T xterm sgr0)" + ERR=false + FAIL=true + fi + else + echo "$(tput -T xterm setaf 2)OK$(tput -T xterm sgr0)" + fi +done + # If we failed one check, return 1 -[ $FAIL == true ] && exit 1 || exit 0 \ No newline at end of file +[ $FAIL == true ] && exit 1 || exit 0 diff --git a/internal/class/class_utils.go b/internal/class/class_utils.go index dfe420dd0..684fb1692 100644 --- a/internal/class/class_utils.go +++ b/internal/class/class_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -20,39 +23,55 @@ import ( "context" "fmt" "sort" - "strconv" "strings" "text/template" - "time" "github.com/ghodss/yaml" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/constant" ) -// GetCustomClassConfigMapName Returns the name of the ConfigMap containing the custom classes -func GetCustomClassConfigMapName(cdName string, componentName string) string { +// ValidateComponentClass checks if component classDefRef or resource is valid +func ValidateComponentClass(comp *v1alpha1.ClusterComponentSpec, compClasses map[string]map[string]*v1alpha1.ComponentClassInstance) (*v1alpha1.ComponentClassInstance, error) { + classes := compClasses[comp.ComponentDefRef] + var cls *v1alpha1.ComponentClassInstance + switch { + case comp.ClassDefRef != nil && comp.ClassDefRef.Class != "": + if classes == nil { + return nil, fmt.Errorf("can not find classes for component %s", comp.ComponentDefRef) + } + cls = classes[comp.ClassDefRef.Class] + if cls == nil { + return nil, fmt.Errorf("unknown component class %s", comp.ClassDefRef.Class) + } + case classes != nil: + cls = ChooseComponentClasses(classes, comp.Resources.Requests) + if cls == nil { + return nil, fmt.Errorf("can not find matching class for component %s", comp.Name) + } + } + return cls, nil +} + +// GetCustomClassObjectName returns the name of the ComponentClassDefinition object containing the custom classes +func GetCustomClassObjectName(cdName string, componentName string) string { return fmt.Sprintf("kb.classes.custom.%s.%s", cdName, componentName) } -// ChooseComponentClasses Choose the classes to be used for a given component with some constraints -func ChooseComponentClasses(classes map[string]*ComponentClass, filters map[string]resource.Quantity) *ComponentClass { - var candidates []*ComponentClass +// ChooseComponentClasses chooses the classes to be used for a given component with constraints +func ChooseComponentClasses(classes map[string]*v1alpha1.ComponentClassInstance, resources corev1.ResourceList) *v1alpha1.ComponentClassInstance { + var candidates []*v1alpha1.ComponentClassInstance for _, cls := range classes { - cpu, ok := filters[corev1.ResourceCPU.String()] - if ok && !cpu.Equal(cls.CPU) { + if !resources.Cpu().IsZero() && !resources.Cpu().Equal(cls.CPU) { continue } - memory, ok := filters[corev1.ResourceMemory.String()] - if ok && !memory.Equal(cls.Memory) { + if !resources.Memory().IsZero() && !resources.Memory().Equal(cls.Memory) { continue } candidates = append(candidates, cls) @@ -64,21 +83,64 @@ func ChooseComponentClasses(classes map[string]*ComponentClass, filters map[stri return candidates[0] } -func GetClassFamilies(dynamic dynamic.Interface) (map[string]*v1alpha1.ClassFamily, error) { - objs, err := dynamic.Resource(types.ClassFamilyGVR()).Namespace("").List(context.TODO(), metav1.ListOptions{ - //LabelSelector: types.ClassFamilyProviderLabelKey, +func GetClasses(classDefinitionList v1alpha1.ComponentClassDefinitionList) (map[string]map[string]*v1alpha1.ComponentClassInstance, error) { + var ( + compTypeLabel = "apps.kubeblocks.io/component-def-ref" + componentClasses = make(map[string]map[string]*v1alpha1.ComponentClassInstance) + ) + for _, classDefinition := range classDefinitionList.Items { + componentType := classDefinition.GetLabels()[compTypeLabel] + if componentType == "" { + return nil, fmt.Errorf("can not find component type label %s", compTypeLabel) + } + var ( + err error + classes = make(map[string]*v1alpha1.ComponentClassInstance) + ) + if classDefinition.GetGeneration() != 0 && + classDefinition.Status.ObservedGeneration == classDefinition.GetGeneration() { + for idx := range classDefinition.Status.Classes { + cls := classDefinition.Status.Classes[idx] + classes[cls.Name] = &cls + } + } else { + classes, err = ParseComponentClasses(classDefinition) + if err != nil { + return nil, err + } + } + if _, ok := componentClasses[componentType]; !ok { + componentClasses[componentType] = classes + } else { + for k, v := range classes { + if _, exists := componentClasses[componentType][k]; exists { + return nil, fmt.Errorf("duplicated component class %s", k) + } + componentClasses[componentType][k] = v + } + } + } + + return componentClasses, nil +} + +// GetResourceConstraints gets all resource constraints +func GetResourceConstraints(dynamic dynamic.Interface) (map[string]*v1alpha1.ComponentResourceConstraint, error) { + objs, err := dynamic.Resource(types.ComponentResourceConstraintGVR()).List(context.TODO(), metav1.ListOptions{ + //LabelSelector: types.ResourceConstraintProviderLabelKey, }) if err != nil { return nil, err } - var classFamilyList v1alpha1.ClassFamilyList - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(objs.UnstructuredContent(), &classFamilyList); err != nil { + var constraintsList v1alpha1.ComponentResourceConstraintList + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(objs.UnstructuredContent(), &constraintsList); err != nil { return nil, err } - result := make(map[string]*v1alpha1.ClassFamily) - for _, cf := range classFamilyList.Items { - if _, ok := cf.GetLabels()[types.ClassFamilyProviderLabelKey]; !ok { + result := make(map[string]*v1alpha1.ComponentResourceConstraint) + for idx := range constraintsList.Items { + cf := constraintsList.Items[idx] + if _, ok := cf.GetLabels()[types.ResourceConstraintProviderLabelKey]; !ok { continue } result[cf.GetName()] = &cf @@ -86,83 +148,26 @@ func GetClassFamilies(dynamic dynamic.Interface) (map[string]*v1alpha1.ClassFami return result, nil } -// GetClasses Get all classes, including kubeblocks default classes and user custom classes -func GetClasses(client kubernetes.Interface, cdName string) (map[string]map[string]*ComponentClass, error) { +// ListClassesByClusterDefinition gets all classes, including kubeblocks default classes and user custom classes +func ListClassesByClusterDefinition(client dynamic.Interface, cdName string) (map[string]map[string]*v1alpha1.ComponentClassInstance, error) { selector := fmt.Sprintf("%s=%s,%s", constant.ClusterDefLabelKey, cdName, types.ClassProviderLabelKey) - cmList, err := client.CoreV1().ConfigMaps(metav1.NamespaceAll).List(context.TODO(), metav1.ListOptions{ + objs, err := client.Resource(types.ComponentClassDefinitionGVR()).Namespace("").List(context.TODO(), metav1.ListOptions{ LabelSelector: selector, }) if err != nil { return nil, err } - return ParseClasses(cmList) -} - -func ParseClasses(cmList *corev1.ConfigMapList) (map[string]map[string]*ComponentClass, error) { - var ( - componentClasses = make(map[string]map[string]*ComponentClass) - ) - for _, cm := range cmList.Items { - if _, ok := cm.GetLabels()[types.ClassProviderLabelKey]; !ok { - continue - } - level := cm.GetLabels()[types.ClassLevelLabelKey] - switch level { - case "component": - componentType := cm.GetLabels()[constant.KBAppComponentDefRefLabelKey] - if componentType == "" { - return nil, fmt.Errorf("failed to find component type") - } - classes, err := ParseComponentClasses(cm.Data) - if err != nil { - return nil, err - } - if _, ok := componentClasses[componentType]; !ok { - componentClasses[componentType] = classes - } else { - for k, v := range classes { - if _, exists := componentClasses[componentType][k]; exists { - return nil, fmt.Errorf("duplicate component class %s", k) - } - componentClasses[componentType][k] = v - } - } - case "cluster": - // TODO - default: - return nil, fmt.Errorf("invalid class level: %s", level) - } + var classDefinitionList v1alpha1.ComponentClassDefinitionList + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(objs.UnstructuredContent(), &classDefinitionList); err != nil { + return nil, err } - - return componentClasses, nil + return GetClasses(classDefinitionList) } -type classVersion int64 - -// ParseComponentClasses parse configmap.data to component classes -func ParseComponentClasses(data map[string]string) (map[string]*ComponentClass, error) { - versions := make(map[classVersion][]*ComponentClassFamilyDef) - - for k, v := range data { - // ConfigMap data key follows the format: families-[version] - // version is the timestamp in unix microseconds which class is created - parts := strings.SplitAfterN(k, "-", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("invalid key: %s", k) - } - version, err := strconv.ParseInt(parts[1], 10, 64) - if err != nil { - return nil, fmt.Errorf("invalid key: %s", k) - } - var families []*ComponentClassFamilyDef - if err := yaml.Unmarshal([]byte(v), &families); err != nil { - return nil, err - } - versions[classVersion(version)] = families - } - - genClassDef := func(nameTpl string, bodyTpl string, vars []string, args []string) (ComponentClassDef, error) { - var def ComponentClassDef +// ParseComponentClasses parses ComponentClassDefinition to component classes +func ParseComponentClasses(classDefinition v1alpha1.ComponentClassDefinition) (map[string]*v1alpha1.ComponentClassInstance, error) { + genClass := func(nameTpl string, bodyTpl string, vars []string, args []string) (v1alpha1.ComponentClass, error) { + var result v1alpha1.ComponentClass values := make(map[string]interface{}) for index, key := range vars { values[key] = args[index] @@ -170,69 +175,57 @@ func ParseComponentClasses(data map[string]string) (map[string]*ComponentClass, classStr, err := renderTemplate(bodyTpl, values) if err != nil { - return def, err + return result, err } - if err = yaml.Unmarshal([]byte(classStr), &def); err != nil { - return def, err + if err = yaml.Unmarshal([]byte(classStr), &result); err != nil { + return result, err } - def.Name, err = renderTemplate(nameTpl, values) + name, err := renderTemplate(nameTpl, values) if err != nil { - return def, err + return result, err } - return def, nil + result.Name = name + return result, nil } - parser := func(family *ComponentClassFamilyDef, series ComponentClassSeriesDef, class ComponentClassDef) (*ComponentClass, error) { - var ( - err error - def = class - ) - + parser := func(group v1alpha1.ComponentClassGroup, series v1alpha1.ComponentClassSeries, class v1alpha1.ComponentClass) (*v1alpha1.ComponentClassInstance, error) { if len(class.Args) > 0 { - def, err = genClassDef(series.Name, family.Template, family.Vars, class.Args) + cls, err := genClass(series.NamingTemplate, group.Template, group.Vars, class.Args) if err != nil { return nil, err } - if class.Name != "" { - def.Name = class.Name + if class.Name == "" && cls.Name != "" { + class.Name = cls.Name } + class.CPU = cls.CPU + class.Memory = cls.Memory } - - result := &ComponentClass{ - Name: def.Name, - Family: family.Family, - CPU: resource.MustParse(def.CPU), - Memory: resource.MustParse(def.Memory), - } - - for _, disk := range def.Storage { - result.Storage = append(result.Storage, &Disk{ - Name: disk.Name, - Class: disk.Class, - Size: resource.MustParse(disk.Size), - }) + result := &v1alpha1.ComponentClassInstance{ + ComponentClass: v1alpha1.ComponentClass{ + Name: class.Name, + CPU: class.CPU, + Memory: class.Memory, + }, + ResourceConstraintRef: group.ResourceConstraintRef, } - return result, nil } - result := make(map[string]*ComponentClass) - for _, families := range versions { - for _, family := range families { - for _, series := range family.Series { - for _, class := range series.Classes { - out, err := parser(family, series, class) - if err != nil { - return nil, err - } - if _, exists := result[out.Name]; exists { - return nil, fmt.Errorf("duplicate component class name: %s", out.Name) - } - result[out.Name] = out + result := make(map[string]*v1alpha1.ComponentClassInstance) + for _, group := range classDefinition.Spec.Groups { + for _, series := range group.Series { + for _, class := range series.Classes { + out, err := parser(group, series, class) + if err != nil { + return nil, err } + if _, exists := result[out.Name]; exists { + return nil, fmt.Errorf("duplicated component class name: %s", out.Name) + } + result[out.Name] = out } } } @@ -250,8 +243,3 @@ func renderTemplate(tpl string, values map[string]interface{}) (string, error) { } return buf.String(), nil } - -// BuildClassDefinitionVersion generate the key in the configmap data field -func BuildClassDefinitionVersion() string { - return fmt.Sprintf("version-%s", time.Now().Format("20060102150405")) -} diff --git a/internal/class/class_utils_test.go b/internal/class/class_utils_test.go index f68ec1fb0..43e06f8db 100644 --- a/internal/class/class_utils_test.go +++ b/internal/class/class_utils_test.go @@ -1,28 +1,37 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class import ( "fmt" + "reflect" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) var _ = Describe("utils", func() { @@ -30,18 +39,22 @@ var _ = Describe("utils", func() { cpuMin = 1 cpuMax = 64 scales = []int{4, 8, 16} - classes map[string]*ComponentClass + classes map[string]*v1alpha1.ComponentClassInstance ) - genComponentClasses := func(cpuMin int, cpuMax int, scales []int) map[string]*ComponentClass { - results := make(map[string]*ComponentClass) + genComponentClasses := func(cpuMin int, cpuMax int, scales []int) map[string]*v1alpha1.ComponentClassInstance { + results := make(map[string]*v1alpha1.ComponentClassInstance) for cpu := cpuMin; cpu <= cpuMax; cpu++ { for _, scale := range scales { - name := fmt.Sprintf("cpu-%d-scale-%d", cpu, scale) - results[name] = &ComponentClass{ - Name: name, - CPU: resource.MustParse(fmt.Sprintf("%d", cpu)), - Memory: resource.MustParse(fmt.Sprintf("%dGi", cpu*scale)), + var ( + clsName = fmt.Sprintf("cpu-%d-scale-%d", cpu, scale) + ) + results[clsName] = &v1alpha1.ComponentClassInstance{ + ComponentClass: v1alpha1.ComponentClass{ + Name: clsName, + CPU: resource.MustParse(fmt.Sprintf("%d", cpu)), + Memory: resource.MustParse(fmt.Sprintf("%dGi", cpu*scale)), + }, } } } @@ -56,44 +69,193 @@ var _ = Describe("utils", func() { // Add any teardown steps that needs to be executed after each test }) - buildFilters := func(cpu string, memory string) map[string]resource.Quantity { - result := make(map[string]resource.Quantity) + buildResourceList := func(cpu string, memory string) corev1.ResourceList { + result := make(corev1.ResourceList) if cpu != "" { - result["cpu"] = resource.MustParse(cpu) + result[corev1.ResourceCPU] = resource.MustParse(cpu) } if memory != "" { - result["memory"] = resource.MustParse(memory) + result[corev1.ResourceMemory] = resource.MustParse(memory) } return result } + Context("validate component class", func() { + var ( + specClassName = testapps.Class1c1gName + comp1Name = "component-have-class-definition" + comp2Name = "component-does-not-have-class-definition" + compClasses map[string]map[string]*v1alpha1.ComponentClassInstance + ) + + BeforeEach(func() { + var err error + classDef := testapps.NewComponentClassDefinitionFactory("custom", "apecloud-mysql", comp1Name). + AddClasses(testapps.DefaultResourceConstraintName, []string{specClassName}). + GetObject() + compClasses, err = GetClasses(v1alpha1.ComponentClassDefinitionList{ + Items: []v1alpha1.ComponentClassDefinition{ + *classDef, + }, + }) + Expect(err).ShouldNot(HaveOccurred()) + }) + + It("should succeed if component has class definition and with valid classDefRef", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp1Name, + ClassDefRef: &v1alpha1.ClassDefRef{Class: specClassName}, + } + cls, err := ValidateComponentClass(comp, compClasses) + Expect(err).ShouldNot(HaveOccurred()) + Expect(reflect.DeepEqual(cls.ComponentClass, testapps.Class1c1g)).Should(BeTrue()) + }) + + It("should fail if component has class definition and with invalid classDefRef", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp1Name, + ClassDefRef: &v1alpha1.ClassDefRef{Class: "class-not-exists"}, + } + _, err := ValidateComponentClass(comp, compClasses) + Expect(err).Should(HaveOccurred()) + }) + + It("should succeed if component has class definition and with valid resource", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp1Name, + Resources: corev1.ResourceRequirements{ + Requests: buildResourceList("1", "1Gi"), + }, + } + cls, err := ValidateComponentClass(comp, compClasses) + Expect(err).ShouldNot(HaveOccurred()) + Expect(reflect.DeepEqual(cls.ComponentClass, testapps.Class1c1g)).Should(BeTrue()) + }) + + It("should fail if component has class definition and with invalid resource", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp1Name, + Resources: corev1.ResourceRequirements{ + Requests: buildResourceList("100", "200Gi"), + }, + } + _, err := ValidateComponentClass(comp, compClasses) + Expect(err).Should(HaveOccurred()) + }) + + It("should succeed if component hasn't class definition and without classDefRef", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp2Name, + } + cls, err := ValidateComponentClass(comp, compClasses) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cls).Should(BeNil()) + }) + + It("should fail if component hasn't class definition and with classDefRef", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp2Name, + ClassDefRef: &v1alpha1.ClassDefRef{Class: specClassName}, + } + _, err := ValidateComponentClass(comp, compClasses) + Expect(err).Should(HaveOccurred()) + }) + + It("should succeed if component hasn't class definition and without classDefRef", func() { + comp := &v1alpha1.ClusterComponentSpec{ + ComponentDefRef: comp2Name, + Resources: corev1.ResourceRequirements{ + Requests: buildResourceList("100", "200Gi"), + }, + } + cls, err := ValidateComponentClass(comp, compClasses) + Expect(err).ShouldNot(HaveOccurred()) + Expect(cls).Should(BeNil()) + }) + }) + Context("sort component classes", func() { + It("should match minimal class if cpu and memory are empty", func() { + class := ChooseComponentClasses(classes, buildResourceList("", "")) + Expect(class).ShouldNot(BeNil()) + Expect(class.CPU.String()).Should(Equal("1")) + Expect(class.Memory.String()).Should(Equal("4Gi")) + }) + It("should match one class by cpu and memory", func() { - class := ChooseComponentClasses(classes, buildFilters("1", "4Gi")) + class := ChooseComponentClasses(classes, buildResourceList("1", "4Gi")) + Expect(class).ShouldNot(BeNil()) Expect(class.CPU.String()).Should(Equal("1")) Expect(class.Memory.String()).Should(Equal("4Gi")) }) It("match multiple classes by cpu", func() { - class := ChooseComponentClasses(classes, buildFilters("1", "")) + class := ChooseComponentClasses(classes, buildResourceList("1", "")) + Expect(class).ShouldNot(BeNil()) Expect(class.CPU.String()).Should(Equal("1")) Expect(class.Memory.String()).Should(Equal("4Gi")) }) It("match multiple classes by memory", func() { - class := ChooseComponentClasses(classes, buildFilters("", "16Gi")) + class := ChooseComponentClasses(classes, buildResourceList("", "16Gi")) + Expect(class).ShouldNot(BeNil()) Expect(class.CPU.String()).Should(Equal("1")) Expect(class.Memory.String()).Should(Equal("16Gi")) }) - It("not match any classes by cpu", func() { - class := ChooseComponentClasses(classes, buildFilters(fmt.Sprintf("%d", cpuMax+1), "")) + It("not match any class by cpu", func() { + class := ChooseComponentClasses(classes, buildResourceList(fmt.Sprintf("%d", cpuMax+1), "")) Expect(class).Should(BeNil()) }) - It("not match any classes by memory", func() { - class := ChooseComponentClasses(classes, buildFilters("", "1Pi")) + It("not match any class by memory", func() { + class := ChooseComponentClasses(classes, buildResourceList("", "1Pi")) Expect(class).Should(BeNil()) }) }) + + Context("get classes", func() { + It("should succeed", func() { + var ( + err error + specClassName = testapps.Class1c1gName + statusClassName = "general-100c100g" + compClasses map[string]map[string]*v1alpha1.ComponentClassInstance + compType = "mysql" + ) + + classDef := testapps.NewComponentClassDefinitionFactory("custom", "apecloud-mysql", compType). + AddClasses(testapps.DefaultResourceConstraintName, []string{specClassName}). + GetObject() + + By("class definition status is out of date") + classDef.SetGeneration(1) + classDef.Status.ObservedGeneration = 0 + classDef.Status.Classes = []v1alpha1.ComponentClassInstance{ + { + ComponentClass: v1alpha1.ComponentClass{ + Name: statusClassName, + CPU: resource.MustParse("100"), + Memory: resource.MustParse("100Gi"), + }, + ResourceConstraintRef: "", + }, + } + compClasses, err = GetClasses(v1alpha1.ComponentClassDefinitionList{ + Items: []v1alpha1.ComponentClassDefinition{*classDef}, + }) + Expect(err).ShouldNot(HaveOccurred()) + Expect(compClasses[compType][specClassName]).ShouldNot(BeNil()) + Expect(compClasses[compType][statusClassName]).Should(BeNil()) + + By("class definition status is in sync with the class definition spec") + classDef.Status.ObservedGeneration = 1 + compClasses, err = GetClasses(v1alpha1.ComponentClassDefinitionList{ + Items: []v1alpha1.ComponentClassDefinition{*classDef}, + }) + Expect(err).ShouldNot(HaveOccurred()) + Expect(compClasses[compType][specClassName]).Should(BeNil()) + Expect(compClasses[compType][statusClassName]).ShouldNot(BeNil()) + }) + }) }) diff --git a/internal/class/suite_test.go b/internal/class/suite_test.go new file mode 100644 index 000000000..e802330f4 --- /dev/null +++ b/internal/class/suite_test.go @@ -0,0 +1,33 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package class + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestClass(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Class Test Suite") +} diff --git a/internal/class/types.go b/internal/class/types.go index dc43a38e2..2dca907ed 100644 --- a/internal/class/types.go +++ b/internal/class/types.go @@ -1,25 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class import ( - "fmt" "sort" - "strings" "gopkg.in/inf.v0" "k8s.io/apimachinery/pkg/api/resource" @@ -27,7 +28,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) -func GetMinCPUAndMemory(model appsv1alpha1.ClassFamilyModel) (*resource.Quantity, *resource.Quantity) { +func GetMinCPUAndMemory(model appsv1alpha1.ResourceConstraint) (*resource.Quantity, *resource.Quantity) { var ( minCPU resource.Quantity minMemory resource.Quantity @@ -50,20 +51,22 @@ func GetMinCPUAndMemory(model appsv1alpha1.ClassFamilyModel) (*resource.Quantity return &minCPU, &minMemory } -type ClassModelWithFamilyName struct { - Family string - Model appsv1alpha1.ClassFamilyModel +type ConstraintWithName struct { + Name string + Constraint appsv1alpha1.ResourceConstraint } -type ByModelList []ClassModelWithFamilyName +var _ sort.Interface = ByConstraintList{} + +type ByConstraintList []ConstraintWithName -func (m ByModelList) Len() int { +func (m ByConstraintList) Len() int { return len(m) } -func (m ByModelList) Less(i, j int) bool { - cpu1, mem1 := GetMinCPUAndMemory(m[i].Model) - cpu2, mem2 := GetMinCPUAndMemory(m[j].Model) +func (m ByConstraintList) Less(i, j int) bool { + cpu1, mem1 := GetMinCPUAndMemory(m[i].Constraint) + cpu2, mem2 := GetMinCPUAndMemory(m[j].Constraint) switch cpu1.Cmp(*cpu2) { case 1: return false @@ -79,21 +82,13 @@ func (m ByModelList) Less(i, j int) bool { return false } -func (m ByModelList) Swap(i, j int) { +func (m ByConstraintList) Swap(i, j int) { m[i], m[j] = m[j], m[i] } -type ComponentClass struct { - Name string `json:"name,omitempty"` - CPU resource.Quantity `json:"cpu,omitempty"` - Memory resource.Quantity `json:"memory,omitempty"` - Storage []*Disk `json:"storage,omitempty"` - Family string `json:"-"` -} - var _ sort.Interface = ByClassCPUAndMemory{} -type ByClassCPUAndMemory []*ComponentClass +type ByClassCPUAndMemory []*appsv1alpha1.ComponentClassInstance func (b ByClassCPUAndMemory) Len() int { return len(b) @@ -114,55 +109,3 @@ func (b ByClassCPUAndMemory) Less(i, j int) bool { func (b ByClassCPUAndMemory) Swap(i, j int) { b[i], b[j] = b[j], b[i] } - -type Filters map[string]resource.Quantity - -func (f Filters) String() string { - var result []string - for k, v := range f { - result = append(result, fmt.Sprintf("%s=%v", k, v.Value())) - } - return strings.Join(result, ",") -} - -type Disk struct { - Name string `json:"name,omitempty"` - Size resource.Quantity `json:"size,omitempty"` - Class string `json:"class,omitempty"` -} - -func (d Disk) String() string { - return fmt.Sprintf("%s=%s", d.Name, d.Size.String()) -} - -type ProviderComponentClassDef struct { - Provider string `json:"provider,omitempty"` - Args []string `json:"args,omitempty"` -} - -type DiskDef struct { - Name string `json:"name,omitempty"` - Size string `json:"size,omitempty"` - Class string `json:"class,omitempty"` -} - -type ComponentClassDef struct { - Name string `json:"name,omitempty"` - CPU string `json:"cpu,omitempty"` - Memory string `json:"memory,omitempty"` - Storage []DiskDef `json:"storage,omitempty"` - Args []string `json:"args,omitempty"` - Variants []ProviderComponentClassDef `json:"variants,omitempty"` -} - -type ComponentClassSeriesDef struct { - Name string `json:"name,omitempty"` - Classes []ComponentClassDef `json:"classes,omitempty"` -} - -type ComponentClassFamilyDef struct { - Family string `json:"family"` - Template string `json:"template,omitempty"` - Vars []string `json:"vars,omitempty"` - Series []ComponentClassSeriesDef `json:"series,omitempty"` -} diff --git a/internal/class/types_test.go b/internal/class/types_test.go index ad65a631b..b75c03be1 100644 --- a/internal/class/types_test.go +++ b/internal/class/types_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -27,15 +30,14 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) -const classFamilyBytes = ` +const resourceConstraintBytes = ` # API scope: cluster -# ClusterClassFamily apiVersion: "apps.kubeblocks.io/v1alpha1" -kind: "ClassFamily" +kind: "ComponentResourceConstraint" metadata: - name: kb-class-family-general + name: kb-resource-constraint-general spec: - models: + constraints: - cpu: min: 0.5 max: 128 @@ -55,11 +57,16 @@ spec: maxPerCPU: 8Gi ` -func TestClassFamily_ByClassCPUAndMemory(t *testing.T) { - buildClass := func(cpu string, memory string) *ComponentClass { - return &ComponentClass{CPU: resource.MustParse(cpu), Memory: resource.MustParse(memory)} +func TestResourceConstraint_ByClassCPUAndMemory(t *testing.T) { + buildClass := func(cpu string, memory string) *appsv1alpha1.ComponentClassInstance { + return &appsv1alpha1.ComponentClassInstance{ + ComponentClass: appsv1alpha1.ComponentClass{ + CPU: resource.MustParse(cpu), + Memory: resource.MustParse(memory), + }, + } } - classes := []*ComponentClass{ + classes := []*appsv1alpha1.ComponentClassInstance{ buildClass("1", "2Gi"), buildClass("1", "1Gi"), buildClass("2", "0.5Gi"), @@ -68,24 +75,24 @@ func TestClassFamily_ByClassCPUAndMemory(t *testing.T) { } sort.Sort(ByClassCPUAndMemory(classes)) candidate := classes[0] - if candidate.CPU != resource.MustParse("0.5") || candidate.Memory != resource.MustParse("10Gi") { + if !candidate.CPU.Equal(resource.MustParse("0.5")) || !candidate.Memory.Equal(resource.MustParse("10Gi")) { t.Errorf("case failed") } } -func TestClassFamily_ModelList(t *testing.T) { - var cf appsv1alpha1.ClassFamily - err := yaml.Unmarshal([]byte(classFamilyBytes), &cf) +func TestResourceConstraint_ConstraintList(t *testing.T) { + var cf appsv1alpha1.ComponentResourceConstraint + err := yaml.Unmarshal([]byte(resourceConstraintBytes), &cf) if err != nil { - panic("Failed to unmarshal class family: %v" + err.Error()) + panic("Failed to unmarshal resource constraint: %v" + err.Error()) } - var models []ClassModelWithFamilyName - for _, model := range cf.Spec.Models { - models = append(models, ClassModelWithFamilyName{Family: cf.Name, Model: model}) + var constraints []ConstraintWithName + for _, constraint := range cf.Spec.Constraints { + constraints = append(constraints, ConstraintWithName{Name: cf.Name, Constraint: constraint}) } resource.MustParse("200Mi") - sort.Sort(ByModelList(models)) - cpu, memory := GetMinCPUAndMemory(models[0].Model) + sort.Sort(ByConstraintList(constraints)) + cpu, memory := GetMinCPUAndMemory(constraints[0].Constraint) assert.Equal(t, cpu.Cmp(resource.MustParse("0.1")) == 0, true) assert.Equal(t, memory.Cmp(resource.MustParse("20Mi")) == 0, true) } diff --git a/internal/cli/cloudprovider/interface.go b/internal/cli/cloudprovider/interface.go index 8ba91bdbc..227ea33e6 100644 --- a/internal/cli/cloudprovider/interface.go +++ b/internal/cli/cloudprovider/interface.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -24,7 +27,7 @@ import ( ) type Interface interface { - // Name return the cloud provider name + // Name returns the cloud provider name Name() string // CreateK8sCluster creates a kubernetes cluster @@ -33,16 +36,16 @@ type Interface interface { // DeleteK8sCluster deletes the created kubernetes cluster DeleteK8sCluster(clusterInfo *K8sClusterInfo) error - // GetClusterInfo get cluster info + // GetClusterInfo gets cluster info GetClusterInfo() (*K8sClusterInfo, error) } func New(provider, tfRootPath string, stdout, stderr io.Writer) (Interface, error) { switch provider { case AWS, TencentCloud, AliCloud, GCP: - return NewCloudProvider(provider, tfRootPath, stdout, stderr) + return newCloudProvider(provider, tfRootPath, stdout, stderr) case Local: - return NewLocalCloudProvider(stdout, stderr), nil + return newLocalCloudProvider(stdout, stderr), nil default: return nil, errors.New(fmt.Sprintf("Unknown cloud provider %s", provider)) } diff --git a/internal/cli/cloudprovider/k3d.go b/internal/cli/cloudprovider/k3d.go index 3561782fa..3f993e578 100644 --- a/internal/cli/cloudprovider/k3d.go +++ b/internal/cli/cloudprovider/k3d.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -22,6 +25,7 @@ import ( "fmt" "io" "net" + "os" "strconv" "strings" @@ -32,6 +36,7 @@ import ( l "github.com/k3d-io/k3d/v5/pkg/logger" "github.com/k3d-io/k3d/v5/pkg/runtimes" k3d "github.com/k3d-io/k3d/v5/pkg/types" + "github.com/k3d-io/k3d/v5/pkg/types/fixes" "github.com/pkg/errors" "github.com/sirupsen/logrus" "k8s.io/client-go/tools/clientcmd" @@ -50,17 +55,17 @@ var ( // K3sImage is k3s image repo K3sImage = "rancher/k3s:" + version.K3sImageTag - // K3dToolsImage is k3d tools image repo - K3dToolsImage = "docker.io/apecloud/k3d-tools:" + version.K3dVersion - // K3dProxyImage is k3d proxy image repo K3dProxyImage = "docker.io/apecloud/k3d-proxy:" + version.K3dVersion + + // K3dFixEnv + KBEnvFix fixes.K3DFixEnv = "KB_FIX_MOUNTS" ) //go:embed assets/k3d-entrypoint-mount.sh var k3dMountEntrypoint []byte -// localCloudProvider will handle the k3d playground cluster creation and management +// localCloudProvider handles the k3d playground cluster creation and management type localCloudProvider struct { cfg config.ClusterConfig stdout io.Writer @@ -72,12 +77,12 @@ var _ Interface = &localCloudProvider{} func init() { if !klog.V(1).Enabled() { - // set k3d log level to warning to avoid so much info log + // set k3d log level to 'warning' to avoid too much info logs l.Log().SetLevel(logrus.WarnLevel) } } -func NewLocalCloudProvider(stdout, stderr io.Writer) *localCloudProvider { +func newLocalCloudProvider(stdout, stderr io.Writer) Interface { return &localCloudProvider{ stdout: stdout, stderr: stderr, @@ -88,7 +93,7 @@ func (p *localCloudProvider) Name() string { return Local } -// CreateK8sCluster create a local kubernetes cluster using k3d +// CreateK8sCluster creates a local kubernetes cluster using k3d func (p *localCloudProvider) CreateK8sCluster(clusterInfo *K8sClusterInfo) error { var err error @@ -103,7 +108,7 @@ func (p *localCloudProvider) CreateK8sCluster(clusterInfo *K8sClusterInfo) error return nil } -// DeleteK8sCluster remove the k3d cluster +// DeleteK8sCluster removes the k3d cluster func (p *localCloudProvider) DeleteK8sCluster(clusterInfo *K8sClusterInfo) error { var err error if clusterInfo == nil { @@ -132,7 +137,7 @@ func (p *localCloudProvider) DeleteK8sCluster(clusterInfo *K8sClusterInfo) error } } - // extra handling to clean up tools nodes + // extra handling to clean up tools nodes defer func() { if nl, err := k3dClient.NodeList(ctx, runtimes.SelectedRuntime); err == nil { toolNode := fmt.Sprintf("k3d-%s-tools", clusterName) @@ -185,7 +190,7 @@ func (p *localCloudProvider) GetKubeConfig() (string, error) { return "", errors.Wrap(err, "unrecognized k3d kubeconfig format") } - // Replace host config with loop back address + // replace host config with loop back address return strings.ReplaceAll(cfgStr, hostToReplace, "127.0.0.1"), nil } @@ -353,6 +358,12 @@ func buildKubeconfigOptions() config.SimpleConfigOptionsKubeconfig { } func setUpK3d(ctx context.Context, cluster *config.ClusterConfig) error { + // add fix Envs + if err := os.Setenv(string(KBEnvFix), "1"); err != nil { + return err + } + fixes.FixEnvs = append(fixes.FixEnvs, KBEnvFix) + l, err := k3dClient.ClusterList(ctx, runtimes.SelectedRuntime) if err != nil { return err @@ -365,7 +376,7 @@ func setUpK3d(ctx context.Context, cluster *config.ClusterConfig) error { for _, c := range l { if c.Name == cluster.Name { if c, err := k3dClient.ClusterGet(ctx, runtimes.SelectedRuntime, c); err == nil { - fmt.Printf(" Detected an existing cluster: %s", c.Name) + klog.V(1).Info("Detected an existing cluster: %s\n", c.Name) return nil } break diff --git a/internal/cli/cloudprovider/k3d_test.go b/internal/cli/cloudprovider/k3d_test.go index f3f16be67..1c07cdfbe 100644 --- a/internal/cli/cloudprovider/k3d_test.go +++ b/internal/cli/cloudprovider/k3d_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -26,8 +29,8 @@ import ( var _ = Describe("playground", func() { var ( - provider = NewLocalCloudProvider(os.Stdout, os.Stderr) - clusterName = "k3d-tb-est" + provider = newLocalCloudProvider(os.Stdout, os.Stderr) + clusterName = "k3d-kb-test" ) It("k3d util function", func() { diff --git a/internal/cli/cloudprovider/provider.go b/internal/cli/cloudprovider/provider.go index abc89b6d1..cb6c9f090 100644 --- a/internal/cli/cloudprovider/provider.go +++ b/internal/cli/cloudprovider/provider.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -33,7 +36,7 @@ type cloudProvider struct { var _ Interface = &cloudProvider{} -func NewCloudProvider(provider, rootPath string, stdout, stderr io.Writer) (Interface, error) { +func newCloudProvider(provider, rootPath string, stdout, stderr io.Writer) (Interface, error) { k8sSvc := K8sService(provider) if k8sSvc == "" { return nil, fmt.Errorf("unknown cloud provider %s", provider) @@ -56,7 +59,7 @@ func (p *cloudProvider) Name() string { return p.name } -// CreateK8sCluster create a kubernetes cluster +// CreateK8sCluster creates a kubernetes cluster func (p *cloudProvider) CreateK8sCluster(clusterInfo *K8sClusterInfo) error { // init terraform fmt.Fprintf(p.stdout, "Check and install terraform... \n") diff --git a/internal/cli/cloudprovider/provider_test.go b/internal/cli/cloudprovider/provider_test.go index 92cf09059..4fe1e4ec3 100644 --- a/internal/cli/cloudprovider/provider_test.go +++ b/internal/cli/cloudprovider/provider_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider diff --git a/internal/cli/cloudprovider/suite_test.go b/internal/cli/cloudprovider/suite_test.go index 64b5e67e4..16eac6749 100644 --- a/internal/cli/cloudprovider/suite_test.go +++ b/internal/cli/cloudprovider/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -31,6 +34,5 @@ func TestPlayground(t *testing.T) { var _ = BeforeSuite(func() { // set fake image info K3sImage = "fake-k3s-image" - K3dToolsImage = "fake-k3s-tools-image" K3dProxyImage = "fake-k3d-proxy-image" }) diff --git a/internal/cli/cloudprovider/terraform.go b/internal/cli/cloudprovider/terraform.go index d52919e0f..933ecacbb 100644 --- a/internal/cli/cloudprovider/terraform.go +++ b/internal/cli/cloudprovider/terraform.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider diff --git a/internal/cli/cloudprovider/terraform_test.go b/internal/cli/cloudprovider/terraform_test.go index 07cd4fda8..4b60b4597 100644 --- a/internal/cli/cloudprovider/terraform_test.go +++ b/internal/cli/cloudprovider/terraform_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider diff --git a/internal/cli/cloudprovider/types.go b/internal/cli/cloudprovider/types.go index d42fb8bbc..b2659c78b 100644 --- a/internal/cli/cloudprovider/types.go +++ b/internal/cli/cloudprovider/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cloudprovider @@ -74,7 +77,7 @@ type K8sClusterInfo struct { KubeConfig string `json:"kube_config,omitempty"` } -// IsValid check if kubernetes cluster info is valid +// IsValid checks if kubernetes cluster info is valid func (c *K8sClusterInfo) IsValid() bool { if c.ClusterName == "" || c.CloudProvider == "" || (c.CloudProvider != Local && c.Region == "") { return false diff --git a/internal/cli/cluster/cluster.go b/internal/cli/cluster/cluster.go index 1128ac0eb..b6297427c 100644 --- a/internal/cli/cluster/cluster.go +++ b/internal/cli/cluster/cluster.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -22,6 +25,7 @@ import ( "strings" corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -37,6 +41,9 @@ import ( "github.com/apecloud/kubeblocks/internal/constant" ) +// ConditionsError cluster displays this status on list cmd when the status of ApplyResources or ProvisioningStarted condition is "False". +const ConditionsError = "ConditionsError" + type GetOptions struct { WithClusterDef bool WithClusterVersion bool @@ -46,6 +53,7 @@ type GetOptions struct { WithSecret bool WithPod bool WithEvent bool + WithDataProtection bool } type ObjectsGetter struct { @@ -63,9 +71,28 @@ func NewClusterObjects() *ClusterObjects { } } +func listResources[T any](dynamic dynamic.Interface, gvr schema.GroupVersionResource, ns string, opts metav1.ListOptions, items *[]T) error { + if *items == nil { + *items = []T{} + } + obj, err := dynamic.Resource(gvr).Namespace(ns).List(context.TODO(), opts) + if err != nil { + return err + } + for _, i := range obj.Items { + var object T + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(i.Object, &object); err != nil { + return err + } + *items = append(*items, object) + } + return nil +} + // Get all kubernetes objects belonging to the database cluster func (o *ObjectsGetter) Get() (*ClusterObjects, error) { var err error + objs := NewClusterObjects() ctx := context.TODO() corev1 := o.Client.CoreV1() @@ -90,12 +117,22 @@ func (o *ObjectsGetter) Get() (*ClusterObjects, error) { return nil, err } - // wrap the cluster phase if the latest ops request is processing - latestOpsProcessedCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) - if latestOpsProcessedCondition != nil && latestOpsProcessedCondition.Status == metav1.ConditionFalse { - objs.Cluster.Status.Phase = appsv1alpha1.ClusterPhase(latestOpsProcessedCondition.Reason) + if objs.Cluster.Status.Phase == appsv1alpha1.SpecReconcilingClusterPhase { + // wrap the cluster phase if the latest ops request is processing + latestOpsProcessedCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) + if latestOpsProcessedCondition != nil && latestOpsProcessedCondition.Status == metav1.ConditionFalse { + objs.Cluster.Status.Phase = appsv1alpha1.ClusterPhase(latestOpsProcessedCondition.Reason) + } + } + provisionCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) + if provisionCondition != nil && provisionCondition.Status == metav1.ConditionFalse { + objs.Cluster.Status.Phase = ConditionsError } + applyResourcesCondition := meta.FindStatusCondition(objs.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeApplyResources) + if applyResourcesCondition != nil && applyResourcesCondition.Status == metav1.ConditionFalse { + objs.Cluster.Status.Phase = ConditionsError + } // get cluster definition if o.WithClusterDef { cd := &appsv1alpha1.ClusterDefinition{} @@ -162,21 +199,24 @@ func (o *ObjectsGetter) Get() (*ClusterObjects, error) { } node, err := corev1.Nodes().Get(ctx, nodeName, metav1.GetOptions{}) - if err != nil { + if err != nil && !apierrors.IsNotFound(err) { return nil, err } - objs.Nodes = append(objs.Nodes, node) + + if node != nil { + objs.Nodes = append(objs.Nodes, node) + } } } // get events if o.WithEvent { - // get all events about cluster + // get all events of cluster if objs.Events, err = corev1.Events(o.Namespace).Search(scheme.Scheme, objs.Cluster); err != nil { return nil, err } - // get all events about pods + // get all events of pods for _, pod := range objs.Pods.Items { events, err := corev1.Events(o.Namespace).Search(scheme.Scheme, &pod) if err != nil { @@ -189,6 +229,19 @@ func (o *ObjectsGetter) Get() (*ClusterObjects, error) { } } } + + if o.WithDataProtection { + dpListOpts := metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", + constant.AppInstanceLabelKey, o.Name), + } + if err = listResources(o.Dynamic, types.BackupPolicyGVR(), o.Namespace, dpListOpts, &objs.BackupPolicies); err != nil { + return nil, err + } + if err = listResources(o.Dynamic, types.BackupGVR(), o.Namespace, dpListOpts, &objs.Backups); err != nil { + return nil, err + } + } return objs, nil } @@ -233,7 +286,7 @@ func (o *ClusterObjects) GetComponentInfo() []*ComponentInfo { } } - // current component has no pod corresponding to it + // current component has no derived pods if len(pods) == 0 { continue } @@ -316,7 +369,11 @@ func (o *ClusterObjects) getStorageInfo(component *appsv1alpha1.ClusterComponent if labels[constant.VolumeClaimTemplateNameLabelKey] != vcTpl.Name { continue } - return *pvc.Spec.StorageClassName + if pvc.Spec.StorageClassName != nil { + return *pvc.Spec.StorageClassName + } else { + return types.None + } } return types.None @@ -359,6 +416,11 @@ func getResourceInfo(reqs, limits corev1.ResourceList) (string, string) { res := types.None limit, req := limits[name], reqs[name] + // if request is empty and limit is not, set limit to request + if util.ResourceIsEmpty(&req) && !util.ResourceIsEmpty(&limit) { + req = limit + } + // if both limit and request are empty, only output none if !util.ResourceIsEmpty(&limit) || !util.ResourceIsEmpty(&req) { res = fmt.Sprintf("%s / %s", req.String(), limit.String()) diff --git a/internal/cli/cluster/cluster_test.go b/internal/cli/cluster/cluster_test.go index 1b87ed59f..43bc74fa4 100644 --- a/internal/cli/cluster/cluster_test.go +++ b/internal/cli/cluster/cluster_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -20,52 +23,71 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/client-go/kubernetes" + "github.com/apecloud/kubeblocks/internal/cli/testing" ) var _ = Describe("cluster util", func() { - client := testing.FakeClientSet( + baseObjs := []runtime.Object{ testing.FakePods(3, testing.Namespace, testing.ClusterName), - testing.FakeNode(), testing.FakeSecrets(testing.Namespace, testing.ClusterName), testing.FakeServices(), - testing.FakePVCs()) + testing.FakePVCs(), + } dynamic := testing.FakeDynamicClient( testing.FakeCluster(testing.ClusterName, testing.Namespace), testing.FakeClusterDef(), testing.FakeClusterVersion()) + getOptions := GetOptions{ + WithClusterDef: true, + WithClusterVersion: true, + WithConfigMap: true, + WithService: true, + WithSecret: true, + WithPVC: true, + WithPod: true, + } + It("get cluster objects", func() { - clusterName := testing.ClusterName - getter := ObjectsGetter{ - Client: client, - Dynamic: dynamic, - Name: clusterName, - Namespace: testing.Namespace, - GetOptions: GetOptions{ - WithClusterDef: true, - WithClusterVersion: true, - WithConfigMap: true, - WithService: true, - WithSecret: true, - WithPVC: true, - WithPod: true, - }, + var ( + err error + objs *ClusterObjects + ) + + testFn := func(client kubernetes.Interface) { + clusterName := testing.ClusterName + getter := ObjectsGetter{ + Client: client, + Dynamic: dynamic, + Name: clusterName, + Namespace: testing.Namespace, + GetOptions: getOptions, + } + + objs, err = getter.Get() + Expect(err).Should(Succeed()) + Expect(objs).ShouldNot(BeNil()) + Expect(objs.Cluster.Name).Should(Equal(clusterName)) + Expect(objs.ClusterDef.Name).Should(Equal(testing.ClusterDefName)) + Expect(objs.ClusterVersion.Name).Should(Equal(testing.ClusterVersionName)) + Expect(len(objs.Pods.Items)).Should(Equal(3)) + Expect(len(objs.Secrets.Items)).Should(Equal(1)) + Expect(len(objs.Services.Items)).Should(Equal(4)) + Expect(len(objs.PVCs.Items)).Should(Equal(1)) + Expect(len(objs.GetComponentInfo())).Should(Equal(1)) } - objs, err := getter.Get() - Expect(err).Should(Succeed()) - Expect(objs).ShouldNot(BeNil()) - Expect(objs.Cluster.Name).Should(Equal(clusterName)) - Expect(objs.ClusterDef.Name).Should(Equal(testing.ClusterDefName)) - Expect(objs.ClusterVersion.Name).Should(Equal(testing.ClusterVersionName)) + By("when node is not found") + testFn(testing.FakeClientSet(baseObjs...)) + Expect(len(objs.Nodes)).Should(Equal(0)) - Expect(len(objs.Pods.Items)).Should(Equal(3)) + By("when node is available") + baseObjs = append(baseObjs, testing.FakeNode()) + testFn(testing.FakeClientSet(baseObjs...)) Expect(len(objs.Nodes)).Should(Equal(1)) - Expect(len(objs.Secrets.Items)).Should(Equal(1)) - Expect(len(objs.Services.Items)).Should(Equal(4)) - Expect(len(objs.PVCs.Items)).Should(Equal(1)) - Expect(len(objs.GetComponentInfo())).Should(Equal(1)) }) }) diff --git a/internal/cli/cluster/helper.go b/internal/cli/cluster/helper.go index 53e5ef68e..023eddd50 100644 --- a/internal/cli/cluster/helper.go +++ b/internal/cli/cluster/helper.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -35,27 +38,41 @@ import ( "github.com/apecloud/kubeblocks/internal/constant" ) -// GetSimpleInstanceInfos return simple instance info that only contains instance name and role, the default +const ( + ComponentNameEmpty = "" +) + +// GetSimpleInstanceInfos returns simple instance info that only contains instance name and role, the default // instance should be the first element in the returned array. -func GetSimpleInstanceInfos(dynamic dynamic.Interface, name string, namespace string) []*InstanceInfo { - // if cluster status contains what we need, return directly - if infos := getInstanceInfoFromStatus(dynamic, name, namespace); len(infos) > 0 { +func GetSimpleInstanceInfos(dynamic dynamic.Interface, name, namespace string) []*InstanceInfo { + return GetSimpleInstanceInfosForComponent(dynamic, name, ComponentNameEmpty, namespace) +} + +// GetSimpleInstanceInfosForComponent returns simple instance info that only contains instance name and role for a component +func GetSimpleInstanceInfosForComponent(dynamic dynamic.Interface, name, componentName, namespace string) []*InstanceInfo { + // first get instance info from status, using the status as a cache + if infos := getInstanceInfoFromStatus(dynamic, name, componentName, namespace); len(infos) > 0 { return infos } - // if cluster status does not contain what we need, try to list all pods and build instance info - return getInstanceInfoByList(dynamic, name, namespace) + // missed in the status, try to list all pods and build instance info + return getInstanceInfoByList(dynamic, name, componentName, namespace) } -// getInstancesInfoFromCluster get instances info from cluster status -func getInstanceInfoFromStatus(dynamic dynamic.Interface, name string, namespace string) []*InstanceInfo { +// getInstancesInfoFromCluster gets instances info from cluster status +func getInstanceInfoFromStatus(dynamic dynamic.Interface, name, componentName, namespace string) []*InstanceInfo { var infos []*InstanceInfo cluster, err := GetClusterByName(dynamic, name, namespace) if err != nil { return nil } - // travel all components, check type - for _, c := range cluster.Status.Components { + // traverse all components, check the workload type + for compName, c := range cluster.Status.Components { + // filter by component name + if len(componentName) > 0 && compName != componentName { + continue + } + var info *InstanceInfo // workload type is Consensus if c.ConsensusSetStatus != nil { @@ -100,16 +117,23 @@ func getInstanceInfoFromStatus(dynamic dynamic.Interface, name string, namespace return infos } -// getInstanceInfoByList get instances info by list all pods -func getInstanceInfoByList(dynamic dynamic.Interface, name string, namespace string) []*InstanceInfo { +// getInstanceInfoByList gets instances info by listing all pods +func getInstanceInfoByList(dynamic dynamic.Interface, name, componentName, namespace string) []*InstanceInfo { var infos []*InstanceInfo + // filter by cluster name + labels := util.BuildLabelSelectorByNames("", []string{name}) + // filter by component name + if len(componentName) > 0 { + labels = util.BuildComponentNameLabels(labels, []string{componentName}) + } + objs, err := dynamic.Resource(schema.GroupVersionResource{Group: corev1.GroupName, Version: types.K8sCoreAPIVersion, Resource: "pods"}). - Namespace(namespace).List(context.TODO(), metav1.ListOptions{ - LabelSelector: util.BuildLabelSelectorByNames("", []string{name}), - }) + Namespace(namespace).List(context.TODO(), metav1.ListOptions{LabelSelector: labels}) + if err != nil { return nil } + for _, o := range objs.Items { infos = append(infos, &InstanceInfo{Name: o.GetName()}) } @@ -179,7 +203,7 @@ func GetComponentServices(svcList *corev1.ServiceList, c *appsv1alpha1.ClusterCo return internalSvcs, externalSvcs } -// GetExternalAddr get external IP from service annotation +// GetExternalAddr gets external IP from service annotation func GetExternalAddr(svc *corev1.Service) string { for _, ingress := range svc.Status.LoadBalancer.Ingress { if ingress.Hostname != "" { @@ -278,7 +302,7 @@ func BuildStorageClass(storages []StorageInfo) string { return util.CheckEmpty(strings.Join(scs, "\n")) } -// GetLatestVersion get the latest cluster versions that reference the cluster definition +// GetLatestVersion gets the latest cluster versions that referencing the cluster definition func GetLatestVersion(dynamic dynamic.Interface, clusterDef string) (string, error) { versionList, err := GetVersionByClusterDef(dynamic, clusterDef) if err != nil { @@ -313,3 +337,111 @@ func findLatestVersion(versions *appsv1alpha1.ClusterVersionList) *appsv1alpha1. } return version } + +type CompInfo struct { + Component *appsv1alpha1.ClusterComponentSpec + ComponentStatus *appsv1alpha1.ClusterComponentStatus + ComponentDef *appsv1alpha1.ClusterComponentDefinition +} + +func (info *CompInfo) InferPodName() (string, error) { + if info.ComponentStatus == nil { + return "", fmt.Errorf("component status is missing") + } + if info.ComponentStatus.Phase != appsv1alpha1.RunningClusterCompPhase || !*info.ComponentStatus.PodsReady { + return "", fmt.Errorf("component is not ready, please try later") + } + if info.ComponentStatus.ConsensusSetStatus != nil { + return info.ComponentStatus.ConsensusSetStatus.Leader.Pod, nil + } + if info.ComponentStatus.ReplicationSetStatus != nil { + return info.ComponentStatus.ReplicationSetStatus.Primary.Pod, nil + } + return "", fmt.Errorf("cannot pick a pod to connect, please specify the pod name explicitly by `--instance` flag") +} + +func FillCompInfoByName(ctx context.Context, dynamic dynamic.Interface, namespace, clusterName, componentName string) (*CompInfo, error) { + cluster, err := GetClusterByName(dynamic, clusterName, namespace) + if err != nil { + return nil, err + } + if cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { + return nil, fmt.Errorf("cluster %s is not running, please try again later", clusterName) + } + + compInfo := &CompInfo{} + // fill component + if len(componentName) == 0 { + compInfo.Component = &cluster.Spec.ComponentSpecs[0] + } else { + compInfo.Component = cluster.Spec.GetComponentByName(componentName) + } + + if compInfo.Component == nil { + return nil, fmt.Errorf("component %s not found in cluster %s", componentName, clusterName) + } + // fill component status + for name, compStatus := range cluster.Status.Components { + if name == compInfo.Component.Name { + compInfo.ComponentStatus = &compStatus + break + } + } + if compInfo.ComponentStatus == nil { + return nil, fmt.Errorf("componentStatus %s not found in cluster %s", componentName, clusterName) + } + + // find cluster def + clusterDef, err := GetClusterDefByName(dynamic, cluster.Spec.ClusterDefRef) + if err != nil { + return nil, err + } + // find component def by reference + for _, compDef := range clusterDef.Spec.ComponentDefs { + if compDef.Name == compInfo.Component.ComponentDefRef { + compInfo.ComponentDef = &compDef + break + } + } + if compInfo.ComponentDef == nil { + return nil, fmt.Errorf("componentDef %s not found in clusterDef %s", compInfo.Component.ComponentDefRef, clusterDef.Name) + } + return compInfo, nil +} + +func GetPodClusterName(pod *corev1.Pod) string { + if pod.Labels == nil { + return "" + } + return pod.Labels[constant.AppInstanceLabelKey] +} + +func GetPodComponentName(pod *corev1.Pod) string { + if pod.Labels == nil { + return "" + } + return pod.Labels[constant.KBAppComponentLabelKey] +} + +func GetPodWorkloadType(pod *corev1.Pod) string { + if pod.Labels == nil { + return "" + } + return pod.Labels[constant.WorkloadTypeLabelKey] +} + +func GetConfigMapByName(dynamic dynamic.Interface, namespace, name string) (*corev1.ConfigMap, error) { + cmObj := &corev1.ConfigMap{} + if err := GetK8SClientObject(dynamic, cmObj, types.ConfigmapGVR(), namespace, name); err != nil { + return nil, err + } + return cmObj, nil +} + +func GetConfigConstraintByName(dynamic dynamic.Interface, name string) (*appsv1alpha1.ConfigConstraint, error) { + ccObj := &appsv1alpha1.ConfigConstraint{} + if err := GetK8SClientObject(dynamic, ccObj, types.ConfigConstraintGVR(), "", name); err != nil { + return nil, err + } + return ccObj, nil +} diff --git a/internal/cli/cluster/helper_test.go b/internal/cli/cluster/helper_test.go index 129335fc6..f41cd4775 100644 --- a/internal/cli/cluster/helper_test.go +++ b/internal/cli/cluster/helper_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -102,4 +105,24 @@ var _ = Describe("helper", func() { Expect(latestVer).ShouldNot(BeNil()) Expect(latestVer.Name).Should(Equal("now-version")) }) + + It("get configmap by name", func() { + cmName := "test-cm" + dynamic := testing.FakeDynamicClient(testing.FakeConfigMap(cmName)) + cm, err := GetConfigMapByName(dynamic, testing.Namespace, cmName) + Expect(err).Should(Succeed()) + Expect(cm).ShouldNot(BeNil()) + + cm, err = GetConfigMapByName(dynamic, testing.Namespace, cmName+"error") + Expect(err).Should(HaveOccurred()) + Expect(cm).Should(BeNil()) + }) + + It("get config constraint by name", func() { + ccName := "test-cc" + dynamic := testing.FakeDynamicClient(testing.FakeConfigConstraint(ccName)) + cm, err := GetConfigConstraintByName(dynamic, ccName) + Expect(err).Should(Succeed()) + Expect(cm).ShouldNot(BeNil()) + }) }) diff --git a/internal/cli/cluster/name_generator.go b/internal/cli/cluster/name_generator.go index 1e1e2ec67..0bbc76a8c 100644 --- a/internal/cli/cluster/name_generator.go +++ b/internal/cli/cluster/name_generator.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cluster/name_generator_test.go b/internal/cli/cluster/name_generator_test.go index cfc65bc0a..55581f7ac 100644 --- a/internal/cli/cluster/name_generator_test.go +++ b/internal/cli/cluster/name_generator_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cluster/printer.go b/internal/cli/cluster/printer.go index 16cb8ccc9..34d95d2eb 100644 --- a/internal/cli/cluster/printer.go +++ b/internal/cli/cluster/printer.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( "io" + "strings" corev1 "k8s.io/api/core/v1" @@ -33,6 +37,7 @@ const ( PrintInstances PrintType = "instances" PrintComponents PrintType = "components" PrintEvents PrintType = "events" + PrintLabels PrintType = "label" ) type PrinterOptions struct { @@ -86,6 +91,11 @@ var mapTblInfo = map[PrintType]tblInfo{ addRow: AddEventRow, getOptions: GetOptions{WithClusterDef: true, WithPod: true, WithEvent: true}, }, + PrintLabels: { + header: []interface{}{"NAME", "NAMESPACE"}, + addRow: AddLabelRow, + getOptions: GetOptions{}, + }, } // Printer prints cluster info @@ -124,6 +134,16 @@ func (p *Printer) GetterOptions() GetOptions { return p.getOptions } +func AddLabelRow(tbl *printer.TablePrinter, objs *ClusterObjects, opt *PrinterOptions) { + c := objs.GetClusterInfo() + info := []interface{}{c.Name, c.Namespace} + if opt.ShowLabels { + labels := strings.ReplaceAll(c.Labels, ",", "\n") + info = append(info, labels) + } + tbl.AddRow(info...) +} + func AddComponentRow(tbl *printer.TablePrinter, objs *ClusterObjects, opt *PrinterOptions) { components := objs.GetComponentInfo() for _, c := range components { diff --git a/internal/cli/cluster/printer_test.go b/internal/cli/cluster/printer_test.go index 7cf3d84c7..cd78cc995 100644 --- a/internal/cli/cluster/printer_test.go +++ b/internal/cli/cluster/printer_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cluster/suite_test.go b/internal/cli/cluster/suite_test.go index 53c9e84b3..bc75d56a7 100644 --- a/internal/cli/cluster/suite_test.go +++ b/internal/cli/cluster/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cluster/types.go b/internal/cli/cluster/types.go index 9c9f17dc3..80708e72c 100644 --- a/internal/cli/cluster/types.go +++ b/internal/cli/cluster/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -20,6 +23,7 @@ import ( corev1 "k8s.io/api/core/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" ) type ClusterObjects struct { @@ -34,6 +38,9 @@ type ClusterObjects struct { Nodes []*corev1.Node ConfigMaps *corev1.ConfigMapList Events *corev1.EventList + + BackupPolicies []dpv1alpha1.BackupPolicy + Backups []dpv1alpha1.Backup } type ClusterInfo struct { diff --git a/internal/cli/cmd/accounts/base.go b/internal/cli/cmd/accounts/base.go index 9b029ce4a..5100416f0 100644 --- a/internal/cli/cmd/accounts/base.go +++ b/internal/cli/cmd/accounts/base.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -26,17 +29,18 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" - klog "k8s.io/klog/v2" + "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" + clusterutil "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/exec" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type AccountBaseOptions struct { - Namespace string ClusterName string CharType string ComponentName string @@ -49,12 +53,13 @@ type AccountBaseOptions struct { } var ( - errClusterNameNum = fmt.Errorf("please specify ONE cluster-name at a time") - errMissingUserName = fmt.Errorf("please specify username") - errMissingRoleName = fmt.Errorf("please specify at least ONE role name") - errInvalidRoleName = fmt.Errorf("invalid role name, should be one of [SUPERUSER, READWRITE, READONLY] ") - errInvalidOp = fmt.Errorf("invalid operation") - errCompNameOrInstName = fmt.Errorf("please specify either --component-name or --instance, not both") + errClusterNameNum = fmt.Errorf("please specify ONE cluster-name at a time") + errMissingUserName = fmt.Errorf("please specify username") + errMissingRoleName = fmt.Errorf("please specify at least ONE role name") + errInvalidRoleName = fmt.Errorf("invalid role name, should be one of [SUPERUSER, READWRITE, READONLY] ") + errInvalidOp = fmt.Errorf("invalid operation") + errCompNameOrInstName = fmt.Errorf("please specify either --component or --instance, they are exclusive") + errClusterNameorInstName = fmt.Errorf("specify either cluster name or --instance") ) func NewAccountBaseOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, op bindings.OperationKind) *AccountBaseOptions { @@ -65,29 +70,33 @@ func NewAccountBaseOptions(f cmdutil.Factory, streams genericclioptions.IOStream } func (o *AccountBaseOptions) AddFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.ComponentName, "component-name", "", "Specify the name of component to be connected. If not specified, the first component will be used.") + cmd.Flags().StringVar(&o.ComponentName, "component", "", "Specify the name of component to be connected. If not specified, pick the first one.") cmd.Flags().StringVarP(&o.PodName, "instance", "i", "", "Specify the name of instance to be connected.") } func (o *AccountBaseOptions) Validate(args []string) error { - if len(args) != 1 { + if len(args) > 1 { return errClusterNameNum - } else { - o.ClusterName = args[0] } - if len(o.PodName) > 0 && len(o.ComponentName) > 0 { - return errCompNameOrInstName + if len(o.PodName) > 0 { + if len(o.ComponentName) > 0 { + return errCompNameOrInstName + } + if len(args) > 0 { + return errClusterNameorInstName + } + } else if len(args) == 0 { + return errClusterNameorInstName + } + if len(args) == 1 { + o.ClusterName = args[0] } return nil } func (o *AccountBaseOptions) Complete(f cmdutil.Factory) error { var err error - o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() - if err != nil { - return err - } err = o.ExecOptions.Complete() if err != nil { return err @@ -96,27 +105,36 @@ func (o *AccountBaseOptions) Complete(f cmdutil.Factory) error { ctx, cancelFn := context.WithCancel(context.Background()) defer cancelFn() - compInfo, err := fillCompInfoByName(ctx, o.ExecOptions.Dynamic, o.Namespace, o.ClusterName, o.ComponentName) + if len(o.PodName) > 0 { + // get pod by name + o.Pod, err = o.ExecOptions.Client.CoreV1().Pods(o.Namespace).Get(ctx, o.PodName, metav1.GetOptions{}) + if err != nil { + return err + } + o.ClusterName = clusterutil.GetPodClusterName(o.Pod) + o.ComponentName = clusterutil.GetPodComponentName(o.Pod) + } + + compInfo, err := clusterutil.FillCompInfoByName(ctx, o.ExecOptions.Dynamic, o.Namespace, o.ClusterName, o.ComponentName) if err != nil { return err } // fill component name if len(o.ComponentName) == 0 { - o.ComponentName = compInfo.comp.Name + o.ComponentName = compInfo.Component.Name } // fill character type - o.CharType = compInfo.compDef.CharacterType + o.CharType = compInfo.ComponentDef.CharacterType - // fill pod name if len(o.PodName) == 0 { - if o.PodName, err = compInfo.inferPodName(); err != nil { + if o.PodName, err = compInfo.InferPodName(); err != nil { + return err + } + // get pod by name + o.Pod, err = o.ExecOptions.Client.CoreV1().Pods(o.Namespace).Get(ctx, o.PodName, metav1.GetOptions{}) + if err != nil { return err } - } - // get pod by name - o.Pod, err = o.ExecOptions.Client.CoreV1().Pods(o.Namespace).Get(ctx, o.PodName, metav1.GetOptions{}) - if err != nil { - return err } o.ExecOptions.Pod = o.Pod @@ -130,29 +148,32 @@ func (o *AccountBaseOptions) Complete(f cmdutil.Factory) error { return nil } -func (o *AccountBaseOptions) Run(f cmdutil.Factory, streams genericclioptions.IOStreams) error { +func (o *AccountBaseOptions) Run(cmd *cobra.Command, f cmdutil.Factory, streams genericclioptions.IOStreams) error { var err error response, err := o.Do() if err != nil { + if channelutil.IsUnSupportedError(err) { + return fmt.Errorf("command `%s` on characterType `%s` (defined in cluster: %s, component: %s) is not supported yet", cmd.Use, o.CharType, o.ClusterName, o.ComponentName) + } return err } switch o.AccountOp { case - sqlchannel.DeleteUserOp, - sqlchannel.RevokeUserRoleOp, - sqlchannel.GrantUserRoleOp: + channelutil.DeleteUserOp, + channelutil.RevokeUserRoleOp, + channelutil.GrantUserRoleOp: o.printGeneralInfo(response) err = nil - case sqlchannel.CreateUserOp: + case channelutil.CreateUserOp: o.printGeneralInfo(response) - if response.Event == sqlchannel.RespEveSucc { - printer.Alert(o.Out, "Please do REMEMBER the password for the new user! Once forgotten, it cannot be retrieved!") + if response.Event == channelutil.RespEveSucc { + printer.Alert(o.Out, "Please do REMEMBER the password for the new user! Once forgotten, it cannot be retrieved!\n") } err = nil - case sqlchannel.DescribeUserOp: + case channelutil.DescribeUserOp: err = o.printRoleInfo(response) - case sqlchannel.ListUsersOp: + case channelutil.ListUsersOp: err = o.printUserInfo(response) default: err = errInvalidOp @@ -168,16 +189,16 @@ func (o *AccountBaseOptions) Run(f cmdutil.Factory, streams genericclioptions.IO return err } -func (o *AccountBaseOptions) Do() (sqlchannel.SQLChannelResponse, error) { +func (o *AccountBaseOptions) Do() (channelutil.SQLChannelResponse, error) { klog.V(1).Info(fmt.Sprintf("connect to cluster %s, component %s, instance %s\n", o.ClusterName, o.ComponentName, o.PodName)) - response := sqlchannel.SQLChannelResponse{} - sqlClient, err := sqlchannel.NewHTTPClientWithPod(o.ExecOptions, o.Pod, o.CharType) + response := channelutil.SQLChannelResponse{} + sqlClient, err := sqlchannel.NewHTTPClientWithChannelPod(o.Pod, o.CharType) if err != nil { return response, err } - request := sqlchannel.SQLChannelRequest{Operation: (string)(o.AccountOp), Metadata: o.RequestMeta} - response, err = sqlClient.SendRequest(request) + request := channelutil.SQLChannelRequest{Operation: (string)(o.AccountOp), Metadata: o.RequestMeta} + response, err = sqlClient.SendRequest(o.ExecOptions, request) return response, err } @@ -189,13 +210,13 @@ func (o *AccountBaseOptions) newTblPrinterWithStyle(title string, header []inter return tblPrinter } -func (o *AccountBaseOptions) printGeneralInfo(response sqlchannel.SQLChannelResponse) { +func (o *AccountBaseOptions) printGeneralInfo(response channelutil.SQLChannelResponse) { tblPrinter := o.newTblPrinterWithStyle("QUERY RESULT", []interface{}{"RESULT", "MESSAGE"}) tblPrinter.AddRow(response.Event, response.Message) tblPrinter.Print() } -func (o *AccountBaseOptions) printMeta(response sqlchannel.SQLChannelResponse) { +func (o *AccountBaseOptions) printMeta(response channelutil.SQLChannelResponse) { meta := response.Metadata tblPrinter := o.newTblPrinterWithStyle("QUERY META", []interface{}{"START TIME", "END TIME", "OPERATION", "DATA"}) tblPrinter.SetStyle(printer.KubeCtlStyle) @@ -203,19 +224,19 @@ func (o *AccountBaseOptions) printMeta(response sqlchannel.SQLChannelResponse) { tblPrinter.Print() } -func (o *AccountBaseOptions) printUserInfo(response sqlchannel.SQLChannelResponse) error { - if response.Event == sqlchannel.RespEveFail { +func (o *AccountBaseOptions) printUserInfo(response channelutil.SQLChannelResponse) error { + if response.Event == channelutil.RespEveFail { o.printGeneralInfo(response) return nil } - // decode user info from metatdata - users := []sqlchannel.UserInfo{} + // decode user info from metadata + users := []channelutil.UserInfo{} err := json.Unmarshal([]byte(response.Message), &users) if err != nil { return err } - // render user info with username and pasword expired boolean + // render user info with username and password expired boolean tblPrinter := o.newTblPrinterWithStyle("USER INFO", []interface{}{"USERNAME", "EXPIRED"}) for _, user := range users { tblPrinter.AddRow(user.UserName, user.Expired) @@ -225,14 +246,14 @@ func (o *AccountBaseOptions) printUserInfo(response sqlchannel.SQLChannelRespons return nil } -func (o *AccountBaseOptions) printRoleInfo(response sqlchannel.SQLChannelResponse) error { - if response.Event == sqlchannel.RespEveFail { +func (o *AccountBaseOptions) printRoleInfo(response channelutil.SQLChannelResponse) error { + if response.Event == channelutil.RespEveFail { o.printGeneralInfo(response) return nil } - // decode role info from metatdata - users := []sqlchannel.UserInfo{} + // decode role info from metadata + users := []channelutil.UserInfo{} err := json.Unmarshal([]byte(response.Message), &users) if err != nil { return err diff --git a/internal/cli/cmd/accounts/base_test.go b/internal/cli/cmd/accounts/base_test.go index 041088afd..f0d124bac 100644 --- a/internal/cli/cmd/accounts/base_test.go +++ b/internal/cli/cmd/accounts/base_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -32,7 +35,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("Base Account Options", func() { @@ -79,19 +82,19 @@ var _ = Describe("Base Account Options", func() { Context("new options", func() { It("new option", func() { - for _, op := range []bindings.OperationKind{sqlchannel.CreateUserOp, sqlchannel.DeleteUserOp, - sqlchannel.ListUsersOp, sqlchannel.DescribeUserOp, - sqlchannel.GrantUserRoleOp, sqlchannel.RevokeUserRoleOp} { + for _, op := range []bindings.OperationKind{channelutil.CreateUserOp, channelutil.DeleteUserOp, + channelutil.ListUsersOp, channelutil.DescribeUserOp, + channelutil.GrantUserRoleOp, channelutil.RevokeUserRoleOp} { o := NewAccountBaseOptions(tf, streams, op) Expect(o).ShouldNot(BeNil()) } }) It("validate options", func() { - o := NewAccountBaseOptions(tf, streams, sqlchannel.CreateUserOp) + o := NewAccountBaseOptions(tf, streams, channelutil.CreateUserOp) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add two elements By("add two args") @@ -105,7 +108,7 @@ var _ = Describe("Base Account Options", func() { // set pod name o.PodName = "testpod" - Expect(o.Validate(args)).Should(Succeed()) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // set component name as well o.ComponentName = "testcomponent" Expect(o.Validate(args)).Should(MatchError(errCompNameOrInstName)) @@ -115,7 +118,7 @@ var _ = Describe("Base Account Options", func() { }) It("complete option", func() { - o := NewAccountBaseOptions(tf, streams, sqlchannel.CreateUserOp) + o := NewAccountBaseOptions(tf, streams, channelutil.CreateUserOp) Expect(o).ShouldNot(BeNil()) o.PodName = pods.Items[0].Name o.ClusterName = clusterName diff --git a/internal/cli/cmd/accounts/create.go b/internal/cli/cmd/accounts/create.go index 3581c6c36..0da00148f 100644 --- a/internal/cli/cmd/accounts/create.go +++ b/internal/cli/cmd/accounts/create.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -22,26 +25,26 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type CreateUserOptions struct { *AccountBaseOptions - info sqlchannel.UserInfo + info channelutil.UserInfo } func NewCreateUserOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *CreateUserOptions { return &CreateUserOptions{ - AccountBaseOptions: NewAccountBaseOptions(f, streams, sqlchannel.CreateUserOp), + AccountBaseOptions: NewAccountBaseOptions(f, streams, channelutil.CreateUserOp), } } func (o *CreateUserOptions) AddFlags(cmd *cobra.Command) { o.AccountBaseOptions.AddFlags(cmd) - cmd.Flags().StringVarP(&o.info.UserName, "username", "u", "", "Required. Specify the name of user, which must be unique.") + cmd.Flags().StringVar(&o.info.UserName, "name", "", "Required. Specify the name of user, which must be unique.") cmd.Flags().StringVarP(&o.info.Password, "password", "p", "", "Optional. Specify the password of user. The default value is empty, which means a random password will be generated.") // TODO:@shanshan add expire flag if needed - // cmd.Flags().DurationVar(&o.info.ExpireAt, "expire", 0, "Optional. Specify the expire time of password. The default value is 0, which means the user will never expire.") + // cmd.Flags().DurationVar(&o.info.ExpireAt, "expire", 0, "Optional. Specify the expired time of password. The default value is 0, which means the user will never expire.") } func (o CreateUserOptions) Validate(args []string) error { @@ -63,7 +66,7 @@ func (o *CreateUserOptions) Complete(f cmdutil.Factory) error { if len(o.info.Password) == 0 { o.info.Password, _ = password.Generate(10, 2, 0, false, false) } - // encode user info to metatdata + // encode user info to metadata o.RequestMeta, err = struct2Map(o.info) return err } diff --git a/internal/cli/cmd/accounts/create_test.go b/internal/cli/cmd/accounts/create_test.go index 04a5c2c79..ce9c6f669 100644 --- a/internal/cli/cmd/accounts/create_test.go +++ b/internal/cli/cmd/accounts/create_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -31,7 +34,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("Create Account Options", func() { @@ -81,14 +84,14 @@ var _ = Describe("Create Account Options", func() { o := NewCreateUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) Expect(o.AccountBaseOptions).ShouldNot(BeNil()) - Expect(o.AccountBaseOptions.AccountOp).Should(Equal(sqlchannel.CreateUserOp)) + Expect(o.AccountBaseOptions.AccountOp).Should(Equal(channelutil.CreateUserOp)) }) It("validate user name and password", func() { o := NewCreateUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add two elements By("add two args") diff --git a/internal/cli/cmd/accounts/delete.go b/internal/cli/cmd/accounts/delete.go index 1e13bb110..3939420e7 100644 --- a/internal/cli/cmd/accounts/delete.go +++ b/internal/cli/cmd/accounts/delete.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -22,23 +25,24 @@ import ( cmdutil "k8s.io/kubectl/pkg/cmd/util" "github.com/apecloud/kubeblocks/internal/cli/delete" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type DeleteUserOptions struct { *AccountBaseOptions - info sqlchannel.UserInfo + AutoApprove bool + info channelutil.UserInfo } func NewDeleteUserOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *DeleteUserOptions { return &DeleteUserOptions{ - AccountBaseOptions: NewAccountBaseOptions(f, streams, sqlchannel.DeleteUserOp), + AccountBaseOptions: NewAccountBaseOptions(f, streams, channelutil.DeleteUserOp), } } func (o *DeleteUserOptions) AddFlags(cmd *cobra.Command) { o.AccountBaseOptions.AddFlags(cmd) - cmd.Flags().StringVarP(&o.info.UserName, "username", "u", "", "Required. Specify the name of user") + cmd.Flags().StringVar(&o.info.UserName, "name", "", "Required user name, please specify it") } func (o *DeleteUserOptions) Validate(args []string) error { @@ -48,6 +52,9 @@ func (o *DeleteUserOptions) Validate(args []string) error { if len(o.info.UserName) == 0 { return errMissingUserName } + if o.AutoApprove { + return nil + } if err := delete.Confirm([]string{o.info.UserName}, o.In); err != nil { return err } diff --git a/internal/cli/cmd/accounts/delete_test.go b/internal/cli/cmd/accounts/delete_test.go index 197d38971..0f1d329a2 100644 --- a/internal/cli/cmd/accounts/delete_test.go +++ b/internal/cli/cmd/accounts/delete_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -32,7 +35,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("Delete Account Options", func() { @@ -83,14 +86,14 @@ var _ = Describe("Delete Account Options", func() { o := NewDeleteUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) Expect(o.AccountBaseOptions).ShouldNot(BeNil()) - Expect(o.AccountBaseOptions.AccountOp).Should(Equal(sqlchannel.DeleteUserOp)) + Expect(o.AccountBaseOptions.AccountOp).Should(Equal(channelutil.DeleteUserOp)) }) It("validate user name and password", func() { o := NewDeleteUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add one element By("add one more args, should fail") diff --git a/internal/cli/cmd/accounts/describe.go b/internal/cli/cmd/accounts/describe.go index 5ed56c42e..e45cfea3b 100644 --- a/internal/cli/cmd/accounts/describe.go +++ b/internal/cli/cmd/accounts/describe.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -21,23 +24,23 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" - sqlchannel "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type DescribeUserOptions struct { *AccountBaseOptions - info sqlchannel.UserInfo + info channelutil.UserInfo } func NewDescribeUserOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *DescribeUserOptions { return &DescribeUserOptions{ - AccountBaseOptions: NewAccountBaseOptions(f, streams, sqlchannel.DescribeUserOp), + AccountBaseOptions: NewAccountBaseOptions(f, streams, channelutil.DescribeUserOp), } } func (o *DescribeUserOptions) AddFlags(cmd *cobra.Command) { o.AccountBaseOptions.AddFlags(cmd) - cmd.Flags().StringVarP(&o.info.UserName, "username", "u", "", "Required. Specify the name of user") + cmd.Flags().StringVar(&o.info.UserName, "name", "", "Required user name, please specify it") } func (o DescribeUserOptions) Validate(args []string) error { diff --git a/internal/cli/cmd/accounts/describe_test.go b/internal/cli/cmd/accounts/describe_test.go index f0584f624..add3ef52b 100644 --- a/internal/cli/cmd/accounts/describe_test.go +++ b/internal/cli/cmd/accounts/describe_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -31,7 +34,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("Describe Account Options", func() { @@ -81,14 +84,14 @@ var _ = Describe("Describe Account Options", func() { o := NewDescribeUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) Expect(o.AccountBaseOptions).ShouldNot(BeNil()) - Expect(o.AccountBaseOptions.AccountOp).Should(Equal(sqlchannel.DescribeUserOp)) + Expect(o.AccountBaseOptions.AccountOp).Should(Equal(channelutil.DescribeUserOp)) }) It("validate user name and password", func() { o := NewDescribeUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add one element By("add one more args, should fail") diff --git a/internal/cli/cmd/accounts/grant.go b/internal/cli/cmd/accounts/grant.go index 6980264a4..75f4ba58c 100644 --- a/internal/cli/cmd/accounts/grant.go +++ b/internal/cli/cmd/accounts/grant.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -23,19 +26,19 @@ import ( "github.com/spf13/cobra" "golang.org/x/exp/slices" "k8s.io/cli-runtime/pkg/genericclioptions" - klog "k8s.io/klog/v2" + "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type GrantOptions struct { *AccountBaseOptions - info sqlchannel.UserInfo + info channelutil.UserInfo } func NewGrantOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, op bindings.OperationKind) *GrantOptions { - if (op != sqlchannel.GrantUserRoleOp) && (op != sqlchannel.RevokeUserRoleOp) { + if (op != channelutil.GrantUserRoleOp) && (op != channelutil.RevokeUserRoleOp) { klog.V(1).Infof("invalid operation kind: %s", op) return nil } @@ -46,11 +49,11 @@ func NewGrantOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, op func (o *GrantOptions) AddFlags(cmd *cobra.Command) { o.AccountBaseOptions.AddFlags(cmd) - cmd.Flags().StringVarP(&o.info.UserName, "username", "u", "", "Required. Specify the name of user.") + cmd.Flags().StringVar(&o.info.UserName, "name", "", "Required user name, please specify it.") cmd.Flags().StringVarP(&o.info.RoleName, "role", "r", "", "Role name should be one of {SUPERUSER, READWRITE, READONLY}") } -func (o GrantOptions) Validate(args []string) error { +func (o *GrantOptions) Validate(args []string) error { if err := o.AccountBaseOptions.Validate(args); err != nil { return err } @@ -67,8 +70,8 @@ func (o GrantOptions) Validate(args []string) error { } func (o *GrantOptions) validRoleName() error { - candiates := []string{sqlchannel.SuperUserRole, sqlchannel.ReadWriteRole, sqlchannel.ReadOnlyRole} - if slices.Contains(candiates, strings.ToLower(o.info.RoleName)) { + candidates := []string{string(channelutil.SuperUserRole), string(channelutil.ReadWriteRole), string(channelutil.ReadOnlyRole)} + if slices.Contains(candidates, strings.ToLower(o.info.RoleName)) { return nil } return errInvalidRoleName diff --git a/internal/cli/cmd/accounts/grant_test.go b/internal/cli/cmd/accounts/grant_test.go index ed7ecb64f..12b7f972b 100644 --- a/internal/cli/cmd/accounts/grant_test.go +++ b/internal/cli/cmd/accounts/grant_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -32,7 +35,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("Grant Account Options", func() { @@ -79,22 +82,22 @@ var _ = Describe("Grant Account Options", func() { Context("new options", func() { It("new option", func() { - for _, op := range []bindings.OperationKind{sqlchannel.GrantUserRoleOp, sqlchannel.RevokeUserRoleOp} { + for _, op := range []bindings.OperationKind{channelutil.GrantUserRoleOp, channelutil.RevokeUserRoleOp} { o := NewGrantOptions(tf, streams, op) Expect(o).ShouldNot(BeNil()) } - for _, op := range []bindings.OperationKind{sqlchannel.CreateUserOp, sqlchannel.DeleteUserOp, sqlchannel.DescribeUserOp, sqlchannel.ListUsersOp} { + for _, op := range []bindings.OperationKind{channelutil.CreateUserOp, channelutil.DeleteUserOp, channelutil.DescribeUserOp, channelutil.ListUsersOp} { o := NewGrantOptions(tf, streams, op) Expect(o).Should(BeNil()) } }) It("validate options", func() { - for _, op := range []bindings.OperationKind{sqlchannel.GrantUserRoleOp, sqlchannel.RevokeUserRoleOp} { + for _, op := range []bindings.OperationKind{channelutil.GrantUserRoleOp, channelutil.RevokeUserRoleOp} { o := NewGrantOptions(tf, streams, op) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add one element By("add one more args, should fail") @@ -114,7 +117,7 @@ var _ = Describe("Grant Account Options", func() { }) It("complete option", func() { - o := NewGrantOptions(tf, streams, sqlchannel.GrantUserRoleOp) + o := NewGrantOptions(tf, streams, channelutil.GrantUserRoleOp) Expect(o).ShouldNot(BeNil()) o.PodName = pods.Items[0].Name o.ClusterName = clusterName diff --git a/internal/cli/cmd/accounts/list.go b/internal/cli/cmd/accounts/list.go index 9e7392c2e..4ce91d3c0 100644 --- a/internal/cli/cmd/accounts/list.go +++ b/internal/cli/cmd/accounts/list.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -20,7 +23,7 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type ListUserOptions struct { @@ -29,7 +32,7 @@ type ListUserOptions struct { func NewListUserOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *ListUserOptions { return &ListUserOptions{ - AccountBaseOptions: NewAccountBaseOptions(f, streams, sqlchannel.ListUsersOp), + AccountBaseOptions: NewAccountBaseOptions(f, streams, channelutil.ListUsersOp), } } func (o ListUserOptions) Validate(args []string) error { diff --git a/internal/cli/cmd/accounts/list_test.go b/internal/cli/cmd/accounts/list_test.go index 130a9f254..e56d4d484 100644 --- a/internal/cli/cmd/accounts/list_test.go +++ b/internal/cli/cmd/accounts/list_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts @@ -31,7 +34,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var _ = Describe("List Account Options", func() { @@ -81,14 +84,14 @@ var _ = Describe("List Account Options", func() { o := NewListUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) Expect(o.AccountBaseOptions).ShouldNot(BeNil()) - Expect(o.AccountBaseOptions.AccountOp).Should(Equal(sqlchannel.ListUsersOp)) + Expect(o.AccountBaseOptions.AccountOp).Should(Equal(channelutil.ListUsersOp)) }) It("validate options", func() { o := NewListUserOptions(tf, streams) Expect(o).ShouldNot(BeNil()) args := []string{} - Expect(o.Validate(args)).Should(MatchError(errClusterNameNum)) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // add two elements By("add two args") @@ -102,7 +105,7 @@ var _ = Describe("List Account Options", func() { // set pod name o.PodName = "pod1" - Expect(o.Validate(args)).Should(Succeed()) + Expect(o.Validate(args)).Should(MatchError(errClusterNameorInstName)) // set component name o.ComponentName = "foo-component" Expect(o.Validate(args)).Should(MatchError(errCompNameOrInstName)) diff --git a/internal/cli/cmd/accounts/suite_test.go b/internal/cli/cmd/accounts/suite_test.go index 1b9cf327b..3c07b6358 100644 --- a/internal/cli/cmd/accounts/suite_test.go +++ b/internal/cli/cmd/accounts/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts diff --git a/internal/cli/cmd/accounts/util.go b/internal/cli/cmd/accounts/util.go index 2ccf2c356..df1e7c942 100644 --- a/internal/cli/cmd/accounts/util.go +++ b/internal/cli/cmd/accounts/util.go @@ -1,32 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package accounts import ( - "context" "encoding/json" - "fmt" - - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/dynamic" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/cli/types" ) func struct2Map(obj interface{}) (map[string]interface{}, error) { @@ -40,82 +34,3 @@ func struct2Map(obj interface{}) (map[string]interface{}, error) { } return m, nil } - -type compInfo struct { - comp *appsv1alpha1.ClusterComponentSpec - compStatus *appsv1alpha1.ClusterComponentStatus - compDef *appsv1alpha1.ClusterComponentDefinition -} - -func (info *compInfo) inferPodName() (string, error) { - if info.compStatus == nil { - return "", fmt.Errorf("component status is missing") - } - if info.compStatus.Phase != appsv1alpha1.RunningClusterCompPhase || !*info.compStatus.PodsReady { - return "", fmt.Errorf("component is not ready, please try later") - } - if info.compStatus.ConsensusSetStatus != nil { - return info.compStatus.ConsensusSetStatus.Leader.Pod, nil - } - if info.compStatus.ReplicationSetStatus != nil { - return info.compStatus.ReplicationSetStatus.Primary.Pod, nil - } - return "", fmt.Errorf("cannot infer the pod to connect, please specify the pod name explicitly by `--instance` flag") -} - -func fillCompInfoByName(ctx context.Context, dynamic dynamic.Interface, namespace, clusterName, componentName string) (*compInfo, error) { - // find cluster - obj, err := dynamic.Resource(types.ClusterGVR()).Namespace(namespace).Get(ctx, clusterName, metav1.GetOptions{}) - if err != nil { - return nil, err - } - cluster := &appsv1alpha1.Cluster{} - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, cluster); err != nil { - return nil, err - } - if cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { - return nil, fmt.Errorf("cluster %s is not running, please try later", clusterName) - } - - compInfo := &compInfo{} - // fill component - if len(componentName) == 0 { - compInfo.comp = &cluster.Spec.ComponentSpecs[0] - } else { - compInfo.comp = cluster.Spec.GetComponentByName(componentName) - } - if compInfo.comp == nil { - return nil, fmt.Errorf("component %s not found in cluster %s", componentName, clusterName) - } - // fill component status - for name, compStatus := range cluster.Status.Components { - if name == compInfo.comp.Name { - compInfo.compStatus = &compStatus - break - } - } - if compInfo.compStatus == nil { - return nil, fmt.Errorf("componentStatus %s not found in cluster %s", componentName, clusterName) - } - - // find cluster def - obj, err = dynamic.Resource(types.ClusterDefGVR()).Namespace(metav1.NamespaceAll).Get(ctx, cluster.Spec.ClusterDefRef, metav1.GetOptions{}) - if err != nil { - return nil, err - } - clusterDef := &appsv1alpha1.ClusterDefinition{} - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, clusterDef); err != nil { - return nil, err - } - // find component def by reference - for _, compDef := range clusterDef.Spec.ComponentDefs { - if compDef.Name == compInfo.comp.ComponentDefRef { - compInfo.compDef = &compDef - break - } - } - if compInfo.compDef == nil { - return nil, fmt.Errorf("componentDef %s not found in clusterDef %s", compInfo.comp.ComponentDefRef, clusterDef.Name) - } - return compInfo, nil -} diff --git a/internal/cli/cmd/addon/addon.go b/internal/cli/cmd/addon/addon.go index 63285fa88..a1e2774d4 100644 --- a/internal/cli/cmd/addon/addon.go +++ b/internal/cli/cmd/addon/addon.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package addon @@ -20,10 +23,13 @@ import ( "context" "encoding/json" "fmt" + "path" + "sort" "strconv" "strings" "github.com/docker/cli/cli" + "github.com/jedib0t/go-pretty/v6/table" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/spf13/viper" @@ -40,6 +46,7 @@ import ( "k8s.io/utils/strings/slices" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/cmd/plugin" "github.com/apecloud/kubeblocks/internal/cli/list" "github.com/apecloud/kubeblocks/internal/cli/patch" "github.com/apecloud/kubeblocks/internal/cli/printer" @@ -124,6 +131,7 @@ func newDescribeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cob Use: "describe ADDON_NAME", Short: "Describe an addon specification.", Args: cli.ExactArgs(1), + Aliases: []string{"desc"}, ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.AddonGVR()), Run: func(cmd *cobra.Command, args []string) { util.CheckErr(o.init(args)) @@ -173,11 +181,12 @@ func newEnableCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra # Enabled "prometheus" addon and its extra alertmanager component with custom resources settings kbcli addon enable prometheus --memory 512Mi/4Gi --storage 8Gi --replicas 2 \ - --memory alertmanager:16Mi/256Mi --storage: alertmanager:1Gi --replicas alertmanager:2 + --memory alertmanager:16Mi/256Mi --storage alertmanager:1Gi --replicas alertmanager:2 # Enabled "prometheus" addon with tolerations - kbcli addon enable prometheus --tolerations '[[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]]' \ - --tolerations 'alertmanager:[[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]]' + kbcli addon enable prometheus \ + --tolerations '[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' \ + --tolerations 'alertmanager:[{"key":"taintkey","operator":"Equal","effect":"NoSchedule","value":"true"}]' # Enabled "prometheus" addon with helm like custom settings kbcli addon enable prometheus --set prometheus.alertmanager.image.tag=v0.24.0 @@ -199,7 +208,9 @@ func newEnableCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra "Sets addon CPU resource values (--cpu [extraName:]/) (can specify multiple if has extra items))") cmd.Flags().StringArrayVar(&o.addonEnableFlags.StorageSets, "storage", []string{}, `Sets addon storage size (--storage [extraName:]) (can specify multiple if has extra items)). -Additional notes for Helm type Addon, that resizing storage will fail if modified value is a storage request size +Additional notes: +1. Specify '0' value will remove storage values settings and explicitly disable 'persistentVolumeEnabled' attribute. +2. For Helm type Addon, that resizing storage will fail if modified value is a storage request size that belongs to StatefulSet's volume claim template, to resolve 'Failed' Addon status possible action is disable and re-enable the addon (More info on how-to resize a PVC: https://kubernetes.io/docs/concepts/storage/persistent-volumes#resources). `) @@ -298,6 +309,11 @@ func (o *addonCmdOpts) validate() error { return fmt.Errorf("addon %s INSTALLABLE-SELECTOR has no matching requirement", o.Names) } } + + if err := o.installAndUpgradePlugins(); err != nil { + fmt.Fprintf(o.Out, "failed to install/upgrade plugins: %v\n", err) + } + return nil } @@ -352,7 +368,7 @@ func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) er return nil } - labels := []string{} + var labels []string for k, v := range o.addon.Labels { if strings.Contains(k, constant.APIGroup) { labels = append(labels, fmt.Sprintf("%s=%s", k, v)) @@ -362,14 +378,18 @@ func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) er printer.PrintPairStringToLine("Description", o.addon.Spec.Description, 0) printer.PrintPairStringToLine("Labels", strings.Join(labels, ","), 0) printer.PrintPairStringToLine("Type", string(o.addon.Spec.Type), 0) - printer.PrintPairStringToLine("Extras", strings.Join(o.addon.GetExtraNames(), ","), 0) + if len(o.addon.GetExtraNames()) > 0 { + printer.PrintPairStringToLine("Extras", strings.Join(o.addon.GetExtraNames(), ","), 0) + } printer.PrintPairStringToLine("Status", string(o.addon.Status.Phase), 0) var autoInstall bool if o.addon.Spec.Installable != nil { autoInstall = o.addon.Spec.Installable.AutoInstall } printer.PrintPairStringToLine("Auto-install", strconv.FormatBool(autoInstall), 0) - printer.PrintPairStringToLine("Auto-install selector", strings.Join(o.addon.Spec.Installable.GetSelectorsStrings(), ","), 0) + if len(o.addon.Spec.Installable.GetSelectorsStrings()) > 0 { + printer.PrintPairStringToLine("Auto-install selector", strings.Join(o.addon.Spec.Installable.GetSelectorsStrings(), ","), 0) + } switch o.addon.Status.Phase { case extensionsv1alpha1.AddonEnabled: @@ -377,7 +397,7 @@ func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) er printer.PrintLineWithTabSeparator() if err := printer.PrintTable(o.Out, nil, printInstalled, "NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS", - "TOLERATIONS", "PV Enabled"); err != nil { + "TOLERATIONS", "PV-ENABLED"); err != nil { return err } default: @@ -402,12 +422,35 @@ func addonDescribeHandler(o *addonCmdOpts, cmd *cobra.Command, args []string) er } if err := printer.PrintTable(o.Out, nil, printInstallable, "NAME", "REPLICAS", "STORAGE", "CPU (REQ/LIMIT)", "MEMORY (REQ/LIMIT)", "STORAGE-CLASS", - "TOLERATIONS", "PV Enabled"); err != nil { + "TOLERATIONS", "PV-ENABLED"); err != nil { return err } printer.PrintLineWithTabSeparator() } } + + // print failed message + if o.addon.Status.Phase == extensionsv1alpha1.AddonFailed { + var tbl *printer.TablePrinter + printHeader := true + for _, c := range o.addon.Status.Conditions { + if c.Status == metav1.ConditionTrue { + continue + } + if printHeader { + fmt.Fprintln(o.Out, "Failed Message") + tbl = printer.NewTablePrinter(o.Out) + tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{ + {Number: 3, WidthMax: 120}, + }) + tbl.SetHeader("TIME", "REASON", "MESSAGE") + printHeader = false + } + tbl.AddRow(util.TimeFormat(&c.LastTransitionTime), c.Reason, c.Message) + } + tbl.Print() + } + return nil } @@ -423,6 +466,7 @@ func addonEnableDisableHandler(o *addonCmdOpts, cmd *cobra.Command, args []strin func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[string]interface{}) (err error) { extraNames := o.addon.GetExtraNames() installSpec := extensionsv1alpha1.AddonInstallSpec{ + Enabled: true, AddonInstallSpecItem: extensionsv1alpha1.NewAddonInstallSpecItem(), } // only using named return value in defer function @@ -441,31 +485,12 @@ func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[s }() if o.addonEnableFlags.useDefault() { - if len(o.addon.Spec.DefaultInstallValues) == 0 { - installSpec.Enabled = true - return nil - } - - for _, di := range o.addon.Spec.GetSortedDefaultInstallValues() { - if len(di.Selectors) == 0 { - installSpec = di.AddonInstallSpec - break - } - for _, s := range di.Selectors { - if !s.MatchesFromConfig() { - continue - } - installSpec = di.AddonInstallSpec - break - } - } - installSpec.Enabled = true return nil } - // extractInstallSpecExtraItem extract extensionsv1alpha1.AddonInstallExtraItem - // for the matching arg name, if not found it will append extensionsv1alpha1.AddonInstallExtraItem - // item to installSpec.ExtraItems and return its pointer. + // extractInstallSpecExtraItem extracts extensionsv1alpha1.AddonInstallExtraItem + // for the matching arg name, if not found, appends extensionsv1alpha1.AddonInstallExtraItem + // item to installSpec.ExtraItems and returns its pointer. extractInstallSpecExtraItem := func(name string) (*extensionsv1alpha1.AddonInstallExtraItem, error) { var pItem *extensionsv1alpha1.AddonInstallExtraItem for i, eItem := range installSpec.ExtraItems { @@ -487,15 +512,13 @@ func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[s return pItem, nil } - twoTuplesProcessor := func(s, flag string, + _tuplesProcessor := func(t []string, s, flag string, valueTransformer func(s, flag string) (interface{}, error), valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), ) error { - t := strings.SplitN(s, ":", 2) l := len(t) var name string var result interface{} - var err error switch l { case 2: name = t[0] @@ -525,6 +548,31 @@ func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[s return nil } + twoTuplesProcessor := func(s, flag string, + valueTransformer func(s, flag string) (interface{}, error), + valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), + ) error { + t := strings.SplitN(s, ":", 2) + return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner) + } + + twoTuplesJSONProcessor := func(s, flag string, + valueTransformer func(s, flag string) (interface{}, error), + valueAssigner func(*extensionsv1alpha1.AddonInstallSpecItem, interface{}), + ) error { + var jsonArray []map[string]interface{} + var t []string + + err := json.Unmarshal([]byte(s), &jsonArray) + if err != nil { + // not a valid JSON array treat it a 2 tuples + t = strings.SplitN(s, ":", 2) + } else { + t = []string{s} + } + return _tuplesProcessor(t, s, flag, valueTransformer, valueAssigner) + } + reqLimitResTransformer := func(s, flag string) (interface{}, error) { t := strings.SplitN(s, "/", 2) if len(t) != 2 { @@ -575,7 +623,7 @@ func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[s } for _, v := range f.TolerationsSet { - if err := twoTuplesProcessor(v, "tolerations", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { + if err := twoTuplesJSONProcessor(v, "tolerations", nil, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { item.Tolerations = i.(string) }); err != nil { return err @@ -590,7 +638,18 @@ func (o *addonCmdOpts) buildEnablePatch(flags []*pflag.Flag, spec, install map[s } return q, nil }, func(item *extensionsv1alpha1.AddonInstallSpecItem, i interface{}) { - item.Resources.Requests[corev1.ResourceStorage] = i.(resource.Quantity) + q := i.(resource.Quantity) + // for 0 storage size, remove storage request value and explicitly disable `persistentVolumeEnabled` + if v, _ := q.AsInt64(); v == 0 { + delete(item.Resources.Requests, corev1.ResourceStorage) + b := false + item.PVEnabled = &b + return + } + item.Resources.Requests[corev1.ResourceStorage] = q + // explicitly enable `persistentVolumeEnabled` if with provided storage size setting + b := true + item.PVEnabled = &b }); err != nil { return err } @@ -723,6 +782,28 @@ func addonListRun(o *list.ListOptions) error { } printRows := func(tbl *printer.TablePrinter) error { + // sort addons with .status.Phase then .metadata.name + sort.SliceStable(infos, func(i, j int) bool { + toAddon := func(idx int) *extensionsv1alpha1.Addon { + addon := &extensionsv1alpha1.Addon{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(infos[idx].Object.(*unstructured.Unstructured).Object, addon); err != nil { + return nil + } + return addon + } + iAddon := toAddon(i) + jAddon := toAddon(j) + if iAddon == nil { + return true + } + if jAddon == nil { + return false + } + if iAddon.Status.Phase == jAddon.Status.Phase { + return iAddon.GetName() < jAddon.GetName() + } + return iAddon.Status.Phase < jAddon.Status.Phase + }) for _, info := range infos { addon := &extensionsv1alpha1.Addon{} obj := info.Object.(*unstructured.Unstructured) @@ -753,3 +834,62 @@ func addonListRun(o *list.ListOptions) error { } return nil } + +func (o *addonCmdOpts) installAndUpgradePlugins() error { + if len(o.addon.Spec.CliPlugins) == 0 { + return nil + } + + plugin.InitPlugin() + + paths := plugin.GetKbcliPluginPath() + indexes, err := plugin.ListIndexes(paths) + if err != nil { + return err + } + + indexRepositoryToNme := make(map[string]string) + for _, index := range indexes { + indexRepositoryToNme[index.URL] = index.Name + } + + var plugins []string + var names []string + for _, p := range o.addon.Spec.CliPlugins { + names = append(names, p.Name) + indexName, ok := indexRepositoryToNme[p.IndexRepository] + if !ok { + // index not found, add it + _, indexName = path.Split(p.IndexRepository) + if err := plugin.AddIndex(paths, indexName, p.IndexRepository); err != nil { + return err + } + } + plugins = append(plugins, fmt.Sprintf("%s/%s", indexName, p.Name)) + } + + installOption := &plugin.PluginInstallOption{ + IOStreams: o.IOStreams, + } + upgradeOption := &plugin.UpgradeOptions{ + IOStreams: o.IOStreams, + } + + // install plugins + if err := installOption.Complete(plugins); err != nil { + return err + } + if err := installOption.Install(); err != nil { + return err + } + + // upgrade existed plugins + if err := upgradeOption.Complete(names); err != nil { + return err + } + if err := upgradeOption.Run(); err != nil { + return err + } + + return nil +} diff --git a/internal/cli/cmd/addon/addon_test.go b/internal/cli/cmd/addon/addon_test.go index 229a0830a..7dfd321cd 100644 --- a/internal/cli/cmd/addon/addon_test.go +++ b/internal/cli/cmd/addon/addon_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package addon diff --git a/internal/cli/cmd/addon/suite_test.go b/internal/cli/cmd/addon/suite_test.go index c641069f2..760d16b2b 100644 --- a/internal/cli/cmd/addon/suite_test.go +++ b/internal/cli/cmd/addon/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package addon diff --git a/internal/cli/cmd/alert/add_receiver.go b/internal/cli/cmd/alert/add_receiver.go index a0a3a9334..402e82716 100644 --- a/internal/cli/cmd/alert/add_receiver.go +++ b/internal/cli/cmd/alert/add_receiver.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert @@ -53,13 +56,13 @@ var ( kbcli alert add-receiver --webhook='url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=XXX' # add email receiver - kbcli alter add-receiver --email='a@foo.com,b@foo.com' + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' # add email receiver, and only receive alert from cluster mycluster - kbcli alter add-receiver --email='a@foo.com,b@foo.com' --cluster=mycluster + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' --cluster=mycluster # add email receiver, and only receive alert from cluster mycluster and alert severity is warning - kbcli alter add-receiver --email='a@foo.com,b@foo.com' --cluster=mycluster --severity=warning + kbcli alter add-receiver --email='user1@kubeblocks.io,user2@kubeblocks.io' --cluster=mycluster --severity=warning # add slack receiver kbcli alert add-receiver --slack api_url=https://hooks.slackConfig.com/services/foo,channel=monitor,username=kubeblocks-alert-bot`) @@ -100,11 +103,11 @@ func newAddReceiverCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) * }, } - cmd.Flags().StringArrayVar(&o.emails, "email", []string{}, "Add email address, such as bar@foo.com, more than one emailConfig can be specified separated by comma") + cmd.Flags().StringArrayVar(&o.emails, "email", []string{}, "Add email address, such as user@kubeblocks.io, more than one emailConfig can be specified separated by comma") cmd.Flags().StringArrayVar(&o.webhooks, "webhook", []string{}, "Add webhook receiver, such as url=https://open.feishu.cn/open-apis/bot/v2/hook/foo,token=xxxxx") cmd.Flags().StringArrayVar(&o.slacks, "slack", []string{}, "Add slack receiver, such as api_url=https://hooks.slackConfig.com/services/foo,channel=monitor,username=kubeblocks-alert-bot") - cmd.Flags().StringArrayVar(&o.clusters, "cluster", []string{}, "Cluster name, such as mycluster, more than one cluster can be specified, such as mycluster,mycluster2") - cmd.Flags().StringArrayVar(&o.severities, "severity", []string{}, "Alert severity, critical, warning or info, more than one severity can be specified, such as critical,warning") + cmd.Flags().StringArrayVar(&o.clusters, "cluster", []string{}, "Cluster name, such as mycluster, more than one cluster can be specified, such as mycluster1,mycluster2") + cmd.Flags().StringArrayVar(&o.severities, "severity", []string{}, "Alert severity level, critical, warning or info, more than one severity level can be specified, such as critical,warning") // register completions util.CheckErr(cmd.RegisterFlagCompletionFunc("severity", @@ -145,7 +148,7 @@ func (o *addReceiverOptions) validate(args []string) error { return fmt.Errorf("must specify at least one receiver, such as --email, --webhook or --slack") } - // if name is not specified, generate a random name + // if name is not specified, generate a random one if len(args) == 0 { o.name = generateReceiverName() } else { @@ -162,7 +165,7 @@ func (o *addReceiverOptions) validate(args []string) error { return nil } -// checkSeverities check if severity is valid +// checkSeverities checks if severity is valid func (o *addReceiverOptions) checkSeverities() error { if len(o.severities) == 0 { return nil @@ -185,7 +188,7 @@ func (o *addReceiverOptions) checkSeverities() error { return nil } -// checkEmails check if email SMTP is configured, if not, do not allow to add email receiver +// checkEmails checks if email SMTP is configured, if not, do not allow to add email receiver func (o *addReceiverOptions) checkEmails() error { if len(o.emails) == 0 { return nil @@ -404,7 +407,7 @@ func buildSlackConfigs(slacks []string) ([]*slackConfig, error) { if len(m) == 0 { return nil, fmt.Errorf("invalid slack: %s, slack config should be in the format of api_url=my-api-url,channel=my-channel,username=my-username", slackStr) } - s := slackConfig{} + s := slackConfig{TitleLink: ""} for k, v := range m { // check slackConfig keys switch slackKey(k) { diff --git a/internal/cli/cmd/alert/add_receiver_test.go b/internal/cli/cmd/alert/add_receiver_test.go index 031279ec1..b146079fc 100644 --- a/internal/cli/cmd/alert/add_receiver_test.go +++ b/internal/cli/cmd/alert/add_receiver_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert @@ -44,11 +47,11 @@ var mockBaseOptions = func(s genericclioptions.IOStreams) baseOptions { o := baseOptions{IOStreams: s} alertManagerConfig := ` global: - smtp_from: alert-test@apecloud.com + smtp_from: user@kubeblocks.io smtp_smarthost: smtp.feishu.cn:587 - smtp_auth_username: alert-test@apecloud.com + smtp_auth_username: admin@kubeblocks.io smtp_auth_password: 123456abc - smtp_auth_identity: alert-test@apecloud.com + smtp_auth_identity: admin@kubeblocks.io receivers: - name: default-receiver - name: receiver-7pb52 @@ -110,7 +113,7 @@ var _ = Describe("add receiver", func() { Expect(o.validate([]string{})).Should(HaveOccurred()) By("set email, do not specify the name") - o.emails = []string{"foo@bar.com"} + o.emails = []string{"user@kubeblocks.io"} o.alterConfigMap = mockConfigmap(alertConfigmapName, alertConfigFileName, "") Expect(o.validate([]string{})).Should(HaveOccurred()) Expect(o.name).ShouldNot(BeEmpty()) @@ -127,7 +130,7 @@ var _ = Describe("add receiver", func() { It("build receiver", func() { o := addReceiverOptions{baseOptions: baseOptions{IOStreams: s}} - o.emails = []string{"foo@bar.com", "foo1@bar.com,foo2@bar.com"} + o.emails = []string{"user@kubeblocks.io", "user1@kubeblocks.io,user2@kubeblocks.io"} o.webhooks = []string{"url=https://oapi.dingtalk.com/robot/send", "url=https://oapi.dingtalk.com/robot/send,url=https://oapi.dingtalk.com/robot/send?"} o.slacks = []string{"api_url=https://foo.com,channel=foo,username=test"} o.webhookConfigMap = mockConfigmap(webhookAdaptorConfigmapName, webhookAdaptorFileName, "") diff --git a/internal/cli/cmd/alert/alert_test.go b/internal/cli/cmd/alert/alert_test.go index 4e49958bb..c4814f350 100644 --- a/internal/cli/cmd/alert/alert_test.go +++ b/internal/cli/cmd/alert/alert_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/alter.go b/internal/cli/cmd/alert/alter.go index 849dcfb60..0fb237605 100644 --- a/internal/cli/cmd/alert/alter.go +++ b/internal/cli/cmd/alert/alter.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/delete_receiver.go b/internal/cli/cmd/alert/delete_receiver.go index 0e6e37966..e6ad93bbe 100644 --- a/internal/cli/cmd/alert/delete_receiver.go +++ b/internal/cli/cmd/alert/delete_receiver.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/delete_receiver_test.go b/internal/cli/cmd/alert/delete_receiver_test.go index 9998a4041..4101bb2d2 100644 --- a/internal/cli/cmd/alert/delete_receiver_test.go +++ b/internal/cli/cmd/alert/delete_receiver_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/list_receivers.go b/internal/cli/cmd/alert/list_receivers.go index a2577e4cd..6bd434a2e 100644 --- a/internal/cli/cmd/alert/list_receivers.go +++ b/internal/cli/cmd/alert/list_receivers.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert @@ -122,7 +125,7 @@ func (o *listReceiversOptions) run() error { return nil } -// getRouteInfo get route clusters and severity +// getRouteInfo gets route clusters and severity func getRouteInfo(route *route) map[string][]string { routeInfoMap := map[string][]string{ routeMatcherClusterKey: {}, diff --git a/internal/cli/cmd/alert/list_receivers_test.go b/internal/cli/cmd/alert/list_receivers_test.go index 77509a4a4..266ba2ed0 100644 --- a/internal/cli/cmd/alert/list_receivers_test.go +++ b/internal/cli/cmd/alert/list_receivers_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/suite_test.go b/internal/cli/cmd/alert/suite_test.go index 7f39d3215..675cd8b53 100644 --- a/internal/cli/cmd/alert/suite_test.go +++ b/internal/cli/cmd/alert/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/types.go b/internal/cli/cmd/alert/types.go index df6eaec49..6ce0c5b89 100644 --- a/internal/cli/cmd/alert/types.go +++ b/internal/cli/cmd/alert/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert @@ -66,7 +69,7 @@ const ( routeMatcherSeverityType = "severity" ) -// severity is the severity of alert +// severity is the severity level of alert type severity string const ( @@ -99,9 +102,10 @@ type slackKey string // slackConfig keys const ( - slackAPIURL slackKey = "api_url" - slackChannel slackKey = "channel" - slackUsername slackKey = "username" + slackAPIURL slackKey = "api_url" + slackChannel slackKey = "channel" + slackUsername slackKey = "username" + slackTitleLink slackKey = "title_link" ) // emailConfig is the email config of receiver @@ -116,10 +120,13 @@ type webhookConfig struct { MaxAlerts int `json:"max_alerts,omitempty"` } +// slackConfig is the alertmanager slack config of receiver +// ref: https://prometheus.io/docs/alerting/latest/configuration/#slack_config type slackConfig struct { - APIURL string `json:"api_url,omitempty"` - Channel string `json:"channel,omitempty"` - Username string `json:"username,omitempty"` + APIURL string `json:"api_url,omitempty"` + Channel string `json:"channel,omitempty"` + Username string `json:"username,omitempty"` + TitleLink string `json:"title_link"` } // receiver is the receiver of alert diff --git a/internal/cli/cmd/alert/util.go b/internal/cli/cmd/alert/util.go index 0cc131a26..906012a99 100644 --- a/internal/cli/cmd/alert/util.go +++ b/internal/cli/cmd/alert/util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert diff --git a/internal/cli/cmd/alert/util_test.go b/internal/cli/cmd/alert/util_test.go index ad7627cbe..25bad4b42 100644 --- a/internal/cli/cmd/alert/util_test.go +++ b/internal/cli/cmd/alert/util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package alert @@ -52,7 +55,7 @@ var _ = Describe("alter", func() { } }) - It("remove duplicate string from slice", func() { + It("remove duplicated string from slice", func() { slice := []string{"a", "b", "a", "c"} res := removeDuplicateStr(slice) Expect(res).ShouldNot(BeNil()) diff --git a/internal/cli/cmd/backupconfig/backup_config.go b/internal/cli/cmd/backupconfig/backup_config.go deleted file mode 100644 index 36368c948..000000000 --- a/internal/cli/cmd/backupconfig/backup_config.go +++ /dev/null @@ -1,64 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 backupconfig - -import ( - "github.com/spf13/cobra" - "k8s.io/cli-runtime/pkg/genericclioptions" - cmdutil "k8s.io/kubectl/pkg/cmd/util" - "k8s.io/kubectl/pkg/util/templates" - - "github.com/apecloud/kubeblocks/internal/cli/cmd/kubeblocks" - "github.com/apecloud/kubeblocks/internal/cli/util" - "github.com/apecloud/kubeblocks/internal/cli/util/helm" -) - -var backupConfigExample = templates.Examples(` - # Enable the snapshot-controller and volume snapshot, to support snapshot backup. - kbcli backup-config --set snapshot-controller.enabled=true - - # If you have already installed a snapshot-controller, only enable the snapshot backup feature - kbcli backup-config --set dataProtection.enableVolumeSnapshot=true - - # Schedule automatic backup at 18:00 every day (UTC timezone) - kbcli backup-config --set dataProtection.backupSchedule="0 18 * * *" - - # Set automatic backup retention for 7 days - kbcli backup-config --set dataProtection.backupTTL="168h0m0s" - `) - -// NewBackupConfigCmd creates the backup-config command -func NewBackupConfigCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := &kubeblocks.InstallOptions{ - Options: kubeblocks.Options{ - IOStreams: streams, - }, - } - - cmd := &cobra.Command{ - Use: "backup-config", - Short: "KubeBlocks backup config.", - Example: backupConfigExample, - Args: cobra.NoArgs, - Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(o.Complete(f, cmd)) - util.CheckErr(o.Upgrade()) - }, - } - helm.AddValueOptionsFlags(cmd.Flags(), &o.ValueOpts) - return cmd -} diff --git a/internal/cli/cmd/backupconfig/backup_config_test.go b/internal/cli/cmd/backupconfig/backup_config_test.go deleted file mode 100644 index d67a57fe7..000000000 --- a/internal/cli/cmd/backupconfig/backup_config_test.go +++ /dev/null @@ -1,79 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 backupconfig - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - "helm.sh/helm/v3/pkg/cli/values" - appsv1 "k8s.io/api/apps/v1" - "k8s.io/cli-runtime/pkg/genericclioptions" - clientfake "k8s.io/client-go/rest/fake" - cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - - "github.com/apecloud/kubeblocks/internal/cli/cmd/kubeblocks" - "github.com/apecloud/kubeblocks/internal/cli/testing" - "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/cli/util/helm" - "github.com/apecloud/kubeblocks/version" -) - -var _ = Describe("backupconfig", func() { - var streams genericclioptions.IOStreams - var tf *cmdtesting.TestFactory - - BeforeEach(func() { - streams, _, _, _ = genericclioptions.NewTestIOStreams() - tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) - tf.Client = &clientfake.RESTClient{} - - // use a fake URL to test - types.KubeBlocksChartName = testing.KubeBlocksChartName - types.KubeBlocksChartURL = testing.KubeBlocksChartURL - }) - - AfterEach(func() { - tf.Cleanup() - }) - - It("run cmd", func() { - mockDeploy := func() *appsv1.Deployment { - deploy := &appsv1.Deployment{} - deploy.SetLabels(map[string]string{ - "app.kubernetes.io/name": types.KubeBlocksChartName, - "app.kubernetes.io/version": "0.3.0", - }) - return deploy - } - - o := &kubeblocks.InstallOptions{ - Options: kubeblocks.Options{ - IOStreams: streams, - HelmCfg: helm.NewFakeConfig(testing.Namespace), - Namespace: "default", - Client: testing.FakeClientSet(mockDeploy()), - Dynamic: testing.FakeDynamicClient(), - }, - Version: version.DefaultKubeBlocksVersion, - Monitor: true, - ValueOpts: values.Options{Values: []string{"snapshot-controller.enabled=true"}}, - } - cmd := NewBackupConfigCmd(tf, streams) - Expect(cmd).ShouldNot(BeNil()) - Expect(o.Install()).Should(Succeed()) - }) -}) diff --git a/internal/cli/cmd/backupconfig/suite_test.go b/internal/cli/cmd/backupconfig/suite_test.go deleted file mode 100644 index 92317831c..000000000 --- a/internal/cli/cmd/backupconfig/suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 backupconfig - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestApps(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "BackupConfig Suite") -} diff --git a/internal/cli/cmd/bench/bench.go b/internal/cli/cmd/bench/bench.go index b5f6a6620..3c6f0d2e7 100644 --- a/internal/cli/cmd/bench/bench.go +++ b/internal/cli/cmd/bench/bench.go @@ -1,110 +1,99 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package bench import ( - "context" - "database/sql" + "embed" "fmt" - "os" - "os/signal" - "syscall" - "time" "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" ) var ( - dbName string - host string - port int - user string - password string - threads int - driver string - totalTime time.Duration - totalCount int - dropData bool - ignoreError bool - outputInterval time.Duration - isolationLevel int - silence bool - maxProcs int - - globalDB *sql.DB - globalCtx context.Context + //go:embed template/* + cueTemplate embed.FS +) + +const ( + CueSysBenchTemplateName = "bench_sysbench_template.cue" ) +type BenchBaseOptions struct { + Driver string `json:"driver"` + Database string `json:"database"` + Host string `json:"host"` + Port int `json:"port"` + User string `json:"user"` + Password string `json:"password"` +} + +func (o *BenchBaseOptions) BaseValidate() error { + if o.Driver == "" { + return fmt.Errorf("driver is required") + } + + if o.Database == "" { + return fmt.Errorf("database is required") + } + + if o.Host == "" { + return fmt.Errorf("host is required") + } + + if o.Port == 0 { + return fmt.Errorf("port is required") + } + + if o.User == "" { + return fmt.Errorf("user is required") + } + + if o.Password == "" { + return fmt.Errorf("password is required") + } + + return nil +} + +func (o *BenchBaseOptions) AddFlags(cmd *cobra.Command) { + cmd.PersistentFlags().StringVar(&o.Driver, "driver", "", "database driver") + cmd.PersistentFlags().StringVar(&o.Database, "database", "", "database name") + cmd.PersistentFlags().StringVar(&o.Host, "host", "", "the host of database") + cmd.PersistentFlags().StringVar(&o.User, "user", "", "the user of database") + cmd.PersistentFlags().StringVar(&o.Password, "password", "", "the password of database") + cmd.PersistentFlags().IntVar(&o.Port, "port", 0, "the port of database") +} + // NewBenchCmd creates the bench command -func NewBenchCmd() *cobra.Command { +func NewBenchCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "bench", Short: "Run a benchmark.", } - cmd.PersistentFlags().IntVar(&maxProcs, "max-procs", 0, "runtime.GOMAXPROCS") - cmd.PersistentFlags().StringVarP(&dbName, "db", "D", "kb_test", "Database name") - cmd.PersistentFlags().StringVarP(&host, "host", "H", "127.0.0.1", "Database host") - cmd.PersistentFlags().StringVarP(&user, "user", "U", "root", "Database user") - cmd.PersistentFlags().StringVarP(&password, "password", "p", "sakila", "Database password") - cmd.PersistentFlags().IntVarP(&port, "port", "P", 3306, "Database port") - cmd.PersistentFlags().IntVarP(&threads, "threads", "T", 1, "Thread concurrency") - cmd.PersistentFlags().StringVarP(&driver, "driver", "d", mysqlDriver, "Database driver: mysql") - cmd.PersistentFlags().DurationVar(&totalTime, "time", 1<<63-1, "Total execution time") - cmd.PersistentFlags().IntVar(&totalCount, "count", 0, "Total execution count, 0 means infinite") - cmd.PersistentFlags().BoolVar(&dropData, "dropdata", false, "Cleanup data before prepare") - cmd.PersistentFlags().BoolVar(&ignoreError, "ignore-error", false, "Ignore error when running workload") - cmd.PersistentFlags().BoolVar(&silence, "silence", false, "Don't print error when running workload") - cmd.PersistentFlags().DurationVar(&outputInterval, "interval", 5*time.Second, "Output interval time") - cobra.EnablePrefixMatching = true - // add subcommands cmd.AddCommand( - NewTpccCmd(), + NewSysBenchCmd(f, streams), ) - var cancel context.CancelFunc - globalCtx, cancel = context.WithCancel(context.Background()) - - sc := make(chan os.Signal, 1) - signal.Notify(sc, - syscall.SIGHUP, - syscall.SIGINT, - syscall.SIGTERM, - syscall.SIGQUIT) - - closeDone := make(chan struct{}, 1) - go func() { - sig := <-sc - fmt.Printf("\nGot signal [%v] to exit.\n", sig) - cancel() - - select { - case <-sc: - // send signal again, return directly - fmt.Printf("\nGot signal [%v] again to exit.\n", sig) - os.Exit(1) - case <-time.After(10 * time.Second): - fmt.Print("\nWait 10s for closed, force exit\n") - os.Exit(1) - case <-closeDone: - return - } - }() - return cmd } diff --git a/internal/cli/cmd/bench/bench_test.go b/internal/cli/cmd/bench/bench_test.go index 4db5fd80c..3cf60f069 100644 --- a/internal/cli/cmd/bench/bench_test.go +++ b/internal/cli/cmd/bench/bench_test.go @@ -1,64 +1,130 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package bench import ( - "context" - "time" + "fmt" + "net/http" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/pingcap/go-tpc/tpcc" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/types" ) var _ = Describe("bench", func() { + const ( + namespace = "default" + clusterName = "test" + ) + + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + cluster = testing.FakeCluster(clusterName, namespace) + pods = testing.FakePods(3, namespace, clusterName) + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(namespace) + tf.Client = &clientfake.RESTClient{} + tf.FakeDynamicClient = testing.FakeDynamicClient() + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + httpResp := func(obj runtime.Object) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} + } + + tf.UnstructuredClient = &clientfake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + urlPrefix := "/api/v1/namespaces/" + namespace + mapping := map[string]*http.Response{ + "/api/v1/nodes/" + testing.NodeName: httpResp(testing.FakeNode()), + urlPrefix + "/services": httpResp(&corev1.ServiceList{}), + urlPrefix + "/events": httpResp(&corev1.EventList{}), + urlPrefix + "/persistentvolumeclaims": httpResp(&corev1.PersistentVolumeClaimList{}), + urlPrefix + "/pods": httpResp(pods), + } + return mapping[req.URL.Path], nil + }), + } + + tf.Client = tf.UnstructuredClient + tf.FakeDynamicClient = testing.FakeDynamicClient(cluster, testing.FakeClusterDef(), testing.FakeClusterVersion()) + }) + + AfterEach(func() { + tf.Cleanup() + }) + It("bench command", func() { - cmd := NewBenchCmd() + cmd := NewBenchCmd(tf, streams) Expect(cmd != nil).Should(BeTrue()) Expect(cmd.HasSubCommands()).Should(BeTrue()) }) - It("tpcc command", func() { - cmd := NewTpccCmd() + It("sysbench command", func() { + cmd := NewSysBenchCmd(tf, streams) Expect(cmd != nil).Should(BeTrue()) Expect(cmd.HasSubCommands()).Should(BeTrue()) - - cmd = newPrepareCmd() - Expect(cmd != nil).Should(BeTrue()) - Expect(cmd.RunE(cmd, []string{})).Should(HaveOccurred()) - - cmd = newRunCmd() - Expect(cmd != nil).Should(BeTrue()) - Expect(cmd.RunE(cmd, []string{})).Should(HaveOccurred()) - - cmd = newCleanCmd() - Expect(cmd != nil).Should(BeTrue()) - Expect(cmd.RunE(cmd, []string{})).Should(HaveOccurred()) }) - It("internal functions", func() { - outputInterval = 120 * time.Second - executeWorkload(context.Background(), &tpcc.Workloader{}, 1, "prepare") + It("test sysbench run", func() { + o := &SysBenchOptions{ + BenchBaseOptions: BenchBaseOptions{ + Driver: "test", + Database: "test", + Host: "svc-1", + Port: 3306, + User: "test", + Password: "test", + }, + Mode: "prepare", + Type: "oltp_read_write_pct", + Tables: 1, + Size: 100, + Times: 1, + factory: tf, + IOStreams: streams, + } + Expect(o.Complete(clusterName)).Should(BeNil()) + Expect(o.Validate()).Should(BeNil()) + Expect(o.Run()).Should(BeNil()) }) - It("util", func() { - Expect(openDB()).Should(HaveOccurred()) - Expect(ping()).Should(Succeed()) - Expect(createDB()).Should(HaveOccurred()) - Expect(closeDB()).Should(Succeed()) + It("parse driver and endpoint", func() { + driver, host, port, err := getDriverAndHostAndPort(cluster, testing.FakeServices()) + Expect(err).Should(BeNil()) + Expect(driver).Should(Equal(testing.ComponentName)) + Expect(host).Should(Equal(fmt.Sprintf("svc-1.%s.svc.cluster.local", testing.Namespace))) + Expect(port).Should(Equal(3306)) }) }) diff --git a/internal/cli/cmd/bench/suite_test.go b/internal/cli/cmd/bench/suite_test.go index fae3f9201..61c76790c 100644 --- a/internal/cli/cmd/bench/suite_test.go +++ b/internal/cli/cmd/bench/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package bench diff --git a/internal/cli/cmd/bench/sysbench.go b/internal/cli/cmd/bench/sysbench.go new file mode 100644 index 000000000..cf2c7bfc1 --- /dev/null +++ b/internal/cli/cmd/bench/sysbench.go @@ -0,0 +1,305 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package bench + +import ( + "context" + "encoding/json" + "fmt" + "net" + "strconv" + + "github.com/leaanthony/debme" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + clientset "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/cluster" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +const ( + prepareOperation = "prepare" + runOperation = "run" + cleanupOperation = "cleanup" +) + +var ( + driverMap = map[string]string{ + "mysql": "mysql", + "postgresql": "pgsql", + } +) + +type SysBenchOptions struct { + factory cmdutil.Factory + client clientset.Interface + dynamic dynamic.Interface + namespace string + + Mode string `json:"mode"` + Type string `json:"type"` + Size int `json:"size"` + Tables int `json:"tables"` + Times int `json:"times"` + + BenchBaseOptions + *cluster.ClusterObjects `json:"-"` + genericclioptions.IOStreams `json:"-"` +} + +func (o *SysBenchOptions) Complete(name string) error { + var err error + o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + if o.dynamic, err = o.factory.DynamicClient(); err != nil { + return err + } + + if o.client, err = o.factory.KubernetesClientSet(); err != nil { + return err + } + + if o.Driver == "" || o.Host == "" || o.Port == 0 { + clusterGetter := cluster.ObjectsGetter{ + Client: o.client, + Dynamic: o.dynamic, + Name: name, + Namespace: o.namespace, + GetOptions: cluster.GetOptions{ + WithClusterDef: true, + WithService: true, + WithPod: true, + WithEvent: true, + WithPVC: true, + WithDataProtection: true, + }, + } + if o.ClusterObjects, err = clusterGetter.Get(); err != nil { + return err + } + o.Driver, o.Host, o.Port, err = getDriverAndHostAndPort(o.Cluster, o.Services) + if err != nil { + return err + } + if driver, ok := driverMap[o.Driver]; ok { + o.Driver = driver + } else { + return fmt.Errorf("unsupported driver %s", o.Driver) + } + } + + return nil +} + +func (o *SysBenchOptions) Validate() error { + if err := o.BaseValidate(); err != nil { + return err + } + + if o.Mode == "" { + return fmt.Errorf("mode is required") + } + + if o.Type == "" { + return fmt.Errorf("type is required") + } + + if o.Tables <= 0 { + return fmt.Errorf("tables must be greater than 0") + } + + if o.Times <= 0 { + return fmt.Errorf("times must be greater than 0") + } + + return nil +} + +func (o *SysBenchOptions) PreCreate(obj *unstructured.Unstructured) error { + p := &corev1.Pod{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, p); err != nil { + return err + } + + data, err := runtime.DefaultUnstructuredConverter.ToUnstructured(p) + if err != nil { + return err + } + obj.SetUnstructuredContent(data) + return nil +} + +func (o *SysBenchOptions) Run() error { + var ( + err error + unstructureObj *unstructured.Unstructured + optionsByte []byte + ) + + if optionsByte, err = json.Marshal(o); err != nil { + return err + } + + cueFS, _ := debme.FS(cueTemplate, "template") + cueTpl, err := intctrlutil.NewCUETplFromBytes(cueFS.ReadFile(CueSysBenchTemplateName)) + if err != nil { + return err + } + cueValue := intctrlutil.NewCUEBuilder(*cueTpl) + if err := cueValue.Fill("options", optionsByte); err != nil { + return err + } + if unstructureObj, err = cueValue.ConvertContentToUnstructured("content"); err != nil { + return err + } + + if _, err := o.dynamic.Resource(types.PodGVR()).Namespace(o.namespace).Create(context.Background(), unstructureObj, metav1.CreateOptions{}); err != nil { + return err + } + + return nil +} + +func NewSysBenchCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := &SysBenchOptions{ + factory: f, + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "sysbench", + Short: "run a SysBench benchmark", + } + + cmd.PersistentFlags().StringVar(&o.Type, "type", "oltp_read_write_pct", "sysbench type") + cmd.PersistentFlags().IntVar(&o.Size, "size", 20000, "the data size of per table") + cmd.PersistentFlags().IntVar(&o.Tables, "tables", 10, "the number of tables") + cmd.PersistentFlags().IntVar(&o.Times, "times", 100, "the number of test times") + o.BenchBaseOptions.AddFlags(cmd) + + cmd.AddCommand(newPrepareCmd(f, o), newRunCmd(f, o), newCleanCmd(f, o)) + + return cmd +} + +func newPrepareCmd(f cmdutil.Factory, o *SysBenchOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "prepare [NAME]", + Short: "Prepare the data of SysBench for a cluster", + Args: cobra.ExactArgs(1), + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(executeSysBench(o, args[0], prepareOperation)) + }, + } + return cmd +} + +func newRunCmd(f cmdutil.Factory, o *SysBenchOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "run [NAME]", + Short: "Run SysBench on cluster", + Args: cobra.ExactArgs(1), + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(executeSysBench(o, args[0], runOperation)) + }, + } + return cmd +} + +func newCleanCmd(f cmdutil.Factory, o *SysBenchOptions) *cobra.Command { + cmd := &cobra.Command{ + Use: "cleanup [NAME]", + Short: "Cleanup the data of SysBench for cluster", + Args: cobra.ExactArgs(1), + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(executeSysBench(o, args[0], cleanupOperation)) + }, + } + return cmd +} + +func executeSysBench(o *SysBenchOptions, name string, mode string) error { + o.Mode = mode + if err := o.Complete(name); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + return nil +} + +func getDriverAndHostAndPort(c *appsv1alpha1.Cluster, svcList *corev1.ServiceList) (driver string, host string, port int, err error) { + var internalEndpoints []string + var externalEndpoints []string + + if c == nil { + return "", "", 0, fmt.Errorf("cluster is nil") + } + + for _, comp := range c.Spec.ComponentSpecs { + driver = comp.Name + internalEndpoints, externalEndpoints = cluster.GetComponentEndpoints(svcList, &comp) + if len(internalEndpoints) > 0 || len(externalEndpoints) > 0 { + break + } + } + switch { + case len(internalEndpoints) > 0: + host, port, err = parseHostAndPort(internalEndpoints[0]) + case len(externalEndpoints) > 0: + host, port, err = parseHostAndPort(externalEndpoints[0]) + default: + err = fmt.Errorf("no endpoints found") + } + + return +} + +func parseHostAndPort(s string) (string, int, error) { + host, port, err := net.SplitHostPort(s) + if err != nil { + return "", 0, err + } + portInt, err := strconv.Atoi(port) + if err != nil { + return "", 0, err + } + return host, portInt, nil +} diff --git a/internal/cli/cmd/bench/template/bench_sysbench_template.cue b/internal/cli/cmd/bench/template/bench_sysbench_template.cue new file mode 100644 index 000000000..9470569d8 --- /dev/null +++ b/internal/cli/cmd/bench/template/bench_sysbench_template.cue @@ -0,0 +1,63 @@ +//Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +//This file is part of KubeBlocks project +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + driver: string + database: string + host: string + port: int + user: string + password: string + mode: string + type: string + tables: int + times: int +} + +// required, k8s api resource content +content: { + apiVersion: "v1" + kind: "Pod" + metadata: { + namespace: "default" + generateName: "test-sysbench-prepare-\(options.driver)-" + } + spec: { + containers: [ + { + name: "test-sysbench" + image: "registry.cn-hangzhou.aliyuncs.com/apecloud/customsuites:latest" + env: [ + { + name: "TYPE" + value: "2" + }, + { + name: "FLAG" + value: "0" + }, + { + name: "CONFIGS" + value: "mode:\(options.mode),driver:\(options.driver),host:\(options.host),user:\(options.user),password:\(options.password),port:\(options.port),db:\(options.database),size:\(options.size),tables:\(options.tables),times:\(options.times),type:\(options.type)" + }, + ] + }, + ] + restartPolicy: "Never" + } +} diff --git a/internal/cli/cmd/bench/tpcc.go b/internal/cli/cmd/bench/tpcc.go deleted file mode 100644 index fac7c0560..000000000 --- a/internal/cli/cmd/bench/tpcc.go +++ /dev/null @@ -1,228 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 bench - -import ( - "context" - "fmt" - "os" - "runtime" - "sync" - "time" - - "github.com/pingcap/go-tpc/pkg/measurement" - "github.com/pingcap/go-tpc/pkg/workload" - "github.com/pingcap/go-tpc/tpcc" - "github.com/pkg/errors" - "github.com/spf13/cobra" -) - -var tpccConfig tpcc.Config - -func NewTpccCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "tpcc", - Short: "Run a TPCC benchmark.", - } - - cmd.PersistentFlags().IntVar(&tpccConfig.Parts, "parts", 1, "Number to partition warehouses") - cmd.PersistentFlags().IntVar(&tpccConfig.PartitionType, "partition-type", 1, "Partition type (1 - HASH, 2 - RANGE, 3 - LIST (like HASH), 4 - LIST (like RANGE)") - cmd.PersistentFlags().IntVar(&tpccConfig.Warehouses, "warehouses", 4, "Number of warehouses") - cmd.PersistentFlags().BoolVar(&tpccConfig.CheckAll, "check-all", false, "Run all consistency checks") - - // add subcommands - cmd.AddCommand(newPrepareCmd(), newRunCmd(), newCleanCmd()) - return cmd -} - -func newPrepareCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "prepare", - Short: "Prepare data for TPCC.", - RunE: func(cmd *cobra.Command, args []string) error { - return executeTpcc("prepare") - }, - } - - cmd.Flags().BoolVar(&tpccConfig.NoCheck, "no-check", false, "TPCC prepare check, default false") - cmd.Flags().StringVar(&tpccConfig.OutputType, "output-type", "", "Output file type."+ - " If empty, then load data to db. Current only support csv") - cmd.Flags().StringVar(&tpccConfig.OutputDir, "output-dir", "", "Output directory for generating file if specified") - cmd.Flags().StringVar(&tpccConfig.SpecifiedTables, "tables", "", "Specified tables for "+ - "generating file, separated by ','. Valid only if output is set. If this flag is not set, generate all tables by default") - cmd.Flags().IntVar(&tpccConfig.PrepareRetryCount, "retry-count", 50, "Retry count when errors occur") - cmd.Flags().DurationVar(&tpccConfig.PrepareRetryInterval, "retry-interval", 5*time.Second, "The interval for each retry") - return cmd -} - -func newRunCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "run", - Short: "Run workload.", - RunE: func(cmd *cobra.Command, args []string) error { - return executeTpcc("run") - }, - } - - cmd.Flags().BoolVar(&tpccConfig.Wait, "wait", false, "including keying & thinking time described on TPC-C Standard Specification") - cmd.Flags().DurationVar(&tpccConfig.MaxMeasureLatency, "max-measure-latency", measurement.DefaultMaxLatency, "max measure latency in millisecond") - cmd.Flags().IntSliceVar(&tpccConfig.Weight, "weight", []int{45, 43, 4, 4, 4}, "Weight for NewOrder, Payment, OrderStatus, Delivery, StockLevel") - - return cmd -} - -func newCleanCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "cleanup", - Short: "Cleanup data for TPCC.", - RunE: func(cmd *cobra.Command, args []string) error { - return executeTpcc("cleanup") - }, - } - return cmd -} - -func executeTpcc(action string) error { - runtime.GOMAXPROCS(maxProcs) - - var ( - w workload.Workloader - err error - ) - - err = openDB() - defer func() { _ = closeDB() }() - if err != nil { - return err - } - - tpccConfig.DBName = dbName - tpccConfig.Threads = threads - tpccConfig.Isolation = isolationLevel - - switch tpccConfig.OutputType { - case "csv", "CSV": - if tpccConfig.OutputDir == "" { - return errors.New("Output Directory cannot be empty when generating files") - } - w, err = tpcc.NewCSVWorkloader(globalDB, &tpccConfig) - default: - w, err = tpcc.NewWorkloader(globalDB, &tpccConfig) - } - - if err != nil { - return fmt.Errorf("failed to init work loader: %v", err) - } - - timeoutCtx, cancel := context.WithTimeout(globalCtx, totalTime) - defer cancel() - - executeWorkload(timeoutCtx, w, threads, action) - - fmt.Println("Finished") - w.OutputStats(true) - return nil -} - -func executeWorkload(ctx context.Context, w workload.Workloader, threads int, action string) { - var wg sync.WaitGroup - wg.Add(threads) - - outputCtx, outputCancel := context.WithCancel(ctx) - ch := make(chan struct{}, 1) - go func() { - ticker := time.NewTicker(outputInterval) - defer ticker.Stop() - - for { - select { - case <-outputCtx.Done(): - ch <- struct{}{} - return - case <-ticker.C: - w.OutputStats(false) - } - } - }() - - for i := 0; i < threads; i++ { - go func(index int) { - defer wg.Done() - if err := execute(ctx, w, action, threads, index); err != nil { - if action == "prepare" { - panic(fmt.Sprintf("a fatal occurred when preparing data: %v", err)) - } - fmt.Printf("execute %s failed, err %v\n", action, err) - return - } - }(i) - } - - wg.Wait() - outputCancel() - - <-ch -} - -func execute(ctx context.Context, w workload.Workloader, action string, threads, index int) error { - var err error - count := totalCount / threads - ctx = w.InitThread(ctx, index) - defer w.CleanupThread(ctx, index) - - defer func() { - if recover() != nil { - fmt.Fprintln(os.Stdout, "Unexpected error") - } - }() - - switch action { - case "prepare": - // Do cleanup only if dropData is set and not generate csv data. - if dropData { - if err := w.Cleanup(ctx, index); err != nil { - return err - } - } - err = w.Prepare(ctx, index) - case "cleanup": - err = w.Cleanup(ctx, index) - case "check": - err = w.Check(ctx, index) - } - - for i := 0; i < count || count <= 0; i++ { - err := w.Run(ctx, index) - - select { - case <-ctx.Done(): - return nil - default: - } - - if err != nil { - if !silence { - fmt.Printf("[%s] execute %s failed, err %v\n", time.Now().Format("2006-01-02 15:04:05"), action, err) - } - if !ignoreError { - return err - } - } - } - - return err -} diff --git a/internal/cli/cmd/bench/util.go b/internal/cli/cmd/bench/util.go index 2e91334e2..38a821692 100644 --- a/internal/cli/cmd/bench/util.go +++ b/internal/cli/cmd/bench/util.go @@ -1,80 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package bench - -import ( - "database/sql" - "fmt" - "strings" - - _ "github.com/go-sql-driver/mysql" -) - -const ( - unknownDB = "Unknown database" - createDBDDL = "CREATE DATABASE IF NOT EXISTS " - mysqlDriver = "mysql" -) - -func openDB() error { - var ( - err error - ds = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s", user, password, host, port, dbName) - ) - - // allow multiple statements in one query to allow q15 on the TPC-H - fullDsn := fmt.Sprintf("%s?multiStatements=true", ds) - globalDB, err = sql.Open(mysqlDriver, fullDsn) - if err != nil { - return err - } - - return ping() -} - -func ping() error { - if globalDB == nil { - return nil - } - if err := globalDB.Ping(); err != nil { - errString := err.Error() - if strings.Contains(errString, unknownDB) { - return createDB() - } else { - globalDB = nil - } - return err - } - return nil -} - -func createDB() error { - tmpDs := fmt.Sprintf("%s:%s@tcp(%s:%d)/", user, password, host, port) - tmpDB, _ := sql.Open(mysqlDriver, tmpDs) - defer tmpDB.Close() - if _, err := tmpDB.Exec(createDBDDL + dbName); err != nil { - return fmt.Errorf("failed to create database, err %v", err) - } - return nil -} - -func closeDB() error { - if globalDB == nil { - return nil - } - return globalDB.Close() -} diff --git a/internal/cli/cmd/builder/builder.go b/internal/cli/cmd/builder/builder.go new file mode 100644 index 000000000..815e17769 --- /dev/null +++ b/internal/cli/cmd/builder/builder.go @@ -0,0 +1,40 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/builder/template" +) + +// NewBuilderCmd for builder functions +func NewBuilderCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "builder", + Short: "builder command.", + } + cmd.AddCommand( + template.NewComponentTemplateRenderCmd(f, streams), + ) + return cmd +} diff --git a/internal/cli/cmd/builder/builder_test.go b/internal/cli/cmd/builder/builder_test.go new file mode 100644 index 000000000..ff8aa23af --- /dev/null +++ b/internal/cli/cmd/builder/builder_test.go @@ -0,0 +1,47 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("builder", func() { + var streams genericclioptions.IOStreams + var tf *cmdtesting.TestFactory + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory() + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("command should succeed", func() { + cmd := NewBuilderCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) +}) diff --git a/internal/cli/cmd/builder/suite_test.go b/internal/cli/cmd/builder/suite_test.go new file mode 100644 index 000000000..e1e9007da --- /dev/null +++ b/internal/cli/cmd/builder/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAppp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Builder Cmd Test Suite") +} diff --git a/internal/cli/cmd/builder/template/helm_helper.go b/internal/cli/cmd/builder/template/helm_helper.go new file mode 100644 index 000000000..ff2c75f9b --- /dev/null +++ b/internal/cli/cmd/builder/template/helm_helper.go @@ -0,0 +1,122 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "bytes" + "os" + "path/filepath" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/generics" +) + +func scanDirectoryPath(rootPath string) ([]string, error) { + dirs, err := os.ReadDir(rootPath) + if err != nil { + return nil, err + } + resourceList := make([]string, 0) + for _, d := range dirs { + if d.IsDir() { + subDirectory, err := scanDirectoryPath(filepath.Join(rootPath, d.Name())) + if err != nil { + return nil, err + } + resourceList = append(resourceList, subDirectory...) + continue + } + if filepath.Ext(d.Name()) != ".yaml" { + continue + } + resourceList = append(resourceList, filepath.Join(rootPath, d.Name())) + } + return resourceList, nil +} + +func getResourceMeta(yamlBytes []byte) (metav1.TypeMeta, error) { + type k8sObj struct { + metav1.TypeMeta `json:",inline"` + } + var o k8sObj + err := yaml.Unmarshal(yamlBytes, &o) + if err != nil { + return metav1.TypeMeta{}, err + } + return o.TypeMeta, nil +} + +func CreateObjectsFromDirectory(rootPath string) ([]client.Object, error) { + allObjs := make([]client.Object, 0) + + // create cr from yaml + resourceList, err := scanDirectoryPath(rootPath) + if err != nil { + return nil, err + } + for _, resourceFile := range resourceList { + yamlBytes, err := os.ReadFile(resourceFile) + if err != nil { + return nil, err + } + objects, err := createObjectsFromYaml(yamlBytes) + if err != nil { + return nil, err + } + allObjs = append(allObjs, objects...) + } + return allObjs, nil +} + +func createObjectsFromYaml(yamlBytes []byte) ([]client.Object, error) { + objects := make([]client.Object, 0) + for _, doc := range bytes.Split(yamlBytes, []byte("---")) { + if len(bytes.TrimSpace(doc)) == 0 { + continue + } + meta, err := getResourceMeta(doc) + if err != nil { + return nil, err + } + switch meta.Kind { + case kindFromResource(corev1.ConfigMap{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.ConfigMapSignature)) + case kindFromResource(corev1.Secret{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.SecretSignature)) + case kindFromResource(appsv1alpha1.ConfigConstraint{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.ConfigConstraintSignature)) + case kindFromResource(appsv1alpha1.ClusterDefinition{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.ClusterDefinitionSignature)) + case kindFromResource(appsv1alpha1.ClusterVersion{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.ClusterVersionSignature)) + case kindFromResource(appsv1alpha1.BackupPolicyTemplate{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.BackupPolicyTemplateSignature)) + case kindFromResource(dataprotectionv1alpha1.BackupTool{}): + objects = append(objects, CreateTypedObjectFromYamlByte(doc, generics.BackupToolSignature)) + } + } + return objects, nil +} diff --git a/internal/cli/cmd/builder/template/k8s_resource.go b/internal/cli/cmd/builder/template/k8s_resource.go new file mode 100644 index 000000000..c43a16bd2 --- /dev/null +++ b/internal/cli/cmd/builder/template/k8s_resource.go @@ -0,0 +1,77 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "bytes" + "os" + + "k8s.io/apimachinery/pkg/util/yaml" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/internal/generics" +) + +func CustomizedObjFromYaml[T generics.Object, PT generics.PObject[T], + L generics.ObjList[T]](filePath string, signature func(T, L)) (PT, error) { + objList, err := CustomizedObjectListFromYaml[T, PT, L](filePath, signature) + if err != nil { + return nil, err + } + if len(objList) == 0 { + return nil, nil + } + return objList[0], nil +} + +func CustomizedObjectListFromYaml[T generics.Object, PT generics.PObject[T], + L generics.ObjList[T]](yamlfile string, signature func(T, L)) ([]PT, error) { + objBytes, err := os.ReadFile(yamlfile) + if err != nil { + return nil, err + } + objList := make([]PT, 0) + for _, doc := range bytes.Split(objBytes, []byte("---")) { + if len(bytes.TrimSpace(doc)) == 0 { + continue + } + objList = append(objList, CreateTypedObjectFromYamlByte[T, PT, L](doc, signature)) + } + return objList, nil +} + +func CreateTypedObjectFromYamlByte[T generics.Object, PT generics.PObject[T], + L generics.ObjList[T]](yamlBytes []byte, _ func(T, L)) PT { + var obj PT + if err := yaml.Unmarshal(yamlBytes, &obj); err != nil { + return nil + } + return obj +} + +func GetTypedResourceObjectBySignature[T generics.Object, PT generics.PObject[T], + L generics.ObjList[T]](objects []client.Object, _ func(T, L)) PT { + for _, object := range objects { + if cd, ok := object.(PT); ok { + return cd + } + } + return nil +} diff --git a/internal/cli/cmd/builder/template/mock_client.go b/internal/cli/cmd/builder/template/mock_client.go new file mode 100644 index 000000000..d4842019c --- /dev/null +++ b/internal/cli/cmd/builder/template/mock_client.go @@ -0,0 +1,125 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + testutil "github.com/apecloud/kubeblocks/internal/testutil/k8s" +) + +type mockClient struct { + objects map[client.ObjectKey]client.Object + kindObjectList map[string][]runtime.Object +} + +func newMockClient(objs []client.Object) client.Client { + return &mockClient{ + objects: fromObjects(objs), + kindObjectList: splitRuntimeObject(objs), + } +} + +func fromObjects(objs []client.Object) map[client.ObjectKey]client.Object { + r := make(map[client.ObjectKey]client.Object) + for _, obj := range objs { + if obj != nil { + r[client.ObjectKeyFromObject(obj)] = obj + } + } + return r +} + +func splitRuntimeObject(objects []client.Object) map[string][]runtime.Object { + r := make(map[string][]runtime.Object) + for _, object := range objects { + kind := object.GetObjectKind().GroupVersionKind().Kind + if _, ok := r[kind]; !ok { + r[kind] = make([]runtime.Object, 0) + } + r[kind] = append(r[kind], object) + } + return r +} + +func (m *mockClient) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error { + objKey := key + if object, ok := m.objects[objKey]; ok { + testutil.SetGetReturnedObject(obj, object) + return nil + } + objKey.Namespace = "" + if object, ok := m.objects[objKey]; ok { + testutil.SetGetReturnedObject(obj, object) + return nil + } + return apierrors.NewNotFound(corev1.SchemeGroupVersion.WithResource("mock_resource").GroupResource(), key.String()) +} + +func (m *mockClient) List(ctx context.Context, list client.ObjectList, opts ...client.ListOption) error { + r := m.kindObjectList[list.GetObjectKind().GroupVersionKind().Kind] + if r != nil { + return testutil.SetListReturnedObjects(list, r) + } + return nil +} + +func (m mockClient) Create(ctx context.Context, obj client.Object, opts ...client.CreateOption) error { + return nil +} + +func (m mockClient) Delete(ctx context.Context, obj client.Object, opts ...client.DeleteOption) error { + return nil +} + +func (m mockClient) Update(ctx context.Context, obj client.Object, opts ...client.UpdateOption) error { + return nil +} + +func (m mockClient) Patch(ctx context.Context, obj client.Object, patch client.Patch, opts ...client.PatchOption) error { + return nil +} + +func (m mockClient) DeleteAllOf(ctx context.Context, obj client.Object, opts ...client.DeleteAllOfOption) error { + return cfgcore.MakeError("not support") +} + +func (m mockClient) Status() client.SubResourceWriter { + panic("implement me") +} + +func (m mockClient) SubResource(subResource string) client.SubResourceClient { + panic("implement me") +} + +func (m mockClient) Scheme() *runtime.Scheme { + panic("implement me") +} + +func (m mockClient) RESTMapper() meta.RESTMapper { + panic("implement me") +} diff --git a/internal/cli/cmd/builder/template/suite_test.go b/internal/cli/cmd/builder/template/suite_test.go new file mode 100644 index 000000000..96fd5542f --- /dev/null +++ b/internal/cli/cmd/builder/template/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAppp(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Template Cmd Test Suite") +} diff --git a/internal/cli/cmd/builder/template/template.go b/internal/cli/cmd/builder/template/template.go new file mode 100644 index 000000000..dd941a20b --- /dev/null +++ b/internal/cli/cmd/builder/template/template.go @@ -0,0 +1,140 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "os" + "path/filepath" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/util" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" +) + +type renderTPLCmdOpts struct { + genericclioptions.IOStreams + + Factory cmdutil.Factory + // dynamic dynamic.Interface + + clusterYaml string + clusterDefYaml string + + outputDir string + clearOutputDir bool + helmOutputDir string + helmTemplateDir string + + opts RenderedOptions +} + +func (o *renderTPLCmdOpts) complete() error { + if err := o.checkAndHelmTemplate(); err != nil { + return err + } + + if o.helmOutputDir == "" { + return cfgcore.MakeError("helm template dir is empty") + } + + if o.clearOutputDir && o.outputDir != "" { + _ = os.RemoveAll(o.outputDir) + } + if o.outputDir == "" { + o.outputDir = filepath.Join("./output", RandomString(6)) + } + return nil +} + +func (o *renderTPLCmdOpts) run() error { + workflow, err := NewWorkflowTemplateRender(o.helmOutputDir, o.opts) + if err != nil { + return err + } + return workflow.Do(o.outputDir) +} + +var templateExamples = templates.Examples(` + # builder template: Provides a mechanism to rendered template for ComponentConfigSpec and ComponentScriptSpec in the ClusterComponentDefinition. + # builder template --helm deploy/redis --memory=64Gi --cpu=16 --replicas=3 --component-name=redis --config-spec=redis-replication-config + + # build all configspec + kbcli builder template --helm deploy/redis -a +`) + +// buildReconfigureCommonFlags build common flags for reconfigure command +func (o *renderTPLCmdOpts) buildTemplateFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.clusterYaml, "cluster", "", "the cluster yaml file") + cmd.Flags().StringVar(&o.clusterDefYaml, "cluster-definition", "", "the cluster definition yaml file") + cmd.Flags().StringVarP(&o.outputDir, "output-dir", "o", "", "specify the output directory") + + cmd.Flags().StringVar(&o.opts.ConfigSpec, "config-spec", "", "specify the config spec to be rendered") + cmd.Flags().BoolVarP(&o.opts.AllConfigSpecs, "all", "a", false, "template all config specs") + + // mock cluster object + cmd.Flags().Int32VarP(&o.opts.Replicas, "replicas", "r", 1, "specify the replicas of the component") + cmd.Flags().StringVar(&o.opts.DataVolumeName, "volume-name", "", "specify the data volume name of the component") + cmd.Flags().StringVar(&o.opts.ComponentName, "component-name", "", "specify the component name of the clusterdefinition") + cmd.Flags().StringVar(&o.helmTemplateDir, "helm", "", "specify the helm template dir") + cmd.Flags().StringVar(&o.helmOutputDir, "helm-output", "", "specify the helm template output dir") + cmd.Flags().StringVar(&o.opts.CPU, "cpu", "", "specify the cpu of the component") + cmd.Flags().StringVar(&o.opts.Memory, "memory", "", "specify the memory of the component") + cmd.Flags().BoolVar(&o.clearOutputDir, "clean", false, "specify whether to clear the output dir") +} + +func (o *renderTPLCmdOpts) checkAndHelmTemplate() error { + if o.helmTemplateDir == "" || o.helmOutputDir != "" { + return nil + } + + if o.helmTemplateDir != "" && o.helmOutputDir == "" { + o.helmOutputDir = filepath.Join("./helm-output", RandomString(6)) + } + + return helmTemplate(o.helmTemplateDir, o.helmOutputDir) +} + +func NewComponentTemplateRenderCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := &renderTPLCmdOpts{ + Factory: f, + IOStreams: streams, + opts: RenderedOptions{ + // for mock cluster object + Namespace: "default", + Name: "cluster-" + RandomString(6), + }, + } + cmd := &cobra.Command{ + Use: "template", + Aliases: []string{"tpl"}, + Short: "tpl - a developer tool integrated with KubeBlocks that can help developers quickly generate rendered configurations or scripts based on Helm templates, and discover errors in the template before creating the database cluster.", + Example: templateExamples, + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.complete()) + util.CheckErr(o.run()) + }, + } + o.buildTemplateFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/builder/template/template_test.go b/internal/cli/cmd/builder/template/template_test.go new file mode 100644 index 000000000..f1bb84aaa --- /dev/null +++ b/internal/cli/cmd/builder/template/template_test.go @@ -0,0 +1,97 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "os" + "path/filepath" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" + + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/test/testdata" +) + +var _ = Describe("template", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = testing.NewTestFactory("default") + }) + + AfterEach(func() { + tf.Cleanup() + }) + + testComponentTemplate := func(helmPath string, helmOutput string) { + cmd := NewComponentTemplateRenderCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + + if helmPath != "" { + _ = cmd.Flags().Set("helm", helmPath) + } + if helmOutput != "" { + _ = cmd.Flags().Set("helm-output", helmOutput) + } + _ = cmd.Flags().Set("memory", "8Gi") + _ = cmd.Flags().Set("cpu", "8") + _ = cmd.Flags().Set("replicas", "3") + _ = cmd.Flags().Set("all", "true") + cmd.Run(cmd, []string{}) + } + + It("should succeed", func() { + componentRootPath := testdata.SubTestDataPath("../../deploy") + testComponents := []string{ + "apecloud-mysql", + "postgresql", + "redis", + "clickhouse", + } + helmOutputRoot, err := os.MkdirTemp(os.TempDir(), "test") + Expect(err).Should(Succeed()) + defer os.RemoveAll(helmOutputRoot) + + _, err = os.ReadDir(componentRootPath) + Expect(err).Should(Succeed()) + for _, component := range testComponents { + componentPath := filepath.Join(componentRootPath, component) + _, err := os.ReadDir(componentPath) + Expect(err).Should(Succeed()) + helmOutput := filepath.Join(helmOutputRoot, component) + Expect(helmTemplate(componentPath, helmOutput)).Should(Succeed()) + testComponentTemplate(componentPath, helmOutput) + } + }) + + It("test config template render without depend on helm", func() { + testComponentTemplate(testdata.SubTestDataPath("../../deploy/apecloud-mysql"), "") + testComponentTemplate(testdata.SubTestDataPath("../../deploy/postgresql"), "") + }) +}) diff --git a/internal/cli/cmd/builder/template/util.go b/internal/cli/cmd/builder/template/util.go new file mode 100644 index 000000000..e23d81f9c --- /dev/null +++ b/internal/cli/cmd/builder/template/util.go @@ -0,0 +1,116 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "reflect" + + "github.com/sethvargo/go-password/password" + "helm.sh/helm/v3/pkg/cli/values" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/util/helm" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" + "github.com/apecloud/kubeblocks/version" +) + +type RenderedOptions struct { + ConfigSpec string + AllConfigSpecs bool + + // mock cluster object + Name string + Namespace string + + Replicas int32 + DataVolumeName string + ComponentName string + + CPU string + Memory string +} + +func mockClusterObject(clusterDefObj *appsv1alpha1.ClusterDefinition, renderedOpts RenderedOptions, clusterVersion *appsv1alpha1.ClusterVersion) *appsv1alpha1.Cluster { + cvReference := "" + if clusterVersion != nil { + cvReference = clusterVersion.Name + } + factory := testapps.NewClusterFactory(renderedOpts.Namespace, renderedOpts.Name, clusterDefObj.Name, cvReference) + for _, component := range clusterDefObj.Spec.ComponentDefs { + factory.AddComponent(component.CharacterType+"-"+RandomString(3), component.Name) + factory.SetReplicas(renderedOpts.Replicas) + if renderedOpts.DataVolumeName != "" { + pvcSpec := testapps.NewPVCSpec("10Gi") + factory.AddVolumeClaimTemplate(renderedOpts.DataVolumeName, pvcSpec) + } + if renderedOpts.CPU != "" || renderedOpts.Memory != "" { + factory.SetResources(fromResource(renderedOpts)) + } + } + return factory.GetObject() +} + +func fromResource(opts RenderedOptions) corev1.ResourceRequirements { + cpu := opts.CPU + memory := opts.Memory + if cpu == "" { + cpu = "1" + } + if memory == "" { + memory = "1Gi" + } + return corev1.ResourceRequirements{ + Limits: corev1.ResourceList{ + "cpu": resource.MustParse(cpu), + "memory": resource.MustParse(memory), + }, + } +} + +func kindFromResource[T any](resource T) string { + t := reflect.TypeOf(resource) + if t.Kind() == reflect.Ptr { + t = t.Elem() + } + return t.Name() +} + +func RandomString(n int) string { + s, _ := password.Generate(n, 0, 0, false, false) + return s +} + +func helmTemplate(helmPath string, helmOutput string) error { + o := helm.InstallOpts{ + Name: testing.KubeBlocksChartName, + Chart: helmPath, + Namespace: "default", + Version: version.DefaultKubeBlocksVersion, + + DryRun: func() *bool { r := true; return &r }(), + OutputDir: helmOutput, + ValueOpts: &values.Options{Values: []string{}}, + } + _, err := o.Install(helm.NewFakeConfig("default")) + return err +} diff --git a/internal/cli/cmd/builder/template/workflow.go b/internal/cli/cmd/builder/template/workflow.go new file mode 100644 index 000000000..f19745df6 --- /dev/null +++ b/internal/cli/cmd/builder/template/workflow.go @@ -0,0 +1,261 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package template + +import ( + "context" + "fmt" + "os" + "path/filepath" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components" + "github.com/apecloud/kubeblocks/internal/cli/printer" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/controller/builder" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" +) + +type componentedConfigSpec struct { + component string + configSpec appsv1alpha1.ComponentTemplateSpec +} + +type templateRenderWorkflow struct { + renderedOpts RenderedOptions + clusterYaml string + clusterDefObj *appsv1alpha1.ClusterDefinition + localObjects []client.Object + + clusterDefComponents []appsv1alpha1.ClusterComponentDefinition +} + +func (w *templateRenderWorkflow) Do(outputDir string) error { + var err error + var cluster *appsv1alpha1.Cluster + var configSpecs []componentedConfigSpec + + cli := newMockClient(w.localObjects) + ctx := intctrlutil.RequestCtx{ + Ctx: context.Background(), + Log: log.Log.WithName("ctool"), + } + + if cluster, err = w.createClusterObject(); err != nil { + return err + } + ctx.Log.V(1).Info(fmt.Sprintf("cluster object : %v", cluster)) + + if configSpecs, err = w.getRenderedConfigSpec(); err != nil { + return err + } + + ctx.Log.Info("rendering template:") + for _, tplSpec := range configSpecs { + ctx.Log.Info(fmt.Sprintf("config spec: %s, template name: %s in the component[%s]", + tplSpec.configSpec.Name, + tplSpec.configSpec.TemplateRef, + tplSpec.component)) + } + + cache := make(map[string][]client.Object) + for _, configSpec := range configSpecs { + compName, err := w.getComponentName(configSpec.component, cluster) + if err != nil { + return err + } + objects, ok := cache[configSpec.component] + if !ok { + objs, err := createComponentObjects(w, ctx, cli, configSpec.component, cluster) + if err != nil { + return err + } + cache[configSpec.component] = objs + objects = objs + } + if err := renderTemplates(configSpec.configSpec, outputDir, cluster.Name, compName, objects, configSpec.component); err != nil { + return err + } + } + return nil +} + +func (w *templateRenderWorkflow) getComponentName(componentType string, cluster *appsv1alpha1.Cluster) (string, error) { + clusterCompSpec := cluster.Spec.GetDefNameMappingComponents()[componentType] + if len(clusterCompSpec) == 0 { + return "", cfgcore.MakeError("component[%s] is not defined in cluster definition", componentType) + } + return clusterCompSpec[0].Name, nil +} + +func (w *templateRenderWorkflow) getRenderedConfigSpec() ([]componentedConfigSpec, error) { + foundSpec := func(com appsv1alpha1.ClusterComponentDefinition, specName string) (appsv1alpha1.ComponentTemplateSpec, bool) { + for _, spec := range com.ConfigSpecs { + if spec.Name == specName { + return spec.ComponentTemplateSpec, true + } + } + for _, spec := range com.ScriptSpecs { + if spec.Name == specName { + return spec, true + } + } + return appsv1alpha1.ComponentTemplateSpec{}, false + } + + if w.renderedOpts.ConfigSpec != "" { + for _, com := range w.clusterDefComponents { + if spec, ok := foundSpec(com, w.renderedOpts.ConfigSpec); ok { + return []componentedConfigSpec{{com.Name, spec}}, nil + } + } + return nil, cfgcore.MakeError("config spec[%s] is not found", w.renderedOpts.ConfigSpec) + } + + if !w.renderedOpts.AllConfigSpecs { + return nil, cfgcore.MakeError("AllConfigSpecs should be set while config spec is unset") + } + configSpecs := make([]componentedConfigSpec, 0) + for _, com := range w.clusterDefComponents { + for _, configSpec := range com.ConfigSpecs { + configSpecs = append(configSpecs, componentedConfigSpec{com.Name, configSpec.ComponentTemplateSpec}) + } + for _, configSpec := range com.ScriptSpecs { + configSpecs = append(configSpecs, componentedConfigSpec{com.Name, configSpec}) + } + } + return configSpecs, nil +} + +func (w *templateRenderWorkflow) createClusterObject() (*appsv1alpha1.Cluster, error) { + if w.clusterYaml != "" { + return CustomizedObjFromYaml(w.clusterYaml, generics.ClusterSignature) + } + + clusterVersionObj := GetTypedResourceObjectBySignature(w.localObjects, generics.ClusterVersionSignature) + return mockClusterObject(w.clusterDefObj, w.renderedOpts, clusterVersionObj), nil +} + +func NewWorkflowTemplateRender(helmTemplateDir string, opts RenderedOptions) (*templateRenderWorkflow, error) { + if _, err := os.Stat(helmTemplateDir); err != nil { + panic("cluster definition yaml file is required") + } + + allObjects, err := CreateObjectsFromDirectory(helmTemplateDir) + if err != nil { + return nil, err + } + + clusterDefObj := GetTypedResourceObjectBySignature(allObjects, generics.ClusterDefinitionSignature) + if clusterDefObj == nil { + return nil, cfgcore.MakeError("cluster definition object is not found in helm template directory[%s]", helmTemplateDir) + } + // hack apiserver auto filefield + checkAndFillPortProtocol(clusterDefObj.Spec.ComponentDefs) + + components := clusterDefObj.Spec.ComponentDefs + if opts.ComponentName != "" { + component := clusterDefObj.GetComponentDefByName(opts.ComponentName) + if component == nil { + return nil, cfgcore.MakeError("component[%s] is not defined in cluster definition", opts.ComponentName) + } + components = []appsv1alpha1.ClusterComponentDefinition{*component} + } + return &templateRenderWorkflow{ + renderedOpts: opts, + clusterDefObj: clusterDefObj, + localObjects: allObjects, + clusterDefComponents: components, + }, nil +} + +func checkAndFillPortProtocol(clusterDefComponents []appsv1alpha1.ClusterComponentDefinition) { + // set a default protocol with 'TCP' to avoid failure in BuildHeadlessSvc + for i := range clusterDefComponents { + for j := range clusterDefComponents[i].PodSpec.Containers { + container := &clusterDefComponents[i].PodSpec.Containers[j] + for k := range container.Ports { + port := &container.Ports[k] + if port.Protocol == "" { + port.Protocol = corev1.ProtocolTCP + } + } + } + } +} + +func renderTemplates(configSpec appsv1alpha1.ComponentTemplateSpec, outputDir, clusterName, compName string, objects []client.Object, componentDefName string) error { + cfgName := cfgcore.GetComponentCfgName(clusterName, compName, configSpec.Name) + output := filepath.Join(outputDir, cfgName) + fmt.Printf("dump rendering template spec: %s, output directory: %s\n", + printer.BoldYellow(fmt.Sprintf("%s.%s", componentDefName, configSpec.Name)), output) + + if err := os.MkdirAll(output, 0755); err != nil { + return err + } + + var ok bool + var cm *corev1.ConfigMap + for _, obj := range objects { + if cm, ok = obj.(*corev1.ConfigMap); !ok || cm.Name != cfgName { + continue + } + for file, val := range cm.Data { + if err := os.WriteFile(filepath.Join(output, file), []byte(val), 0755); err != nil { + return err + } + } + break + } + return nil +} + +func createComponentObjects(w *templateRenderWorkflow, ctx intctrlutil.RequestCtx, cli client.Client, + componentType string, cluster *appsv1alpha1.Cluster) ([]client.Object, error) { + compName, err := w.getComponentName(componentType, cluster) + if err != nil { + return nil, err + } + clusterVersionObj := GetTypedResourceObjectBySignature(w.localObjects, generics.ClusterVersionSignature) + component, err := components.NewComponent(ctx, cli, w.clusterDefObj, clusterVersionObj, cluster, compName, nil) + if err != nil { + return nil, err + } + + objs := make([]client.Object, 0) + secret, err := builder.BuildConnCredentialLow(w.clusterDefObj, cluster, component.GetSynthesizedComponent()) + if err != nil { + return nil, err + } + objs = append(objs, secret) + + compObjs, err := component.GetBuiltObjects(ctx, cli) + if err != nil { + return nil, err + } + objs = append(objs, compObjs...) + + return objs, nil +} diff --git a/internal/cli/cmd/class/class.go b/internal/cli/cmd/class/class.go index 4b10d796f..bde026923 100644 --- a/internal/cli/cmd/class/class.go +++ b/internal/cli/cmd/class/class.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -22,11 +25,6 @@ import ( cmdutil "k8s.io/kubectl/pkg/cmd/util" ) -const ( - CustomClassNamespace = "kube-system" - CMDataKeyDefinition = "definition" -) - func NewClassCommand(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "class", diff --git a/internal/cli/cmd/class/class_test.go b/internal/cli/cmd/class/class_test.go index 58544ac58..c4d92e43a 100644 --- a/internal/cli/cmd/class/class_test.go +++ b/internal/cli/cmd/class/class_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class diff --git a/internal/cli/cmd/class/create.go b/internal/cli/cmd/class/create.go index ac82d22fa..1dd11b465 100644 --- a/internal/cli/cmd/class/create.go +++ b/internal/cli/cmd/class/create.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -28,12 +31,14 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" - "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" + "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/class" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" @@ -44,23 +49,21 @@ type CreateOptions struct { genericclioptions.IOStreams Factory cmdutil.Factory - client kubernetes.Interface dynamic dynamic.Interface ClusterDefRef string - ClassFamily string + Constraint string ComponentType string ClassName string CPU string Memory string - Storage []string File string } var classCreateExamples = templates.Examples(` - # Create a class following class family kubeblocks-general-classes for component mysql in cluster definition apecloud-mysql, which have 1 cpu core, 2Gi memory and storage is 10Gi - kbcli class create custom-1c2g --cluster-definition apecloud-mysql --type mysql --class-family kubeblocks-general-classes --cpu 1 --memory 2Gi --storage name=data,size=10Gi + # Create a class with constraint kb-resource-constraint-general for component mysql in cluster definition apecloud-mysql, which has 1 CPU core and 1Gi memory + kbcli class create custom-1c1g --cluster-definition apecloud-mysql --type mysql --constraint kb-resource-constraint-general --cpu 1 --memory 1Gi - # Create classes for component mysql in cluster definition apecloud-mysql, where classes is defined in file + # Create classes for component mysql in cluster definition apecloud-mysql, with classes defined in file kbcli class create --cluster-definition apecloud-mysql --type mysql --file ./classes.yaml `) @@ -76,23 +79,22 @@ func NewCreateCommand(f cmdutil.Factory, streams genericclioptions.IOStreams) *c util.CheckErr(o.run()) }, } - cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definition") + cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definitions") util.CheckErr(cmd.MarkFlagRequired("cluster-definition")) cmd.Flags().StringVar(&o.ComponentType, "type", "", "Specify component type") util.CheckErr(cmd.MarkFlagRequired("type")) - cmd.Flags().StringVar(&o.ClassFamily, "class-family", "", "Specify class family") - cmd.Flags().StringVar(&o.CPU, corev1.ResourceCPU.String(), "", "Specify component cpu cores") + cmd.Flags().StringVar(&o.Constraint, "constraint", "", "Specify resource constraint") + cmd.Flags().StringVar(&o.CPU, corev1.ResourceCPU.String(), "", "Specify component CPU cores") cmd.Flags().StringVar(&o.Memory, corev1.ResourceMemory.String(), "", "Specify component memory size") - cmd.Flags().StringArrayVar(&o.Storage, corev1.ResourceStorage.String(), []string{}, "Specify component storage disks") - cmd.Flags().StringVar(&o.File, "file", "", "Specify file path which contains YAML definition of class") + cmd.Flags().StringVar(&o.File, "file", "", "Specify file path of class definition YAML") return cmd } func (o *CreateOptions) validate(args []string) error { - // just validate creating by resource arguments + // validate creating by resource arguments if o.File != "" { return nil } @@ -116,38 +118,29 @@ func (o *CreateOptions) validate(args []string) error { func (o *CreateOptions) complete(f cmdutil.Factory) error { var err error - if o.client, err = f.KubernetesClientSet(); err != nil { - return err - } - if o.dynamic, err = f.DynamicClient(); err != nil { - return err - } - return nil + o.dynamic, err = f.DynamicClient() + return err } func (o *CreateOptions) run() error { - componentClasses, err := class.GetClasses(o.client, o.ClusterDefRef) + componentClasses, err := class.ListClassesByClusterDefinition(o.dynamic, o.ClusterDefRef) if err != nil { return err } classes, ok := componentClasses[o.ComponentType] if !ok { - classes = make(map[string]*class.ComponentClass) + classes = make(map[string]*v1alpha1.ComponentClassInstance) } - families, err := class.GetClassFamilies(o.dynamic) + constraints, err := class.GetResourceConstraints(o.dynamic) if err != nil { return err } var ( - // new class definition version key - cmK = class.BuildClassDefinitionVersion() - // new class definition version value - cmV string - // newly created class names - classNames []string + classInstances []*v1alpha1.ComponentClassInstance + componentClassGroups []v1alpha1.ComponentClassGroup ) if o.File != "" { @@ -155,105 +148,106 @@ func (o *CreateOptions) run() error { if err != nil { return err } - newClasses, err := class.ParseComponentClasses(map[string]string{cmK: string(data)}) + if err := yaml.Unmarshal(data, &componentClassGroups); err != nil { + return err + } + classDefinition := v1alpha1.ComponentClassDefinition{ + Spec: v1alpha1.ComponentClassDefinitionSpec{Groups: componentClassGroups}, + } + newClasses, err := class.ParseComponentClasses(classDefinition) if err != nil { return err } - for name, cls := range newClasses { - if _, ok = families[cls.Family]; !ok { - return fmt.Errorf("family %s is not found", cls.Family) - } - if _, ok = classes[name]; ok { - return fmt.Errorf("class name conflicted %s", name) - } - classNames = append(classNames, name) + for _, cls := range newClasses { + classInstances = append(classInstances, cls) } - cmV = string(data) } else { if _, ok = classes[o.ClassName]; ok { return fmt.Errorf("class name conflicted %s", o.ClassName) } - if _, ok = families[o.ClassFamily]; !ok { - return fmt.Errorf("family %s is not found", o.ClassFamily) + if _, ok = constraints[o.Constraint]; !ok { + return fmt.Errorf("resource constraint %s is not found", o.Constraint) } - def, err := o.buildClassFamilyDef() + cls := v1alpha1.ComponentClass{Name: o.ClassName, CPU: resource.MustParse(o.CPU), Memory: resource.MustParse(o.Memory)} if err != nil { return err } - data, err := yaml.Marshal([]*class.ComponentClassFamilyDef{def}) - if err != nil { - return err + componentClassGroups = []v1alpha1.ComponentClassGroup{ + { + ResourceConstraintRef: o.Constraint, + Series: []v1alpha1.ComponentClassSeries{ + { + Classes: []v1alpha1.ComponentClass{cls}, + }, + }, + }, + } + classInstances = append(classInstances, &v1alpha1.ComponentClassInstance{ComponentClass: cls, ResourceConstraintRef: o.Constraint}) + } + + var classNames []string + for _, item := range classInstances { + constraint, ok := constraints[item.ResourceConstraintRef] + if !ok { + return fmt.Errorf("resource constraint %s is not found", item.ResourceConstraintRef) + } + if _, ok = classes[item.Name]; ok { + return fmt.Errorf("class name conflicted %s", item.Name) + } + if !constraint.MatchClass(item) { + return fmt.Errorf("class %s does not conform to constraint %s", item.Name, item.ResourceConstraintRef) } - cmV = string(data) - classNames = append(classNames, o.ClassName) + classNames = append(classNames, item.Name) } - cmName := class.GetCustomClassConfigMapName(o.ClusterDefRef, o.ComponentType) - cm, err := o.client.CoreV1().ConfigMaps(CustomClassNamespace).Get(context.TODO(), cmName, metav1.GetOptions{}) + objName := class.GetCustomClassObjectName(o.ClusterDefRef, o.ComponentType) + obj, err := o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Get(context.TODO(), objName, metav1.GetOptions{}) if err != nil && !errors.IsNotFound(err) { return err } + var classDefinition v1alpha1.ComponentClassDefinition if err == nil { - cm.Data[cmK] = cmV - if _, err = o.client.CoreV1().ConfigMaps(cm.GetNamespace()).Update(context.TODO(), cm, metav1.UpdateOptions{}); err != nil { + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &classDefinition); err != nil { + return err + } + classDefinition.Spec.Groups = append(classDefinition.Spec.Groups, componentClassGroups...) + unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&classDefinition) + if err != nil { + return err + } + if _, err = o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Update( + context.Background(), &unstructured.Unstructured{Object: unstructuredMap}, metav1.UpdateOptions{}); err != nil { return err } } else { - cm = &corev1.ConfigMap{ + gvr := types.ComponentClassDefinitionGVR() + classDefinition = v1alpha1.ComponentClassDefinition{ + TypeMeta: metav1.TypeMeta{ + Kind: types.KindComponentClassDefinition, + APIVersion: gvr.Group + "/" + gvr.Version, + }, ObjectMeta: metav1.ObjectMeta{ - Name: class.GetCustomClassConfigMapName(o.ClusterDefRef, o.ComponentType), - Namespace: CustomClassNamespace, + Name: class.GetCustomClassObjectName(o.ClusterDefRef, o.ComponentType), Labels: map[string]string{ constant.ClusterDefLabelKey: o.ClusterDefRef, types.ClassProviderLabelKey: "user", - types.ClassLevelLabelKey: "component", constant.KBAppComponentDefRefLabelKey: o.ComponentType, }, }, - Data: map[string]string{cmK: cmV}, + Spec: v1alpha1.ComponentClassDefinitionSpec{ + Groups: componentClassGroups, + }, } - if _, err = o.client.CoreV1().ConfigMaps(CustomClassNamespace).Create(context.TODO(), cm, metav1.CreateOptions{}); err != nil { + unstructuredMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&classDefinition) + if err != nil { return err } - } - _, _ = fmt.Fprintf(o.Out, "Successfully created class [%s].", strings.Join(classNames, ",")) - return nil -} - -func (o *CreateOptions) buildClassFamilyDef() (*class.ComponentClassFamilyDef, error) { - clsDef := class.ComponentClassDef{Name: o.ClassName, CPU: o.CPU, Memory: o.Memory} - for _, disk := range o.Storage { - kvs := strings.Split(disk, ",") - def := class.DiskDef{} - for _, kv := range kvs { - parts := strings.Split(kv, "=") - if len(parts) != 2 { - return nil, fmt.Errorf("invalid storage disk: %s", disk) - } - switch parts[0] { - case "name": - def.Name = parts[1] - case "size": - def.Size = parts[1] - case "class": - def.Class = parts[1] - default: - return nil, fmt.Errorf("invalid storage disk: %s", disk) - } - } - // validate disk size - if _, err := resource.ParseQuantity(def.Size); err != nil { - return nil, fmt.Errorf("invalid disk size: %s", disk) - } - if def.Name == "" { - return nil, fmt.Errorf("invalid disk name: %s", disk) + if _, err = o.dynamic.Resource(types.ComponentClassDefinitionGVR()).Create( + context.Background(), &unstructured.Unstructured{Object: unstructuredMap}, metav1.CreateOptions{}); err != nil { + return err } - clsDef.Storage = append(clsDef.Storage, def) } - def := &class.ComponentClassFamilyDef{ - Family: o.ClassFamily, - Series: []class.ComponentClassSeriesDef{{Classes: []class.ComponentClassDef{clsDef}}}, - } - return def, nil + _, _ = fmt.Fprintf(o.Out, "Successfully create class [%s].\n", strings.Join(classNames, ",")) + return nil } diff --git a/internal/cli/cmd/class/create_test.go b/internal/cli/cmd/class/create_test.go index f529487de..a7a08bebd 100644 --- a/internal/cli/cmd/class/create_test.go +++ b/internal/cli/cmd/class/create_test.go @@ -1,36 +1,33 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class import ( "bytes" - "net/http" + "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes/scheme" - clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -39,59 +36,32 @@ import ( var _ = Describe("create", func() { var ( - o *CreateOptions - cd *appsv1alpha1.ClusterDefinition - out *bytes.Buffer - tf *cmdtesting.TestFactory - streams genericclioptions.IOStreams + createOptions *CreateOptions + out *bytes.Buffer + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams ) fillResources := func(o *CreateOptions, cpu string, memory string, storage []string) { o.CPU = cpu o.Memory = memory - o.Storage = storage + o.ClassName = fmt.Sprintf("custom-%s-%s", cpu, memory) + o.Constraint = generalResourceConstraint.Name } BeforeEach(func() { - cd = testing.FakeClusterDef() - streams, _, out, _ = genericclioptions.NewTestIOStreams() tf = testing.NewTestFactory(namespace) + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + tf.FakeDynamicClient = testing.FakeDynamicClient(&classDef, &generalResourceConstraint, &memoryOptimizedResourceConstraint) - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - httpResp := func(obj runtime.Object) *http.Response { - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} - } - cms := testing.FakeComponentClassDef(cd, classDef) - - resources := map[string]runtime.Object{ - "/api/v1/configmaps": cms, - } - - tf.UnstructuredClient = &clientfake.RESTClient{ - GroupVersion: schema.GroupVersion{Group: "core", Version: "v1"}, - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - if req.Method == "POST" { - return httpResp(&corev1.ConfigMap{}), nil - } - resource, ok := resources[req.URL.Path] - if !ok { - return nil, errors.NewNotFound(schema.GroupResource{}, req.URL.Path) - } - return httpResp(resource), nil - }), - } - tf.Client = tf.UnstructuredClient - tf.FakeDynamicClient = testing.FakeDynamicClient(&generalClassFamily, &memoryOptimizedClassFamily, cd) - - o = &CreateOptions{ + createOptions = &CreateOptions{ Factory: tf, IOStreams: streams, - ClusterDefRef: cd.Name, - ComponentType: testing.ComponentDefName, + ClusterDefRef: "apecloud-mysql", + ComponentType: "mysql", } - Expect(o.complete(tf)).ShouldNot(HaveOccurred()) + Expect(createOptions.complete(tf)).ShouldNot(HaveOccurred()) }) AfterEach(func() { @@ -106,39 +76,59 @@ var _ = Describe("create", func() { Context("with resource arguments", func() { It("should fail if required arguments is missing", func() { - o.ClassFamily = generalClassFamily.Name - fillResources(o, "", "48Gi", nil) - Expect(o.validate([]string{"general-12c48g"})).Should(HaveOccurred()) - fillResources(o, "12", "", nil) - Expect(o.validate([]string{"general-12c48g"})).Should(HaveOccurred()) - fillResources(o, "12", "48g", nil) - Expect(o.validate([]string{})).Should(HaveOccurred()) + fillResources(createOptions, "", "48Gi", nil) + Expect(createOptions.validate([]string{"general-12c48g"})).Should(HaveOccurred()) + fillResources(createOptions, "12", "", nil) + Expect(createOptions.validate([]string{"general-12c48g"})).Should(HaveOccurred()) + fillResources(createOptions, "12", "48g", nil) + Expect(createOptions.validate([]string{})).Should(HaveOccurred()) }) It("should succeed with required arguments", func() { - o.ClassFamily = generalClassFamily.Name - fillResources(o, "12", "48Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) - Expect(o.validate([]string{"general-12c48g"})).ShouldNot(HaveOccurred()) - Expect(o.run()).ShouldNot(HaveOccurred()) - Expect(out.String()).Should(ContainSubstring(o.ClassName)) + fillResources(createOptions, "96", "384Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) + Expect(createOptions.validate([]string{"general-96c384g"})).ShouldNot(HaveOccurred()) + Expect(createOptions.run()).ShouldNot(HaveOccurred()) + Expect(out.String()).Should(ContainSubstring(createOptions.ClassName)) + }) + + It("should fail if constraint not existed", func() { + fillResources(createOptions, "2", "8Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) + createOptions.Constraint = "constraint-not-exist" + Expect(createOptions.run()).Should(HaveOccurred()) + }) + + It("should fail if not conformed to constraint", func() { + By("memory not conformed to constraint") + fillResources(createOptions, "2", "9Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) + Expect(createOptions.run()).Should(HaveOccurred()) + + By("CPU with invalid step") + fillResources(createOptions, "0.6", "0.6Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) + Expect(createOptions.run()).Should(HaveOccurred()) }) It("should fail if class name is conflicted", func() { - o.ClassName = "general-1c1g" - fillResources(o, "1", "1Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) - Expect(o.run()).Should(HaveOccurred()) + fillResources(createOptions, "1", "1Gi", []string{"name=data,size=10Gi", "name=log,size=1Gi"}) + createOptions.ClassName = "general-1c1g" + Expect(createOptions.run()).Should(HaveOccurred()) + + fillResources(createOptions, "0.5", "0.5Gi", []string{}) + Expect(createOptions.run()).ShouldNot(HaveOccurred()) + + fillResources(createOptions, "0.5", "0.5Gi", []string{}) + Expect(createOptions.run()).Should(HaveOccurred()) }) }) Context("with class definitions file", func() { It("should succeed", func() { - o.File = testCustomClassDefsPath - Expect(o.run()).ShouldNot(HaveOccurred()) + createOptions.File = testCustomClassDefsPath + Expect(createOptions.run()).ShouldNot(HaveOccurred()) Expect(out.String()).Should(ContainSubstring("custom-1c1g")) - Expect(out.String()).Should(ContainSubstring("custom-200c400g")) + Expect(out.String()).Should(ContainSubstring("custom-4c16g")) // memory optimized classes - Expect(out.String()).Should(ContainSubstring("custom-1c32g")) - Expect(out.String()).Should(ContainSubstring("custom-2c64g")) + Expect(out.String()).Should(ContainSubstring("custom-2c16g")) + Expect(out.String()).Should(ContainSubstring("custom-4c64g")) }) }) diff --git a/internal/cli/cmd/class/list.go b/internal/cli/cmd/class/list.go index aae922ea2..830c5d7a2 100644 --- a/internal/cli/cmd/class/list.go +++ b/internal/cli/cmd/class/list.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -22,20 +25,23 @@ import ( "strings" "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/client-go/kubernetes" + "k8s.io/client-go/dynamic" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/class" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/util/flags" ) type ListOptions struct { ClusterDefRef string Factory cmdutil.Factory - client *kubernetes.Clientset + dynamic dynamic.Interface genericclioptions.IOStreams } @@ -55,59 +61,79 @@ func NewListCommand(f cmdutil.Factory, streams genericclioptions.IOStreams) *cob util.CheckErr(o.run()) }, } - cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli cluster-definition list\" to show all available cluster definition") + flags.AddClusterDefinitionFlag(f, cmd, &o.ClusterDefRef) util.CheckErr(cmd.MarkFlagRequired("cluster-definition")) return cmd } func (o *ListOptions) complete(f cmdutil.Factory) error { var err error - o.client, err = f.KubernetesClientSet() - if err != nil { - return err - } + o.dynamic, err = f.DynamicClient() return err } func (o *ListOptions) run() error { - componentClasses, err := class.GetClasses(o.client, o.ClusterDefRef) + componentClasses, err := class.ListClassesByClusterDefinition(o.dynamic, o.ClusterDefRef) if err != nil { return err } - familyClassMap := make(map[string]map[string][]*class.ComponentClass) + constraintClassMap := make(map[string]map[string][]*appsv1alpha1.ComponentClassInstance) for compName, items := range componentClasses { for _, item := range items { - if _, ok := familyClassMap[item.Family]; !ok { - familyClassMap[item.Family] = make(map[string][]*class.ComponentClass) + if _, ok := constraintClassMap[item.ResourceConstraintRef]; !ok { + constraintClassMap[item.ResourceConstraintRef] = make(map[string][]*appsv1alpha1.ComponentClassInstance) } - familyClassMap[item.Family][compName] = append(familyClassMap[item.Family][compName], item) + constraintClassMap[item.ResourceConstraintRef][compName] = append(constraintClassMap[item.ResourceConstraintRef][compName], item) } } - var familyNames []string - for name := range familyClassMap { - familyNames = append(familyNames, name) + var constraintNames []string + for name := range constraintClassMap { + constraintNames = append(constraintNames, name) } - sort.Strings(familyNames) - for _, family := range familyNames { - for compName, classes := range familyClassMap[family] { - o.printClassFamily(family, compName, classes) + sort.Strings(constraintNames) + for _, constraintName := range constraintNames { + for compName, classes := range constraintClassMap[constraintName] { + o.printClass(constraintName, compName, classes) } _, _ = fmt.Fprint(o.Out, "\n") } return nil } -func (o *ListOptions) printClassFamily(family string, compName string, classes []*class.ComponentClass) { +func (o *ListOptions) printClass(constraintName string, compName string, classes []*appsv1alpha1.ComponentClassInstance) { tbl := printer.NewTablePrinter(o.Out) - _, _ = fmt.Fprintf(o.Out, "\nFamily %s:\n", family) - tbl.SetHeader("COMPONENT", "CLASS", "CPU", "MEMORY", "STORAGE") + _, _ = fmt.Fprintf(o.Out, "\nConstraint %s:\n", constraintName) + tbl.SetHeader("COMPONENT", "CLASS", "CPU", "MEMORY") sort.Sort(class.ByClassCPUAndMemory(classes)) - for _, class := range classes { - var disks []string - for _, disk := range class.Storage { - disks = append(disks, disk.String()) - } - tbl.AddRow(compName, class.Name, class.CPU.String(), class.Memory.String(), strings.Join(disks, ",")) + for _, cls := range classes { + tbl.AddRow(compName, cls.Name, cls.CPU.String(), normalizeMemory(cls.Memory)) } tbl.Print() } + +func normalizeMemory(mem resource.Quantity) string { + if !strings.HasSuffix(mem.String(), "m") { + return mem.String() + } + + var ( + value float64 + suffix string + bytes = float64(mem.MilliValue()) / 1000 + ) + switch { + case bytes < 1024: + value = bytes / 1024 + suffix = "Ki" + case bytes < 1024*1024: + value = bytes / 1024 / 1024 + suffix = "Mi" + case bytes < 1024*1024*1024: + value = bytes / 1024 / 1024 / 1024 + suffix = "Gi" + default: + value = bytes / 1024 / 1024 / 1024 / 1024 + suffix = "Ti" + } + return strings.TrimRight(fmt.Sprintf("%.3f", value), "0") + suffix +} diff --git a/internal/cli/cmd/class/list_test.go b/internal/cli/cmd/class/list_test.go index c872514bf..508f3d97c 100644 --- a/internal/cli/cmd/class/list_test.go +++ b/internal/cli/cmd/class/list_test.go @@ -1,35 +1,32 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class import ( "bytes" - "net/http" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/cli-runtime/pkg/genericclioptions" - "k8s.io/cli-runtime/pkg/resource" "k8s.io/client-go/kubernetes/scheme" - clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -38,34 +35,16 @@ import ( var _ = Describe("list", func() { var ( - cd *appsv1alpha1.ClusterDefinition out *bytes.Buffer tf *cmdtesting.TestFactory streams genericclioptions.IOStreams ) BeforeEach(func() { - cd = testing.FakeClusterDef() - streams, _, out, _ = genericclioptions.NewTestIOStreams() tf = testing.NewTestFactory(namespace) - - _ = corev1.AddToScheme(scheme.Scheme) - codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - httpResp := func(obj runtime.Object) *http.Response { - return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} - } - - tf.UnstructuredClient = &clientfake.RESTClient{ - GroupVersion: schema.GroupVersion{Group: "core", Version: "v1"}, - NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, - Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { - return map[string]*http.Response{ - "/api/v1/configmaps": httpResp(testing.FakeComponentClassDef(cd, classDef)), - }[req.URL.Path], nil - }), - } - tf.Client = tf.UnstructuredClient + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + tf.FakeDynamicClient = testing.FakeDynamicClient(&classDef) }) AfterEach(func() { @@ -75,10 +54,53 @@ var _ = Describe("list", func() { It("should succeed", func() { cmd := NewListCommand(tf, streams) Expect(cmd).ShouldNot(BeNil()) - cmd.Run(cmd, []string{"--cluster-definition", cd.GetName()}) + _ = cmd.Flags().Set("cluster-definition", "apecloud-mysql") + cmd.Run(cmd, []string{}) Expect(out.String()).To(ContainSubstring("general-1c1g")) - Expect(out.String()).To(ContainSubstring(testing.ComponentDefName)) - Expect(out.String()).To(ContainSubstring(generalClassFamily.Name)) - Expect(out.String()).To(ContainSubstring(memoryOptimizedClassFamily.Name)) + Expect(out.String()).To(ContainSubstring("mysql")) + Expect(out.String()).To(ContainSubstring(generalResourceConstraint.Name)) + }) + + It("memory should be normalized", func() { + cases := []struct { + memory string + normalized string + }{ + { + memory: "0.2Gi", + normalized: "0.2Gi", + }, + { + memory: "0.2Mi", + normalized: "0.2Mi", + }, + { + memory: "0.2Ki", + normalized: "0.2Ki", + }, + { + memory: "1024Mi", + normalized: "1Gi", + }, + { + memory: "1025Mi", + normalized: "1025Mi", + }, + { + memory: "1023Mi", + normalized: "1023Mi", + }, + { + memory: "1Gi", + normalized: "1Gi", + }, + { + memory: "512Mi", + normalized: "512Mi", + }, + } + for _, item := range cases { + Expect(normalizeMemory(resource.MustParse(item.memory))).Should(Equal(item.normalized)) + } }) }) diff --git a/internal/cli/cmd/class/suite_test.go b/internal/cli/cmd/class/suite_test.go index ef9544a90..a027135aa 100644 --- a/internal/cli/cmd/class/suite_test.go +++ b/internal/cli/cmd/class/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -22,6 +25,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/util/yaml" "k8s.io/client-go/kubernetes/scheme" @@ -30,35 +34,35 @@ import ( ) const ( - namespace = "test" - testDefaultClassDefsPath = "../../testing/testdata/class.yaml" - testCustomClassDefsPath = "../../testing/testdata/custom_class.yaml" - testGeneralClassFamilyPath = "../../testing/testdata/classfamily-general.yaml" - testMemoryOptimizedClassFamilyPath = "../../testing/testdata/classfamily-memory-optimized.yaml" + namespace = "test" + testDefaultClassDefsPath = "../../testing/testdata/class.yaml" + testCustomClassDefsPath = "../../testing/testdata/custom_class.yaml" + testGeneralResourceConstraintPath = "../../testing/testdata/resource-constraint-general.yaml" + testMemoryOptimizedResourceConstraintPath = "../../testing/testdata/resource-constraint-memory-optimized.yaml" ) var ( - classDef []byte - generalFamilyDef []byte - memoryOptimizedFamilyDef []byte - generalClassFamily appsv1alpha1.ClassFamily - memoryOptimizedClassFamily appsv1alpha1.ClassFamily + classDef appsv1alpha1.ComponentClassDefinition + generalResourceConstraint appsv1alpha1.ComponentResourceConstraint + memoryOptimizedResourceConstraint appsv1alpha1.ComponentResourceConstraint ) var _ = BeforeSuite(func() { var err error - classDef, err = os.ReadFile(testDefaultClassDefsPath) + classDefBytes, err := os.ReadFile(testDefaultClassDefsPath) + Expect(err).ShouldNot(HaveOccurred()) + err = yaml.Unmarshal(classDefBytes, &classDef) Expect(err).ShouldNot(HaveOccurred()) - generalFamilyDef, err = os.ReadFile(testGeneralClassFamilyPath) + generalResourceConstraintBytes, err := os.ReadFile(testGeneralResourceConstraintPath) Expect(err).ShouldNot(HaveOccurred()) - err = yaml.Unmarshal(generalFamilyDef, &generalClassFamily) + err = yaml.Unmarshal(generalResourceConstraintBytes, &generalResourceConstraint) Expect(err).ShouldNot(HaveOccurred()) - memoryOptimizedFamilyDef, err = os.ReadFile(testMemoryOptimizedClassFamilyPath) + memoryOptimizedResourceConstraintBytes, err := os.ReadFile(testMemoryOptimizedResourceConstraintPath) Expect(err).ShouldNot(HaveOccurred()) - err = yaml.Unmarshal(memoryOptimizedFamilyDef, &memoryOptimizedClassFamily) + err = yaml.Unmarshal(memoryOptimizedResourceConstraintBytes, &memoryOptimizedResourceConstraint) Expect(err).ShouldNot(HaveOccurred()) err = appsv1alpha1.AddToScheme(scheme.Scheme) diff --git a/internal/cli/cmd/class/template.go b/internal/cli/cmd/class/template.go index 3b25e59aa..31d2dfadb 100644 --- a/internal/cli/cmd/class/template.go +++ b/internal/cli/cmd/class/template.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class @@ -27,30 +30,23 @@ import ( ) const ComponentClassTemplate = ` -- # class family name, such as general, memory-optimized, cpu-optimized etc. - family: kb-class-family-general - # class schema template, you can set default resource values here +- resourceConstraintRef: kb-resource-constraint-general + # class template, you can declare variables and set default values here template: | cpu: "{{ or .cpu 1 }}" memory: "{{ or .memory 4 }}Gi" - storage: - - name: data - size: "{{ or .dataStorageSize 10 }}Gi" - - name: log - size: "{{ or .logStorageSize 1 }}Gi" - # class schema template variables - vars: [cpu, memory, dataStorageSize, logStorageSize] + # template variables used to define classes + vars: [cpu, memory] series: - - # class name generator, you can reference variables in class schema template - # it's also ok to define static class name in following class definitions - name: "custom-{{ .cpu }}c{{ .memory }}g" + - # class naming template, you can reference variables in class template + # it's also ok to define static class name in the following class definitions + namingTemplate: "custom-{{ .cpu }}c{{ .memory }}g" # class definitions, we support two kinds of class definitions: - # 1. define arguments for class schema variables, class schema will be dynamically generated - # 2. statically define complete class schema + # 1. dynamically classes rendered with defined values & template variables + # 2. statically defined classes classes: - # arguments for dynamically generated class - - args: [1, 1, 100, 10] + - args: [1, 1] ` type TemplateOptions struct { diff --git a/internal/cli/cmd/class/template_test.go b/internal/cli/cmd/class/template_test.go index e2a239d3d..c9ad814e8 100644 --- a/internal/cli/cmd/class/template_test.go +++ b/internal/cli/cmd/class/template_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package class diff --git a/internal/cli/cmd/cli.go b/internal/cli/cmd/cli.go index 394fca51f..4e98d1ef4 100644 --- a/internal/cli/cmd/cli.go +++ b/internal/cli/cmd/cli.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cmd @@ -19,27 +22,32 @@ package cmd import ( "fmt" "os" + "strings" "github.com/spf13/cobra" "github.com/spf13/viper" "k8s.io/cli-runtime/pkg/genericclioptions" cliflag "k8s.io/component-base/cli/flag" + kccmd "k8s.io/kubectl/pkg/cmd" cmdutil "k8s.io/kubectl/pkg/cmd/util" utilcomp "k8s.io/kubectl/pkg/util/completion" "k8s.io/kubectl/pkg/util/templates" "github.com/apecloud/kubeblocks/internal/cli/cmd/addon" "github.com/apecloud/kubeblocks/internal/cli/cmd/alert" - "github.com/apecloud/kubeblocks/internal/cli/cmd/backupconfig" "github.com/apecloud/kubeblocks/internal/cli/cmd/bench" + "github.com/apecloud/kubeblocks/internal/cli/cmd/builder" "github.com/apecloud/kubeblocks/internal/cli/cmd/class" "github.com/apecloud/kubeblocks/internal/cli/cmd/cluster" "github.com/apecloud/kubeblocks/internal/cli/cmd/clusterdefinition" "github.com/apecloud/kubeblocks/internal/cli/cmd/clusterversion" "github.com/apecloud/kubeblocks/internal/cli/cmd/dashboard" + "github.com/apecloud/kubeblocks/internal/cli/cmd/fault" "github.com/apecloud/kubeblocks/internal/cli/cmd/kubeblocks" + "github.com/apecloud/kubeblocks/internal/cli/cmd/migration" "github.com/apecloud/kubeblocks/internal/cli/cmd/options" "github.com/apecloud/kubeblocks/internal/cli/cmd/playground" + "github.com/apecloud/kubeblocks/internal/cli/cmd/plugin" "github.com/apecloud/kubeblocks/internal/cli/cmd/version" "github.com/apecloud/kubeblocks/internal/cli/util" ) @@ -48,6 +56,46 @@ const ( cliName = "kbcli" ) +func init() { + if _, err := util.GetCliHomeDir(); err != nil { + fmt.Println("Failed to create kbcli home dir:", err) + } +} + +func NewDefaultCliCmd() *cobra.Command { + cmd := NewCliCmd() + + pluginHandler := kccmd.NewDefaultPluginHandler(plugin.ValidPluginFilenamePrefixes) + + if len(os.Args) > 1 { + cmdPathPieces := os.Args[1:] + + // only look for suitable extension executables if + // the specified command does not exist + if _, _, err := cmd.Find(cmdPathPieces); err != nil { + var cmdName string + for _, arg := range cmdPathPieces { + if !strings.HasPrefix(arg, "-") { + cmdName = arg + break + } + } + + switch cmdName { + case "help", cobra.ShellCompRequestCmd, cobra.ShellCompNoDescRequestCmd: + // Don't search for a plugin + default: + if err := kccmd.HandlePluginCommand(pluginHandler, cmdPathPieces); err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + os.Exit(1) + } + } + } + } + + return cmd +} + func NewCliCmd() *cobra.Command { cmd := &cobra.Command{ Use: cliName, @@ -72,7 +120,7 @@ A Command Line Interface for KubeBlocks`, }, } - // From this point and forward we get warnings on flags that contain "_" separators + // Start from this point we get warnings on flags that contain "_" separators // when adding them with hyphen instead of the original name. cmd.SetGlobalNormalizationFunc(cliflag.WarnWordSepNormalizeFunc) @@ -84,6 +132,9 @@ A Command Line Interface for KubeBlocks`, matchVersionKubeConfigFlags := cmdutil.NewMatchVersionFlags(kubeConfigFlags) matchVersionKubeConfigFlags.AddFlags(flags) + // add klog flags + util.AddKlogFlags(flags) + f := cmdutil.NewFactory(matchVersionKubeConfigFlags) ioStreams := genericclioptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr} @@ -91,22 +142,33 @@ A Command Line Interface for KubeBlocks`, cmd.AddCommand( playground.NewPlaygroundCmd(ioStreams), kubeblocks.NewKubeBlocksCmd(f, ioStreams), - cluster.NewClusterCmd(f, ioStreams), - bench.NewBenchCmd(), + bench.NewBenchCmd(f, ioStreams), options.NewCmdOptions(ioStreams.Out), version.NewVersionCmd(f), - backupconfig.NewBackupConfigCmd(f, ioStreams), dashboard.NewDashboardCmd(f, ioStreams), clusterversion.NewClusterVersionCmd(f, ioStreams), clusterdefinition.NewClusterDefinitionCmd(f, ioStreams), class.NewClassCommand(f, ioStreams), alert.NewAlertCmd(f, ioStreams), addon.NewAddonCmd(f, ioStreams), + migration.NewMigrationCmd(f, ioStreams), + plugin.NewPluginCmd(ioStreams), + fault.NewFaultCmd(f, ioStreams), + builder.NewBuilderCmd(f, ioStreams), ) filters := []string{"options"} templates.ActsAsRootCommand(cmd, filters, []templates.CommandGroup{}...) + helpFunc := cmd.HelpFunc() + usageFunc := cmd.UsageFunc() + + // clusterCmd sets its own usage and help function and its subcommand will inherit it, + // so we need to set its subcommand's usage and help function back to the root command + clusterCmd := cluster.NewClusterCmd(f, ioStreams) + registerUsageAndHelpFuncForSubCommand(clusterCmd, helpFunc, usageFunc) + cmd.AddCommand(clusterCmd) + utilcomp.SetFactoryForCompletion(f) registerCompletionFuncForGlobalFlags(cmd, f) @@ -159,3 +221,10 @@ func registerCompletionFuncForGlobalFlags(cmd *cobra.Command, f cmdutil.Factory) return utilcomp.ListUsersInConfig(toComplete), cobra.ShellCompDirectiveNoFileComp })) } + +func registerUsageAndHelpFuncForSubCommand(cmd *cobra.Command, helpFunc func(*cobra.Command, []string), usageFunc func(command *cobra.Command) error) { + for _, subCmd := range cmd.Commands() { + subCmd.SetHelpFunc(helpFunc) + subCmd.SetUsageFunc(usageFunc) + } +} diff --git a/internal/cli/cmd/cluster/accounts.go b/internal/cli/cmd/cluster/accounts.go index 04f6b4fdd..cee3e6eac 100644 --- a/internal/cli/cmd/cluster/accounts.go +++ b/internal/cli/cmd/cluster/accounts.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -25,43 +28,62 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/cmd/accounts" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" - "github.com/apecloud/kubeblocks/internal/sqlchannel" + channelutil "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) var ( createUserExamples = templates.Examples(` - # create account - kbcli cluster create-account NAME --component-name COMPNAME --username NAME --password PASSWD + # create account with password + kbcli cluster create-account CLUSTERNAME --component COMPNAME --name USERNAME --password PASSWD # create account without password - kbcli cluster create-account NAME --component-name COMPNAME --username NAME - # create account with expired interval - kbcli cluster create-account NAME --component-name COMPNAME --username NAME --password PASSWD --expiredAt 2046-01-02T15:04:05Z + kbcli cluster create-account CLUSTERNAME --component COMPNAME --name USERNAME + # create account with default component + kbcli cluster create-account CLUSTERNAME --name USERNAME + # create account for instance + kbcli cluster create-account --instance INSTANCE --name USERNAME `) deleteUserExamples = templates.Examples(` # delete account by name - kbcli cluster delete-account NAME --component-name COMPNAME --username NAME + kbcli cluster delete-account CLUSTERNAME --component COMPNAME --name USERNAME + # delete account with default component + kbcli cluster delete-account CLUSTERNAME --name USERNAME + # delete account for instance + kbcli cluster delete-account --instance INSTANCE --name USERNAME `) descUserExamples = templates.Examples(` # describe account and show role information - kbcli cluster describe-account NAME --component-name COMPNAME--username NAME + kbcli cluster describe-account CLUSTERNAME --component COMPNAME --name USERNAME + # describe account with default component + kbcli cluster describe-account CLUSTERNAME --name USERNAME + # describe account for instance + kbcli cluster describe-account --instance INSTANCE --name USERNAME `) listUsersExample = templates.Examples(` - # list all users from specified component of a cluster - kbcli cluster list-accounts NAME --component-name COMPNAME --show-connected-users - - # list all users from cluster's one particular instance - kbcli cluster list-accounts NAME -i INSTANCE + # list all users for component + kbcli cluster list-accounts CLUSTERNAME --component COMPNAME + # list all users with default component + kbcli cluster list-accounts CLUSTERNAME + # list all users from instance + kbcli cluster list-accounts --instance INSTANCE `) grantRoleExamples = templates.Examples(` # grant role to user - kbcli cluster grant-role NAME --component-name COMPNAME --username NAME --role ROLENAME + kbcli cluster grant-role CLUSTERNAME --component COMPNAME --name USERNAME --role ROLENAME + # grant role to user with default component + kbcli cluster grant-role CLUSTERNAME --name USERNAME --role ROLENAME + # grant role to user for instance + kbcli cluster grant-role --instance INSTANCE --name USERNAME --role ROLENAME `) revokeRoleExamples = templates.Examples(` # revoke role from user - kbcli cluster revoke-role NAME --component-name COMPNAME --role ROLENAME + kbcli cluster revoke-role CLUSTERNAME --component COMPNAME --name USERNAME --role ROLENAME + # revoke role from user with default component + kbcli cluster revoke-role CLUSTERNAME --name USERNAME --role ROLENAME + # revoke role from user for instance + kbcli cluster revoke-role --instance INSTANCE --name USERNAME --role ROLENAME `) ) @@ -75,7 +97,7 @@ func NewCreateAccountCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) @@ -92,10 +114,11 @@ func NewDeleteAccountCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) + cmd.Flags().BoolVar(&o.AutoApprove, "auto-approve", false, "Skip interactive approval before deleting account") return cmd } @@ -109,7 +132,7 @@ func NewDescAccountCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) * Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) @@ -128,7 +151,7 @@ func NewListAccountsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) @@ -136,7 +159,7 @@ func NewListAccountsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) } func NewGrantOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := accounts.NewGrantOptions(f, streams, sqlchannel.GrantUserRoleOp) + o := accounts.NewGrantOptions(f, streams, channelutil.GrantUserRoleOp) cmd := &cobra.Command{ Use: "grant-role", @@ -147,7 +170,7 @@ func NewGrantOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *co Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) @@ -155,7 +178,7 @@ func NewGrantOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *co } func NewRevokeOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := accounts.NewGrantOptions(f, streams, sqlchannel.RevokeUserRoleOp) + o := accounts.NewGrantOptions(f, streams, channelutil.RevokeUserRoleOp) cmd := &cobra.Command{ Use: "revoke-role", @@ -166,7 +189,7 @@ func NewRevokeOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *c Run: func(cmd *cobra.Command, args []string) { cmdutil.CheckErr(o.Validate(args)) cmdutil.CheckErr(o.Complete(f)) - cmdutil.CheckErr(o.Run(f, streams)) + cmdutil.CheckErr(o.Run(cmd, f, streams)) }, } o.AddFlags(cmd) diff --git a/internal/cli/cmd/cluster/cluster.go b/internal/cli/cmd/cluster/cluster.go index 857fdc4eb..28fffb69d 100644 --- a/internal/cli/cmd/cluster/cluster.go +++ b/internal/cli/cmd/cluster/cluster.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -50,6 +53,7 @@ func NewClusterCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr NewListInstancesCmd(f, streams), NewListComponentsCmd(f, streams), NewListEventsCmd(f, streams), + NewLabelCmd(f, streams), NewDeleteCmd(f, streams), }, }, @@ -67,9 +71,15 @@ func NewClusterCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr NewDescribeOpsCmd(f, streams), NewListOpsCmd(f, streams), NewDeleteOpsCmd(f, streams), + NewExposeCmd(f, streams), + NewCancelCmd(f, streams), + }, + }, + { + Message: "Cluster Configuration Operation Commands:", + Commands: []*cobra.Command{ NewReconfigureCmd(f, streams), NewEditConfigureCmd(f, streams), - NewExposeCmd(f, streams), NewDescribeReconfigureCmd(f, streams), NewExplainReconfigureCmd(f, streams), NewDiffConfigureCmd(f, streams), @@ -78,12 +88,12 @@ func NewClusterCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr { Message: "Backup/Restore Commands:", Commands: []*cobra.Command{ + NewListBackupPolicyCmd(f, streams), + NewEditBackupPolicyCmd(f, streams), NewCreateBackupCmd(f, streams), NewListBackupCmd(f, streams), NewDeleteBackupCmd(f, streams), NewCreateRestoreCmd(f, streams), - NewListRestoreCmd(f, streams), - NewDeleteRestoreCmd(f, streams), }, }, { diff --git a/internal/cli/cmd/cluster/cluster_test.go b/internal/cli/cmd/cluster/cluster_test.go index c43f6fa4c..372c614ce 100644 --- a/internal/cli/cmd/cluster/cluster_test.go +++ b/internal/cli/cmd/cluster/cluster_test.go @@ -1,23 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( - "os" + "fmt" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -29,20 +32,32 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) var _ = Describe("Cluster", func() { - const testComponentPath = "../../testing/testdata/component.yaml" - const testClassDefsPath = "../../testing/testdata/class.yaml" + const ( + testComponentPath = "../../testing/testdata/component.yaml" + testComponentWithClassPath = "../../testing/testdata/component_with_class_1c1g.yaml" + testComponentWithInvalidClassPath = "../../testing/testdata/component_with_invalid_class.yaml" + testComponentWithResourcePath = "../../testing/testdata/component_with_resource_1c1g.yaml" + testComponentWithInvalidResourcePath = "../../testing/testdata/component_with_invalid_resource.yaml" + testClusterPath = "../../testing/testdata/cluster.yaml" + ) + const ( + clusterName = "test" + namespace = "default" + ) var streams genericclioptions.IOStreams var tf *cmdtesting.TestFactory BeforeEach(func() { streams, _, _, _ = genericclioptions.NewTestIOStreams() - tf = cmdtesting.NewTestFactory().WithNamespace("default") + tf = cmdtesting.NewTestFactory().WithNamespace(namespace) cd := testing.FakeClusterDef() - tf.FakeDynamicClient = testing.FakeDynamicClient(cd, testing.FakeClusterVersion()) + fakeDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName, testing.IsDefautl) + tf.FakeDynamicClient = testing.FakeDynamicClient(cd, fakeDefaultStorageClass, testing.FakeClusterVersion()) tf.Client = &clientfake.RESTClient{} }) @@ -59,77 +74,302 @@ var _ = Describe("Cluster", func() { UpdatableFlags: UpdatableFlags{ TerminationPolicy: "Delete", }, - BaseOptions: create.BaseOptions{ - Dynamic: tf.FakeDynamicClient, + CreateOptions: create.CreateOptions{ + Factory: tf, + Dynamic: tf.FakeDynamicClient, + IOStreams: streams, }, } - o.IOStreams = streams + o.Options = o + Expect(o.Complete()).To(Succeed()) Expect(o.Validate()).To(Succeed()) Expect(o.Name).ShouldNot(BeEmpty()) + Expect(o.Run()).Should(HaveOccurred()) }) + }) - It("new command", func() { - cmd := NewCreateCmd(tf, streams) - Expect(cmd).ShouldNot(BeNil()) - Expect(cmd.Flags().Set("cluster-definition", testing.ClusterDefName)).Should(Succeed()) - Expect(cmd.Flags().Set("cluster-version", testing.ClusterVersionName)).Should(Succeed()) - Expect(cmd.Flags().Set("set-file", testComponentPath)).Should(Succeed()) - Expect(cmd.Flags().Set("termination-policy", "Delete")).Should(Succeed()) - - // must succeed otherwise exit 1 and make test fails - cmd.Run(nil, []string{"test1"}) - }) + Context("run", func() { + var o *CreateOptions - It("run", func() { + BeforeEach(func() { clusterDef := testing.FakeClusterDef() - tf.FakeDynamicClient = testing.FakeDynamicClient(clusterDef) - data, err := os.ReadFile(testClassDefsPath) - Expect(err).NotTo(HaveOccurred()) - clientSet := testing.FakeClientSet(testing.FakeComponentClassDef(clusterDef, data)) - o := &CreateOptions{ - BaseOptions: create.BaseOptions{IOStreams: streams, Name: "test", Dynamic: tf.FakeDynamicClient, ClientSet: clientSet}, + tf.FakeDynamicClient = testing.FakeDynamicClient( + clusterDef, + testing.FakeStorageClass(testing.StorageClassName, testing.IsDefautl), + testing.FakeClusterVersion(), + testing.FakeComponentClassDef(fmt.Sprintf("custom-%s", testing.ComponentDefName), clusterDef.Name, testing.ComponentDefName), + testing.FakeComponentClassDef("custom-mysql", clusterDef.Name, "mysql"), + ) + o = &CreateOptions{ + CreateOptions: create.CreateOptions{ + IOStreams: streams, + Name: clusterName, + Dynamic: tf.FakeDynamicClient, + CueTemplateName: CueTemplateName, + Factory: tf, + GVR: types.ClusterGVR(), + }, SetFile: "", ClusterDefRef: testing.ClusterDefName, - ClusterVersionRef: "cluster-version", + ClusterVersionRef: testing.ClusterVersionName, UpdatableFlags: UpdatableFlags{ PodAntiAffinity: "Preferred", TopologyKeys: []string{"kubernetes.io/hostname"}, NodeLabels: map[string]string{"testLabelKey": "testLabelValue"}, - TolerationsRaw: []string{"key=engineType,value=mongo,operator=Equal,effect=NoSchedule"}, + TolerationsRaw: []string{"engineType=mongo:NoSchedule"}, Tenancy: string(appsv1alpha1.SharedNode), }, } + o.TerminationPolicy = "WipeOut" + }) + + Run := func() { + o.CreateOptions.Options = o + o.Args = []string{clusterName} + Expect(o.CreateOptions.Complete()).Should(Succeed()) + Expect(o.Namespace).To(Equal(namespace)) + Expect(o.Name).To(Equal(clusterName)) + Expect(o.Run()).Should(Succeed()) + } + It("validate tolerations", func() { Expect(len(o.TolerationsRaw)).Should(Equal(1)) Expect(o.Complete()).Should(Succeed()) Expect(len(o.Tolerations)).Should(Equal(1)) + }) + + It("validate termination policy should be set", func() { + o.TerminationPolicy = "" Expect(o.Validate()).Should(HaveOccurred()) + }) - o.TerminationPolicy = "WipeOut" + It("should succeed if component with valid class", func() { + o.Values = []string{fmt.Sprintf("type=%s,class=%s", testing.ComponentDefName, testapps.Class1c1gName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if component with invalid class", func() { + o.Values = []string{fmt.Sprintf("type=%s,class=class-not-exists", testing.ComponentDefName)} + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should succeed if component with resource matching to one class", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=1,memory=1Gi", testing.ComponentDefName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should succeed if component with resource equivalent to class", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=1000m,memory=1024Mi", testing.ComponentDefName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if component with resource not matching any class", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=1,memory=2Gi", testing.ComponentDefName)} + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should succeed if component with cpu matching one class", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=1", testing.ComponentDefName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if component with cpu not matching any class", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=3", testing.ComponentDefName)} + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should succeed if component with memory matching one class", func() { + o.Values = []string{fmt.Sprintf("type=%s,memory=1Gi", testing.ComponentDefName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if component with memory not matching any class", func() { + o.Values = []string{fmt.Sprintf("type=%s,memory=7Gi", testing.ComponentDefName)} + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should succeed if component hasn't class definition", func() { + o.Values = []string{fmt.Sprintf("type=%s,cpu=3,memory=7Gi", testing.ExtraComponentDefName)} + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if create cluster by non-existed file", func() { o.SetFile = "test.yaml" - Expect(o.Complete()).ShouldNot(Succeed()) + Expect(o.Complete()).Should(HaveOccurred()) + }) + It("should succeed if create cluster by empty file", func() { o.SetFile = "" Expect(o.Complete()).Should(Succeed()) Expect(o.Validate()).Should(Succeed()) + Run() + }) + It("should succeed if create cluster by file without class and resource", func() { o.SetFile = testComponentPath Expect(o.Complete()).Should(Succeed()) Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should succeed if create cluster by file with class", func() { + o.SetFile = testComponentWithClassPath + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should succeed if create cluster by file with resource", func() { + o.SetFile = testComponentWithResourcePath + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Run() + }) + + It("should fail if create cluster by file with non-existed class", func() { + o.SetFile = testComponentWithInvalidClassPath + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should fail if create cluster by file with resource not matching any class", func() { + o.SetFile = testComponentWithInvalidResourcePath + Expect(o.Complete()).Should(HaveOccurred()) + }) + + It("should succeed if create cluster with a complete config file", func() { + o.SetFile = testClusterPath + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + }) + }) - inputs := create.Inputs{ - ResourceName: types.ResourceClusters, - CueTemplateName: CueTemplateName, - Options: o, - Factory: tf, + Context("create validate", func() { + var o *CreateOptions + BeforeEach(func() { + o = &CreateOptions{ + ClusterDefRef: testing.ClusterDefName, + ClusterVersionRef: testing.ClusterVersionName, + SetFile: testComponentPath, + UpdatableFlags: UpdatableFlags{ + TerminationPolicy: "Delete", + }, + CreateOptions: create.CreateOptions{ + Factory: tf, + Namespace: "default", + Name: "mycluster", + Dynamic: tf.FakeDynamicClient, + IOStreams: streams, + }, + ComponentSpecs: make([]map[string]interface{}, 1), } + o.ComponentSpecs[0] = make(map[string]interface{}) + o.ComponentSpecs[0]["volumeClaimTemplates"] = make([]interface{}, 1) + vct := o.ComponentSpecs[0]["volumeClaimTemplates"].([]interface{}) + vct[0] = make(map[string]interface{}) + vct[0].(map[string]interface{})["spec"] = make(map[string]interface{}) + spec := vct[0].(map[string]interface{})["spec"] + spec.(map[string]interface{})["storageClassName"] = testing.StorageClassName + }) - Expect(o.BaseOptions.Complete(inputs, []string{"test"})).Should(Succeed()) - Expect(o.Namespace).To(Equal("default")) - Expect(o.Name).To(Equal("test")) + It("can validate whether the ClusterDefRef is null when create a new cluster ", func() { + Expect(o.ClusterDefRef).ShouldNot(BeEmpty()) + Expect(o.Validate()).Should(Succeed()) + o.ClusterDefRef = "" + Expect(o.Validate()).Should(HaveOccurred()) + }) - Expect(o.Run(inputs)).Should(Succeed()) + It("can validate whether the TerminationPolicy is null when create a new cluster ", func() { + Expect(o.TerminationPolicy).ShouldNot(BeEmpty()) + Expect(o.Validate()).Should(Succeed()) + o.TerminationPolicy = "" + Expect(o.Validate()).Should(HaveOccurred()) }) + + It("can validate whether the ClusterVersionRef is null and can't get latest version from client when create a new cluster ", func() { + Expect(o.ClusterVersionRef).ShouldNot(BeEmpty()) + Expect(o.Validate()).Should(Succeed()) + o.ClusterVersionRef = "" + Expect(o.Validate()).Should(Succeed()) + }) + + It("can validate whether --set and --set-file both are specified when create a new cluster ", func() { + Expect(o.SetFile).ShouldNot(BeEmpty()) + Expect(o.Values).Should(BeNil()) + Expect(o.Validate()).Should(Succeed()) + o.Values = []string{"notEmpty"} + Expect(o.Validate()).Should(HaveOccurred()) + }) + + It("can validate whether the name is not specified and fail to generate a random cluster name when create a new cluster ", func() { + Expect(o.Name).ShouldNot(BeEmpty()) + Expect(o.Validate()).Should(Succeed()) + o.Name = "" + // Expected to generate a random name + Expect(o.Validate()).Should(Succeed()) + }) + + It("can validate whether the name is not longer than 16 characters when create a new cluster", func() { + Expect(len(o.Name)).Should(BeNumerically("<=", 16)) + Expect(o.Validate()).Should(Succeed()) + moreThan16 := 17 + bytes := make([]byte, 0) + var clusterNameMoreThan16 string + for i := 0; i < moreThan16; i++ { + bytes = append(bytes, byte(i%26+'a')) + } + clusterNameMoreThan16 = string(bytes) + Expect(len(clusterNameMoreThan16)).Should(BeNumerically(">", 16)) + o.Name = clusterNameMoreThan16 + Expect(o.Validate()).Should(HaveOccurred()) + }) + + Context("validate storageClass", func() { + It("can get all StorageClasses in K8S and check out if the cluster have a defalut StorageClasses by GetStorageClasses()", func() { + storageClasses, existedDefault, err := getStorageClasses(o.Dynamic) + Expect(err).Should(Succeed()) + Expect(storageClasses).Should(HaveKey(testing.StorageClassName)) + Expect(existedDefault).Should(BeTrue()) + fakeNotDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName, testing.IsNotDefault) + cd := testing.FakeClusterDef() + tf.FakeDynamicClient = testing.FakeDynamicClient(cd, fakeNotDefaultStorageClass, testing.FakeClusterVersion()) + storageClasses, existedDefault, err = getStorageClasses(tf.FakeDynamicClient) + Expect(err).Should(Succeed()) + Expect(storageClasses).Should(HaveKey(testing.StorageClassName)) + Expect(existedDefault).ShouldNot(BeTrue()) + }) + + It("can specify the StorageClass and the StorageClass must exist", func() { + Expect(validateStorageClass(o.Dynamic, o.ComponentSpecs)).Should(Succeed()) + fakeNotDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName+"-other", testing.IsNotDefault) + cd := testing.FakeClusterDef() + FakeDynamicClientWithNotDefaultSC := testing.FakeDynamicClient(cd, fakeNotDefaultStorageClass, testing.FakeClusterVersion()) + Expect(validateStorageClass(FakeDynamicClientWithNotDefaultSC, o.ComponentSpecs)).Should(HaveOccurred()) + }) + + It("can get valiate the default StorageClasses", func() { + vct := o.ComponentSpecs[0]["volumeClaimTemplates"].([]interface{}) + spec := vct[0].(map[string]interface{})["spec"] + delete(spec.(map[string]interface{}), "storageClassName") + Expect(validateStorageClass(o.Dynamic, o.ComponentSpecs)).Should(Succeed()) + fakeNotDefaultStorageClass := testing.FakeStorageClass(testing.StorageClassName+"-other", testing.IsNotDefault) + cd := testing.FakeClusterDef() + FakeDynamicClientWithNotDefaultSC := testing.FakeDynamicClient(cd, fakeNotDefaultStorageClass, testing.FakeClusterVersion()) + Expect(validateStorageClass(FakeDynamicClientWithNotDefaultSC, o.ComponentSpecs)).Should(HaveOccurred()) + }) + }) + }) It("delete", func() { diff --git a/internal/cli/cmd/cluster/config.go b/internal/cli/cmd/cluster/config.go index a919a12d8..d18799126 100644 --- a/internal/cli/cmd/cluster/config.go +++ b/internal/cli/cmd/cluster/config.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -92,33 +95,33 @@ var ( kbcli cluster describe-config mycluster # describe a component, e.g. cluster name is mycluster, component name is mysql - kbcli cluster describe-config mycluster --component-name=mysql + kbcli cluster describe-config mycluster --component=mysql - # describe all configuration files. - kbcli cluster describe-config mycluster --component-name=mysql --show-detail + # describe all configuration files. + kbcli cluster describe-config mycluster --component=mysql --show-detail - # describe a content of configuration file. - kbcli cluster describe-config mycluster --component-name=mysql --config-file=my.cnf --show-detail`) + # describe a content of configuration file. + kbcli cluster describe-config mycluster --component=mysql --config-file=my.cnf --show-detail`) explainReconfigureExample = templates.Examples(` - # describe a cluster, e.g. cluster name is mycluster + # explain a cluster, e.g. cluster name is mycluster kbcli cluster explain-config mycluster - # describe a specified configure template, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl + # explain a specified configure template, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl - # describe a specified configure template, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl --trunc-document=false --trunc-enum=false + # explain a specified configure template, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --component=mysql --config-specs=mysql-3node-tpl --trunc-document=false --trunc-enum=false - # describe a specified parameters, e.g. cluster name is mycluster - kbcli cluster explain-config mycluster --component-name=mysql --config-specs=mysql-3node-tpl --param=sql_mode`) + # explain a specified parameters, e.g. cluster name is mycluster + kbcli cluster explain-config mycluster --param=sql_mode`) diffConfigureExample = templates.Examples(` - # compare config files + # compare config files kbcli cluster diff-config opsrequest1 opsrequest2`) ) func (r *reconfigureOptions) addCommonFlags(cmd *cobra.Command) { - cmd.Flags().StringVar(&r.componentName, "component-name", "", "Specify the name of Component to be describe (e.g. for apecloud-mysql: --component-name=mysql). If the cluster has only one component, unset the parameter.\"") - cmd.Flags().StringSliceVar(&r.configSpecs, "config-specs", nil, "Specify the name of the configuration template to be describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl)") + cmd.Flags().StringVar(&r.componentName, "component", "", "Specify the name of Component to describe (e.g. for apecloud-mysql: --component=mysql). If the cluster has only one component, unset the parameter.\"") + cmd.Flags().StringSliceVar(&r.configSpecs, "config-specs", nil, "Specify the name of the configuration template to describe. (e.g. for apecloud-mysql: --config-specs=mysql-3node-tpl)") } func (r *reconfigureOptions) validate() error { @@ -133,7 +136,7 @@ func (r *reconfigureOptions) validate() error { } if r.isExplain && len(r.configSpecs) != 1 { - return cfgcore.MakeError("explain require one template") + return cfgcore.MakeError("explain command requires one template") } for _, tplName := range r.configSpecs { @@ -142,7 +145,7 @@ func (r *reconfigureOptions) validate() error { return err } if r.isExplain && len(tpl.ConfigConstraintRef) == 0 { - return cfgcore.MakeError("explain command require template has config constraint options") + return cfgcore.MakeError("explain command requires template with config constraint options") } } return nil @@ -178,7 +181,7 @@ func (r *reconfigureOptions) complete2(args []string) error { return err } if len(r.tpls) == 0 { - return cfgcore.MakeError("not any config template, not support describe") + return cfgcore.MakeError("config template is not set") } templateNames := make([]string, 0, len(r.tpls)) @@ -225,7 +228,7 @@ func (r *reconfigureOptions) syncClusterComponent() error { return makeClusterNotExistErr(r.clusterName) } if len(componentNames) != 1 { - return cfgcore.MakeError("when multi component exist, must specify which component to use.") + return cfgcore.MakeError("please specify a component as there are more than one component in cluster.") } r.componentName = componentNames[0] return nil @@ -307,7 +310,7 @@ func (r *reconfigureOptions) getReconfigureMeta() ([]types.ConfigTemplateInfo, e Name: cmName, Namespace: r.namespace, }, r.dynamic, cmObj); err != nil { - return nil, cfgcore.WrapError(err, "template config instance is not exist, template name: %s, cfg name: %s", tplName, cmName) + return nil, cfgcore.WrapError(err, "config not found, template name: %s, cfg name: %s", tplName, cmName) } configs = append(configs, types.ConfigTemplateInfo{ Name: tplName, diff --git a/internal/cli/cmd/cluster/config_edit.go b/internal/cli/cmd/cluster/config_edit.go index 073aea666..662a82e0d 100644 --- a/internal/cli/cmd/cluster/config_edit.go +++ b/internal/cli/cmd/cluster/config_edit.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -34,6 +37,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/prompt" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + cfgcm "github.com/apecloud/kubeblocks/internal/configuration/config_manager" ) type editConfigOptions struct { @@ -43,17 +47,18 @@ type editConfigOptions struct { replaceFile bool } -var editConfigExample = templates.Examples(` - # edit config for component - kbcli cluster edit-config [--component=] [--config-spec=] [--config-file=] +var ( + editConfigUse = "edit-config NAME [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file]" + editConfigExample = templates.Examples(` # update mysql max_connections, cluster name is mycluster kbcli cluster edit-config mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf `) +) func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv1alpha1.ConfigConstraintSpec) error) error { wrapper := o.wrapper - cfgEditContext := newConfigContext(o.BaseOptions, o.Name, wrapper.ComponentName(), wrapper.ConfigSpecName(), wrapper.ConfigFile()) + cfgEditContext := newConfigContext(o.CreateOptions, o.Name, wrapper.ComponentName(), wrapper.ConfigSpecName(), wrapper.ConfigFile()) if err := cfgEditContext.prepare(); err != nil { return err } @@ -66,7 +71,7 @@ func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv return err } - diff, err := cfgEditContext.getUnifiedDiffString() + diff, err := util.GetUnifiedDiffString(cfgEditContext.original, cfgEditContext.edited) if err != nil { return err } @@ -75,7 +80,7 @@ func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv return nil } - displayDiffWithColor(o.IOStreams.Out, diff) + util.DisplayDiffWithColor(o.IOStreams.Out, diff) oldVersion := map[string]string{ o.CfgFile: cfgEditContext.getOriginal(), @@ -84,7 +89,7 @@ func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv o.CfgFile: cfgEditContext.getEdited(), } - configSpec := wrapper.ConfigSpec() + configSpec := wrapper.ConfigTemplateSpec() configConstraintKey := client.ObjectKey{ Namespace: "", Name: configSpec.ConfigConstraintRef, @@ -114,7 +119,7 @@ func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv } confirmPrompt := confirmApplyReconfigurePrompt - if !dynamicUpdated { + if !dynamicUpdated || !cfgcm.IsSupportReload(configConstraint.Spec.ReloadOptions) { confirmPrompt = restartConfirmPrompt } yes, err := o.confirmReconfigure(confirmPrompt) @@ -124,6 +129,14 @@ func (o *editConfigOptions) Run(fn func(info *cfgcore.ConfigPatchInfo, cc *appsv if !yes { return nil } + + validatedData := map[string]string{ + o.CfgFile: cfgEditContext.getEdited(), + } + options := cfgcore.WithKeySelector(wrapper.ConfigTemplateSpec().Keys) + if err = cfgcore.NewConfigValidator(&configConstraint.Spec, options).Validate(validatedData); err != nil { + return cfgcore.WrapError(err, "failed to validate edited config") + } return fn(configPatch, &configConstraint.Spec) } @@ -133,7 +146,7 @@ func (o *editConfigOptions) confirmReconfigure(promptStr string) (bool, error) { confirmStr := []string{yesStr, noStr} printer.Warning(o.Out, promptStr) - input, err := prompt.NewPrompt("Please type [yes/No] to confirm:", + input, err := prompt.NewPrompt("Please type [Yes/No] to confirm:", func(input string) error { if !slices.Contains(confirmStr, strings.ToLower(input)) { return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr) @@ -148,40 +161,32 @@ func (o *editConfigOptions) confirmReconfigure(promptStr string) (bool, error) { // NewEditConfigureCmd shows the difference between two configuration version. func NewEditConfigureCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - editOptions := &editConfigOptions{ + o := &editConfigOptions{ configOpsOptions: configOpsOptions{ editMode: true, - OperationsOptions: newBaseOperationsOptions(streams, appsv1alpha1.ReconfiguringType, false), + OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false), }} - inputs := buildOperationsInputs(f, editOptions.OperationsOptions) - inputs.Use = "edit-config" - inputs.Short = "Edit the config file of the component." - inputs.Example = editConfigExample - inputs.BuildFlags = func(cmd *cobra.Command) { - editOptions.buildReconfigureCommonFlags(cmd) - cmd.Flags().BoolVar(&editOptions.replaceFile, "replace", false, "Specify whether to replace the config file. Default to false.") - } - inputs.Complete = editOptions.Complete - inputs.Validate = editOptions.Validate cmd := &cobra.Command{ - Use: inputs.Use, - Short: inputs.Short, - Example: inputs.Example, + Use: editConfigUse, + Short: "Edit the config file of the component.", + Example: editConfigExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(inputs.BaseOptionsObj.Complete(inputs, args)) - util.CheckErr(inputs.BaseOptionsObj.Validate(inputs)) - util.CheckErr(editOptions.Run(func(info *cfgcore.ConfigPatchInfo, cc *appsv1alpha1.ConfigConstraintSpec) error { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + util.CheckErr(o.Complete()) + util.CheckErr(o.Validate()) + util.CheckErr(o.Run(func(info *cfgcore.ConfigPatchInfo, cc *appsv1alpha1.ConfigConstraintSpec) error { // generate patch for config formatterConfig := cc.FormatterConfig params := cfgcore.GenerateVisualizedParamsList(info, formatterConfig, nil) - editOptions.KeyValues = fromKeyValuesToMap(params, editOptions.CfgFile) - return inputs.BaseOptionsObj.Run(inputs) + o.KeyValues = fromKeyValuesToMap(params, o.CfgFile) + return o.CreateOptions.Run() })) }, } - if inputs.BuildFlags != nil { - inputs.BuildFlags(cmd) - } + o.buildReconfigureCommonFlags(cmd) + cmd.Flags().BoolVar(&o.replaceFile, "replace", false, "Boolean flag to enable replacing config file. Default with false.") return cmd } diff --git a/internal/cli/cmd/cluster/config_ops.go b/internal/cli/cmd/cluster/config_ops.go index ff48df16c..7657cb722 100644 --- a/internal/cli/cmd/cluster/config_ops.go +++ b/internal/cli/cmd/cluster/config_ops.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -27,12 +30,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/prompt" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + cfgcm "github.com/apecloud/kubeblocks/internal/configuration/config_manager" ) type configOpsOptions struct { @@ -50,10 +53,11 @@ type configOpsOptions struct { var ( createReconfigureExample = templates.Examples(` # update component params - kbcli cluster configure --component= --config-spec= --config-file= --set max_connections=1000,general_log=OFF + kbcli cluster configure mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf --set max_connections=1000,general_log=OFF + # if only one component, and one config spec, and one config file, simplify the searching process of configure. e.g: # update mysql max_connections, cluster name is mycluster - kbcli cluster configure mycluster --component=mysql --config-spec=mysql-3node-tpl --config-file=my.cnf --set max_connections=2000 + kbcli cluster configure mycluster --set max_connections=2000 `) ) @@ -70,7 +74,7 @@ func (o *configOpsOptions) Complete() error { o.KeyValues = kvs } - wrapper, err := newConfigWrapper(o.BaseOptions, o.Name, o.ComponentName, o.CfgTemplateName, o.CfgFile, o.KeyValues) + wrapper, err := newConfigWrapper(o.CreateOptions, o.Name, o.ComponentName, o.CfgTemplateName, o.CfgFile, o.KeyValues) if err != nil { return err } @@ -92,7 +96,7 @@ func (o *configOpsOptions) Validate() error { if o.editMode { return nil } - if err := o.validateConfigParams(o.wrapper.ConfigSpec()); err != nil { + if err := o.validateConfigParams(o.wrapper.ConfigTemplateSpec()); err != nil { return err } o.printConfigureTips() @@ -128,6 +132,10 @@ func (o *configOpsOptions) checkChangedParamsAndDoubleConfirm(cc *appsv1alpha1.C return r } + if !cfgcm.IsSupportReload(cc.ReloadOptions) { + return o.confirmReconfigureWithRestart() + } + configPatch, _, err := cfgcore.CreateConfigPatch(mockEmptyData(data), data, cc.FormatterConfig.Format, tpl.Keys, false) if err != nil { return err @@ -144,12 +152,15 @@ func (o *configOpsOptions) checkChangedParamsAndDoubleConfirm(cc *appsv1alpha1.C } func (o *configOpsOptions) confirmReconfigureWithRestart() error { + if o.autoApprove { + return nil + } const confirmStr = "yes" printer.Warning(o.Out, restartConfirmPrompt) _, err := prompt.NewPrompt(fmt.Sprintf("Please type \"%s\" to confirm:", confirmStr), func(input string) error { if input != confirmStr { - return fmt.Errorf("typed \"%s\" does not match \"%s\"", input, confirmStr) + return fmt.Errorf("typed \"%s\" not match \"%s\"", input, confirmStr) } return nil }, o.In).Run() @@ -184,30 +195,38 @@ func (o *configOpsOptions) printConfigureTips() { printer.NewPair("ClusterName", o.Name)) } -// buildCommonFlags build common flags for operations command +// buildReconfigureCommonFlags build common flags for reconfigure command func (o *configOpsOptions) buildReconfigureCommonFlags(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().StringSliceVar(&o.Parameters, "set", nil, "Specify updated parameter list. For details about the parameters, refer to kbcli sub command: 'kbcli cluster describe-config'.") + o.addCommonFlags(cmd) + cmd.Flags().StringSliceVar(&o.Parameters, "set", nil, "Specify parameters list to be updated. For more details, refer to 'kbcli cluster describe-config'.") cmd.Flags().StringVar(&o.ComponentName, "component", "", "Specify the name of Component to be updated. If the cluster has only one component, unset the parameter.") - cmd.Flags().StringVar(&o.CfgTemplateName, "config-spec", "", "Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'.") - cmd.Flags().StringVar(&o.CfgFile, "config-file", "", "Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). What templates or configure files are available for this cluster can refer to kbcli sub command: 'kbcli cluster describe-config'.") + cmd.Flags().StringVar(&o.CfgTemplateName, "config-spec", "", "Specify the name of the configuration template to be updated (e.g. for apecloud-mysql: --config-spec=mysql-3node-tpl). "+ + "For available templates and configs, refer to: 'kbcli cluster describe-config'.") + cmd.Flags().StringVar(&o.CfgFile, "config-file", "", "Specify the name of the configuration file to be updated (e.g. for mysql: --config-file=my.cnf). "+ + "For available templates and configs, refer to: 'kbcli cluster describe-config'.") } // NewReconfigureCmd creates a Reconfiguring command func NewReconfigureCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := &configOpsOptions{ editMode: false, - OperationsOptions: newBaseOperationsOptions(streams, appsv1alpha1.ReconfiguringType, false), - } - inputs := buildOperationsInputs(f, o.OperationsOptions) - inputs.Use = "configure" - inputs.Short = "Reconfigure parameters with the specified components in the cluster." - inputs.Example = createReconfigureExample - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildReconfigureCommonFlags(cmd) - } - - inputs.Complete = o.Complete - inputs.Validate = o.Validate - return create.BuildCommand(inputs) + OperationsOptions: newBaseOperationsOptions(f, streams, appsv1alpha1.ReconfiguringType, false), + } + cmd := &cobra.Command{ + Use: "configure NAME --set key=value[,key=value] [--component=component-name] [--config-spec=config-spec-name] [--config-file=config-file]", + Short: "Configure parameters with the specified components in the cluster.", + Example: createReconfigureExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.buildReconfigureCommonFlags(cmd) + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before reconfiguring the cluster") + return cmd } diff --git a/internal/cli/cmd/cluster/config_ops_test.go b/internal/cli/cmd/cluster/config_ops_test.go index 236fa865e..834c17e27 100644 --- a/internal/cli/cmd/cluster/config_ops_test.go +++ b/internal/cli/cmd/cluster/config_ops_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -66,14 +69,14 @@ var _ = Describe("reconfigure test", func() { It("check params for reconfiguring operations", func() { const ( - ns = "default" - clusterDefName = "test-clusterdef" - clusterVersionName = "test-clusterversion" - clusterName = "test-cluster" - statefulCompType = "replicasets" - statefulCompName = "mysql" - configSpecName = "mysql-config-tpl" - configVolumeName = "mysql-config" + ns = "default" + clusterDefName = "test-clusterdef" + clusterVersionName = "test-clusterversion" + clusterName = "test-cluster" + statefulCompDefName = "replicasets" + statefulCompName = "mysql" + configSpecName = "mysql-config-tpl" + configVolumeName = "mysql-config" ) By("Create configmap and config constraint obj") @@ -83,30 +86,30 @@ var _ = Describe("reconfigure test", func() { componentConfig := testapps.NewConfigMap(ns, cfgcore.GetComponentCfgName(clusterName, statefulCompName, configSpecName), testapps.SetConfigMapData("my.cnf", "")) By("Create a clusterDefinition obj") clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, statefulCompType). + AddComponentDef(testapps.StatefulMySQLComponent, statefulCompDefName). AddConfigTemplate(configSpecName, configmap.Name, constraint.Name, ns, configVolumeName). GetObject() By("Create a clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(statefulCompType). + AddComponentVersion(statefulCompDefName). GetObject() By("creating a cluster") clusterObj := testapps.NewClusterFactory(ns, clusterName, clusterDefObj.Name, ""). - AddComponent(statefulCompName, statefulCompType).GetObject() + AddComponent(statefulCompName, statefulCompDefName).GetObject() objs := []runtime.Object{configmap, constraint, clusterDefObj, clusterVersionObj, clusterObj, componentConfig} ttf, ops := NewFakeOperationsOptions(ns, clusterObj.Name, appsv1alpha1.ReconfiguringType, objs...) o := &configOpsOptions{ // nil cannot be set to a map struct in CueLang, so init the map of KeyValues. OperationsOptions: &OperationsOptions{ - BaseOptions: *ops, + CreateOptions: *ops, }, } o.KeyValues = make(map[string]string) defer ttf.Cleanup() - By("validate reconfiguring parameter") + By("validate reconfiguring parameters") o.ComponentNames = []string{statefulCompName} _, err := o.parseUpdatedParams() Expect(err.Error()).To(ContainSubstring(missingUpdatedParametersErrMessage)) @@ -124,7 +127,7 @@ var _ = Describe("reconfigure test", func() { in := &bytes.Buffer{} in.Write([]byte("yes\n")) - o.BaseOptions.In = io.NopCloser(in) + o.CreateOptions.In = io.NopCloser(in) Expect(o.Validate()).Should(Succeed()) }) diff --git a/internal/cli/cmd/cluster/config_util.go b/internal/cli/cmd/cluster/config_util.go index 8d03756f7..45b31a842 100644 --- a/internal/cli/cmd/cluster/config_util.go +++ b/internal/cli/cmd/cluster/config_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -19,12 +22,8 @@ package cluster import ( "bytes" "fmt" - "io" "path/filepath" - "strings" - "github.com/fatih/color" - "github.com/pmezard/go-difflib/difflib" corev1 "k8s.io/api/core/v1" "k8s.io/kubectl/pkg/cmd/util/editor" "sigs.k8s.io/controller-runtime/pkg/client" @@ -33,10 +32,11 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + cfgutil "github.com/apecloud/kubeblocks/internal/configuration/util" ) type configEditContext struct { - create.BaseOptions + create.CreateOptions clusterName string componentName string @@ -67,7 +67,7 @@ func (c *configEditContext) prepare() error { val, ok := cmObj.Data[c.configKey] if !ok { - return makeNotFoundConfigFileErr(c.configKey, c.configSpecName, cfgcore.ToSet(cmObj.Data).AsSlice()) + return makeNotFoundConfigFileErr(c.configKey, c.configSpecName, cfgutil.ToSet(cmObj.Data).AsSlice()) } c.original = val @@ -84,20 +84,9 @@ func (c *configEditContext) editConfig(editor editor.Editor) error { return nil } -func (c *configEditContext) getUnifiedDiffString() (string, error) { - diff := difflib.UnifiedDiff{ - A: difflib.SplitLines(c.original), - B: difflib.SplitLines(c.edited), - FromFile: "Original", - ToFile: "Current", - Context: 3, - } - return difflib.GetUnifiedDiffString(diff) -} - -func newConfigContext(baseOptions create.BaseOptions, clusterName, componentName, configSpec, file string) *configEditContext { +func newConfigContext(baseOptions create.CreateOptions, clusterName, componentName, configSpec, file string) *configEditContext { return &configEditContext{ - BaseOptions: baseOptions, + CreateOptions: baseOptions, clusterName: clusterName, componentName: componentName, configSpecName: configSpec, @@ -105,22 +94,6 @@ func newConfigContext(baseOptions create.BaseOptions, clusterName, componentName } } -func displayDiffWithColor(out io.Writer, diffText string) { - for _, line := range difflib.SplitLines(diffText) { - switch { - case strings.HasPrefix(line, "---"), strings.HasPrefix(line, "+++"): - line = color.HiYellowString(line) - case strings.HasPrefix(line, "@@"): - line = color.HiBlueString(line) - case strings.HasPrefix(line, "-"): - line = color.RedString(line) - case strings.HasPrefix(line, "+"): - line = color.GreenString(line) - } - fmt.Fprint(out, line) - } -} - func fromKeyValuesToMap(params []cfgcore.VisualizedParam, file string) map[string]string { result := make(map[string]string) for _, param := range params { diff --git a/internal/cli/cmd/cluster/config_util_test.go b/internal/cli/cmd/cluster/config_util_test.go index 0dfe2d71a..b239e9069 100644 --- a/internal/cli/cmd/cluster/config_util_test.go +++ b/internal/cli/cmd/cluster/config_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -29,10 +32,10 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/types" ) -func NewFakeOperationsOptions(ns, cName string, opsType appsv1alpha1.OpsType, objs ...runtime.Object) (*cmdtesting.TestFactory, *create.BaseOptions) { +func NewFakeOperationsOptions(ns, cName string, opsType appsv1alpha1.OpsType, objs ...runtime.Object) (*cmdtesting.TestFactory, *create.CreateOptions) { streams, _, _, _ := genericclioptions.NewTestIOStreams() tf := cmdtesting.NewTestFactory().WithNamespace(ns) - baseOptions := &create.BaseOptions{ + baseOptions := &create.CreateOptions{ IOStreams: streams, Name: cName, Namespace: ns, diff --git a/internal/cli/cmd/cluster/config_wrapper.go b/internal/cli/cmd/cluster/config_wrapper.go index 9543683c8..9f265f5fb 100644 --- a/internal/cli/cmd/cluster/config_wrapper.go +++ b/internal/cli/cmd/cluster/config_wrapper.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -26,28 +29,29 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + cfgutil "github.com/apecloud/kubeblocks/internal/configuration/util" ) type configWrapper struct { - create.BaseOptions + create.CreateOptions clusterName string updatedParams map[string]string - // auto fill field + // autofill field componentName string configSpecName string - configKey string + configFileKey string - configSpec appsv1alpha1.ComponentConfigSpec + configTemplateSpec appsv1alpha1.ComponentConfigSpec clusterObj *appsv1alpha1.Cluster clusterDefObj *appsv1alpha1.ClusterDefinition clusterVerObj *appsv1alpha1.ClusterVersion } -func (w *configWrapper) ConfigSpec() *appsv1alpha1.ComponentConfigSpec { - return &w.configSpec +func (w *configWrapper) ConfigTemplateSpec() *appsv1alpha1.ComponentConfigSpec { + return &w.configTemplateSpec } func (w *configWrapper) ConfigSpecName() string { @@ -59,10 +63,10 @@ func (w *configWrapper) ComponentName() string { } func (w *configWrapper) ConfigFile() string { - return w.configKey + return w.configFileKey } -// AutoFillRequiredParam auto fill required param. +// AutoFillRequiredParam auto fills required param. func (w *configWrapper) AutoFillRequiredParam() error { if err := w.fillComponent(); err != nil { return err @@ -73,14 +77,14 @@ func (w *configWrapper) AutoFillRequiredParam() error { return w.fillConfigFile() } -// ValidateRequiredParam validate required param. +// ValidateRequiredParam validates required param. func (w *configWrapper) ValidateRequiredParam() error { - // step1: validate component exist. + // step1: check existence of component. if w.clusterObj.Spec.GetComponentByName(w.componentName) == nil { return makeComponentNotExistErr(w.clusterName, w.componentName) } - // step2: validate configmap exist. + // step2: check existence of configmap cmObj := corev1.ConfigMap{} cmKey := client.ObjectKey{ Name: cfgcore.GetComponentCfgName(w.clusterName, w.componentName, w.configSpecName), @@ -90,14 +94,14 @@ func (w *configWrapper) ValidateRequiredParam() error { return err } - // step3: validate fileKey exist. - if _, ok := cmObj.Data[w.configKey]; !ok { - return makeNotFoundConfigFileErr(w.configKey, w.configSpecName, cfgcore.ToSet(cmObj.Data).AsSlice()) + // step3: check existence of config file + if _, ok := cmObj.Data[w.configFileKey]; !ok { + return makeNotFoundConfigFileErr(w.configFileKey, w.configSpecName, cfgutil.ToSet(cmObj.Data).AsSlice()) } // TODO support all config file update. - if !cfgcore.CheckConfigTemplateReconfigureKey(w.configSpec, w.configKey) { - return makeNotSupportConfigFileUpdateErr(w.configKey, w.configSpec) + if !cfgcore.IsSupportConfigFileReconfigure(w.configTemplateSpec, w.configFileKey) { + return makeNotSupportConfigFileUpdateErr(w.configFileKey, w.configTemplateSpec) } return nil } @@ -121,7 +125,7 @@ func (w *configWrapper) fillConfigSpec() error { foundConfigSpec := func(configSpecs []appsv1alpha1.ComponentConfigSpec, name string) *appsv1alpha1.ComponentConfigSpec { for _, configSpec := range configSpecs { if configSpec.Name == name { - w.configSpec = configSpec + w.configTemplateSpec = configSpec return &configSpec } } @@ -151,7 +155,7 @@ func (w *configWrapper) fillConfigSpec() error { return nil } - w.configSpec = configSpecs[0] + w.configTemplateSpec = configSpecs[0] if len(configSpecs) == 1 { w.configSpecName = configSpecs[0].Name return nil @@ -164,7 +168,7 @@ func (w *configWrapper) fillConfigSpec() error { } } if len(supportUpdatedTpl) == 1 { - w.configSpec = configSpecs[0] + w.configTemplateSpec = configSpecs[0] w.configSpecName = supportUpdatedTpl[0].Name return nil } @@ -172,11 +176,11 @@ func (w *configWrapper) fillConfigSpec() error { } func (w *configWrapper) fillConfigFile() error { - if w.configKey != "" { + if w.configFileKey != "" { return nil } - if w.configSpec.TemplateRef == "" { + if w.configTemplateSpec.TemplateRef == "" { return makeNotFoundTemplateErr(w.clusterName, w.componentName) } @@ -189,12 +193,12 @@ func (w *configWrapper) fillConfigFile() error { return err } if len(cmObj.Data) == 0 { - return cfgcore.MakeError("not support reconfiguring because there is no config file.") + return cfgcore.MakeError("not supported reconfiguring because there is no config file.") } keys := w.filterForReconfiguring(cmObj.Data) if len(keys) == 1 { - w.configKey = keys[0] + w.configFileKey = keys[0] return nil } return cfgcore.MakeError(multiConfigFileErrorMessage) @@ -202,15 +206,15 @@ func (w *configWrapper) fillConfigFile() error { func (w *configWrapper) filterForReconfiguring(data map[string]string) []string { keys := make([]string, 0, len(data)) - for k := range data { - if cfgcore.CheckConfigTemplateReconfigureKey(w.configSpec, k) { - keys = append(keys, k) + for configFileKey := range data { + if cfgcore.IsSupportConfigFileReconfigure(w.configTemplateSpec, configFileKey) { + keys = append(keys, configFileKey) } } return keys } -func newConfigWrapper(baseOptions create.BaseOptions, clusterName, componentName, configSpec, configKey string, params map[string]string) (*configWrapper, error) { +func newConfigWrapper(baseOptions create.CreateOptions, clusterName, componentName, configSpec, configKey string, params map[string]string) (*configWrapper, error) { var ( err error clusterObj *appsv1alpha1.Cluster @@ -225,14 +229,14 @@ func newConfigWrapper(baseOptions create.BaseOptions, clusterName, componentName } w := &configWrapper{ - BaseOptions: baseOptions, + CreateOptions: baseOptions, clusterObj: clusterObj, clusterDefObj: clusterDefObj, clusterName: clusterName, componentName: componentName, configSpecName: configSpec, - configKey: configKey, + configFileKey: configKey, updatedParams: params, } diff --git a/internal/cli/cmd/cluster/connect.go b/internal/cli/cmd/cluster/connect.go index fc8cf3fa7..6000e000a 100644 --- a/internal/cli/cmd/cluster/connect.go +++ b/internal/cli/cmd/cluster/connect.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -19,32 +22,40 @@ package cluster import ( "context" "fmt" + "os" "strings" "github.com/spf13/cobra" + "golang.org/x/crypto/ssh/terminal" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - computil "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/cli/cluster" - "github.com/apecloud/kubeblocks/internal/cli/engine" "github.com/apecloud/kubeblocks/internal/cli/exec" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/sqlchannel/engine" ) var connectExample = templates.Examples(` - # connect to a specified cluster, default connect to the leader or primary instance + # connect to a specified cluster, default connect to the leader/primary instance kbcli cluster connect mycluster + # connect to cluster as user + kbcli cluster connect mycluster --as-user myuser + # connect to a specified instance kbcli cluster connect -i mycluster-instance-0 + # connect to a specified component + kbcli cluster connect mycluster --component mycomponent + # show cli connection example kbcli cluster connect mycluster --show-example --client=cli @@ -55,7 +66,9 @@ var connectExample = templates.Examples(` kbcli cluster connect mycluster --show-example`) type ConnectOptions struct { - name string + clusterName string + componentName string + clientType string showExample bool engine engine.Interface @@ -63,10 +76,18 @@ type ConnectOptions struct { privateEndPoint bool svc *corev1.Service + component *appsv1alpha1.ClusterComponentSpec + componentDef *appsv1alpha1.ClusterComponentDefinition + targetCluster *appsv1alpha1.Cluster + targetClusterDef *appsv1alpha1.ClusterDefinition + + userName string + userPasswd string + *exec.ExecOptions } -// NewConnectCmd return the cmd of connecting a cluster +// NewConnectCmd returns the cmd of connecting to a cluster func NewConnectCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := &ConnectOptions{ExecOptions: exec.NewExecOptions(f, streams)} cmd := &cobra.Command{ @@ -75,17 +96,22 @@ func NewConnectCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr Example: connectExample, ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(o.ExecOptions.Complete()) + util.CheckErr(o.validate(args)) + util.CheckErr(o.complete()) if o.showExample { - util.CheckErr(o.runShowExample(args)) + util.CheckErr(o.runShowExample()) } else { - util.CheckErr(o.connect(args)) + util.CheckErr(o.connect()) } }, } cmd.Flags().StringVarP(&o.PodName, "instance", "i", "", "The instance name to connect.") - cmd.Flags().BoolVar(&o.showExample, "show-example", false, "Show how to connect to cluster or instance from different client.") + cmd.Flags().StringVar(&o.componentName, "component", "", "The component to connect. If not specified, pick up the first one.") + cmd.Flags().BoolVar(&o.showExample, "show-example", false, "Show how to connect to cluster/instance from different clients.") cmd.Flags().StringVar(&o.clientType, "client", "", "Which client connection example should be output, only valid if --show-example is true.") + + cmd.Flags().StringVar(&o.userName, "as-user", "", "Connect to cluster as user") + util.CheckErr(cmd.RegisterFlagCompletionFunc("client", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var types []string for _, t := range engine.ClientTypes() { @@ -98,28 +124,22 @@ func NewConnectCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr return cmd } -func (o *ConnectOptions) runShowExample(args []string) error { - if len(args) > 1 { - return fmt.Errorf("only support to connect one cluster") - } - - if len(args) == 0 { - return fmt.Errorf("cluster name should be specified when --show-example is true") - } - - o.name = args[0] - +func (o *ConnectOptions) runShowExample() error { // get connection info info, err := o.getConnectionInfo() if err != nil { return err } + // make sure engine is initialized + if o.engine == nil { + return fmt.Errorf("engine is not initialized yet") + } - // if cluster does not have public endpoints, tell user to use port-forward command and - // connect cluster from local host + // if cluster does not have public endpoints, prompts to use port-forward command and + // connect cluster from localhost if o.privateEndPoint { - fmt.Fprintf(o.Out, "# cluster %s does not have public endpoints, you can run following command and connect cluster from local host\n"+ - "kubectl port-forward service/%s %s:%s\n\n", o.name, o.svc.Name, info.Port, info.Port) + fmt.Fprintf(o.Out, "# cluster %s does not have public endpoints, you can run following command and connect cluster from localhost\n"+ + "kubectl port-forward service/%s %s:%s\n\n", o.clusterName, o.svc.Name, info.Port, info.Port) info.Host = "127.0.0.1" } @@ -127,59 +147,161 @@ func (o *ConnectOptions) runShowExample(args []string) error { return nil } -// connect create parameters for connecting cluster and connect -func (o *ConnectOptions) connect(args []string) error { +func (o *ConnectOptions) validate(args []string) error { if len(args) > 1 { return fmt.Errorf("only support to connect one cluster") } - if len(args) == 0 && len(o.PodName) == 0 { - return fmt.Errorf("cluster name or instance name should be specified") + // cluster name and pod instance are mutual exclusive + if len(o.PodName) > 0 { + if len(args) > 0 { + return fmt.Errorf("specify either cluster name or instance name, they are exclusive") + } + if len(o.componentName) > 0 { + return fmt.Errorf("component name is valid only when cluster name is specified") + } + } else if len(args) == 0 { + return fmt.Errorf("either cluster name or instance name should be specified") } + // set custer name if len(args) > 0 { - o.name = args[0] + o.clusterName = args[0] } - // get target pod name, if not specified, find default pod from cluster - if len(o.PodName) == 0 { - if err := o.getTargetPod(); err != nil { + // validate user name and password + if len(o.userName) > 0 { + // read password from stdin + fmt.Print("Password: ") + if bytePassword, err := terminal.ReadPassword(int(os.Stdin.Fd())); err != nil { return err + } else { + o.userPasswd = string(bytePassword) } } + return nil +} - // get the pod object - pod, err := o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{}) - if err != nil { +func (o *ConnectOptions) complete() error { + var err error + if err = o.ExecOptions.Complete(); err != nil { return err } + // opt 1. specified pod name + // 1.1 get pod by name + if len(o.PodName) > 0 { + if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.Background(), o.PodName, metav1.GetOptions{}); err != nil { + return err + } + o.clusterName = cluster.GetPodClusterName(o.Pod) + o.componentName = cluster.GetPodComponentName(o.Pod) + } - // cluster name is not specified, get from pod label - if o.name == "" { - if name, ok := pod.Annotations[constant.AppInstanceLabelKey]; !ok { - return fmt.Errorf("failed to find the cluster to which the instance belongs") - } else { - o.name = name + // cannot infer characterType from pod directly (neither from pod annotation nor pod label) + // so we have to get cluster definition first to get characterType + // opt 2. specified cluster name + // 2.1 get cluster by name + if o.targetCluster, err = cluster.GetClusterByName(o.Dynamic, o.clusterName, o.Namespace); err != nil { + return err + } + // get cluster def + if o.targetClusterDef, err = cluster.GetClusterDefByName(o.Dynamic, o.targetCluster.Spec.ClusterDefRef); err != nil { + return err + } + + // 2.2 fill component name, use the first component by default + if len(o.componentName) == 0 { + o.component = &o.targetCluster.Spec.ComponentSpecs[0] + o.componentName = o.component.Name + } else { + // verify component + if o.component = o.targetCluster.Spec.GetComponentByName(o.componentName); o.component == nil { + return fmt.Errorf("failed to get component %s. Check the list of components use: \n\tkbcli cluster list-components %s -n %s", o.componentName, o.clusterName, o.Namespace) } } - info, err := o.getConnectionInfo() - if err != nil { + // 2.3 get character type + if o.componentDef = o.targetClusterDef.GetComponentDefByName(o.component.ComponentDefRef); o.componentDef == nil { + return fmt.Errorf("failed to get component def :%s", o.component.ComponentDefRef) + } + + // 2.4. get pod to connect, make sure o.clusterName, o.componentName are set before this step + if len(o.PodName) == 0 { + if err = o.getTargetPod(); err != nil { + return err + } + if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{}); err != nil { + return err + } + } + return nil +} + +// connect creates connection string and connects to cluster +func (o *ConnectOptions) connect() error { + if o.componentDef == nil { + return fmt.Errorf("component def is not initialized") + } + + var err error + + if o.engine, err = engine.New(o.componentDef.CharacterType); err != nil { + return err + } + + var authInfo *engine.AuthInfo + if len(o.userName) > 0 { + authInfo = &engine.AuthInfo{} + authInfo.UserName = o.userName + authInfo.UserPasswd = o.userPasswd + } else if authInfo, err = o.getAuthInfo(); err != nil { return err } - o.Command = buildCommand(info) - o.Pod = pod + o.ExecOptions.ContainerName = o.engine.Container() + o.ExecOptions.Command = o.engine.ConnectCommand(authInfo) + if klog.V(1).Enabled() { + fmt.Fprintf(o.Out, "connect with cmd: %s", o.ExecOptions.Command) + } return o.ExecOptions.Run() } +func (o *ConnectOptions) getAuthInfo() (*engine.AuthInfo, error) { + // select secrets by labels, admin account is preferred + labels := fmt.Sprintf("%s=%s,%s=%s,%s=%s", + constant.AppInstanceLabelKey, o.clusterName, + constant.KBAppComponentLabelKey, o.componentName, + constant.ClusterAccountLabelKey, (string)(appsv1alpha1.AdminAccount), + ) + + secrets, err := o.Client.CoreV1().Secrets(o.Namespace).List(context.Background(), metav1.ListOptions{LabelSelector: labels}) + if err != nil { + return nil, fmt.Errorf("failed to list secrets for cluster %s, component %s, err %v", o.clusterName, o.componentName, err) + } + if len(secrets.Items) == 0 { + return nil, nil + } + return &engine.AuthInfo{ + UserName: string(secrets.Items[0].Data["username"]), + UserPasswd: string(secrets.Items[0].Data["password"]), + }, nil +} + func (o *ConnectOptions) getTargetPod() error { - infos := cluster.GetSimpleInstanceInfos(o.Dynamic, o.name, o.Namespace) - if len(infos) == 0 || infos[0].Name == computil.ComponentStatusDefaultPodName { + // make sure cluster name and component name are set + if len(o.clusterName) == 0 { + return fmt.Errorf("cluster name is not set yet") + } + if len(o.componentName) == 0 { + return fmt.Errorf("component name is not set yet") + } + + // get instances for given cluster name and component name + infos := cluster.GetSimpleInstanceInfosForComponent(o.Dynamic, o.clusterName, o.componentName, o.Namespace) + if len(infos) == 0 || infos[0].Name == constant.ComponentStatusDefaultPodName { return fmt.Errorf("failed to find the instance to connect, please check cluster status") } - // first element is the default instance to connect o.PodName = infos[0].Name // print instance info that we connect @@ -202,11 +324,16 @@ func (o *ConnectOptions) getTargetPod() error { } func (o *ConnectOptions) getConnectionInfo() (*engine.ConnectionInfo, error) { + // make sure component and componentDef are set before this step + if o.component == nil || o.componentDef == nil { + return nil, fmt.Errorf("failed to get component or component def") + } + info := &engine.ConnectionInfo{} getter := cluster.ObjectsGetter{ Client: o.Client, Dynamic: o.Dynamic, - Name: o.name, + Name: o.clusterName, Namespace: o.Namespace, GetOptions: cluster.GetOptions{ WithClusterDef: true, @@ -220,6 +347,9 @@ func (o *ConnectOptions) getConnectionInfo() (*engine.ConnectionInfo, error) { return nil, err } + info.ClusterName = o.clusterName + info.ComponentName = o.componentName + info.HeadlessEndpoint = getOneHeadlessEndpoint(objs.ClusterDef, objs.Secrets) // get username and password if info.User, info.Password, err = getUserAndPassword(objs.ClusterDef, objs.Secrets); err != nil { return nil, err @@ -230,41 +360,32 @@ func (o *ConnectOptions) getConnectionInfo() (*engine.ConnectionInfo, error) { // TODO: now the primary component is the first component, that may not be correct, // maybe show all components connection info in the future. - primaryCompDef := objs.ClusterDef.Spec.ComponentDefs[0] - primaryComp := cluster.FindClusterComp(objs.Cluster, primaryCompDef.Name) - internalSvcs, externalSvcs := cluster.GetComponentServices(objs.Services, primaryComp) + internalSvcs, externalSvcs := cluster.GetComponentServices(objs.Services, o.component) switch { case len(externalSvcs) > 0: - // cluster has public endpoint + // cluster has public endpoints o.svc = externalSvcs[0] info.Host = cluster.GetExternalAddr(o.svc) info.Port = fmt.Sprintf("%d", o.svc.Spec.Ports[0].Port) case len(internalSvcs) > 0: - // cluster does not have public endpoint + // cluster does not have public endpoints o.svc = internalSvcs[0] info.Host = o.svc.Spec.ClusterIP info.Port = fmt.Sprintf("%d", o.svc.Spec.Ports[0].Port) o.privateEndPoint = true default: - // does not find any endpoints + // find no endpoints return nil, fmt.Errorf("failed to find any cluster endpoints") } - info.Command, info.Args, err = getCompCommandArgs(&primaryCompDef) - if err != nil { - return nil, err - } - - // get engine - o.engine, err = engine.New(objs.ClusterDef.Spec.ComponentDefs[0].CharacterType) - if err != nil { + if o.engine, err = engine.New(o.componentDef.CharacterType); err != nil { return nil, err } return info, nil } -// get cluster user and password from secrets +// getUserAndPassword gets cluster user and password from secrets func getUserAndPassword(clusterDef *appsv1alpha1.ClusterDefinition, secrets *corev1.SecretList) (string, string, error) { var ( user, password = "", "" @@ -297,6 +418,7 @@ func getUserAndPassword(clusterDef *appsv1alpha1.ClusterDefinition, secrets *cor for i, s := range secrets.Items { if strings.Contains(s.Name, "conn-credential") { secret = secrets.Items[i] + break } } user, err = getSecretVal(&secret, "username") @@ -309,80 +431,14 @@ func getUserAndPassword(clusterDef *appsv1alpha1.ClusterDefinition, secrets *cor return user, password, err } -func getCompCommandArgs(compDef *appsv1alpha1.ClusterComponentDefinition) ([]string, []string, error) { - failErr := fmt.Errorf("failed to find the connection command") - if compDef == nil || compDef.SystemAccounts == nil || - compDef.SystemAccounts.CmdExecutorConfig == nil { - return nil, nil, failErr - } - - execCfg := compDef.SystemAccounts.CmdExecutorConfig - command := execCfg.Command - if len(command) == 0 { - return nil, nil, failErr - } - return command, execCfg.Args, nil -} - -// buildCommand build connection command by SystemAccounts.CmdExecutorConfig. -// CLI should not be coupled to a specific engine, so read command info from -// clusterDefinition, but now these information is used to create system -// accounts, we need to do some special handling. -// -// TODO: Refactoring using command channel -// examples of info.Args are: -// mysql : -// command: -// - mysql -// args: -// - -u$(MYSQL_ROOT_USER) -// - -p$(MYSQL_ROOT_PASSWORD) -// - -h -// - $(KB_ACCOUNT_ENDPOINT) -// - -e -// - $(KB_ACCOUNT_STATEMENT) -// but in redis, it looks like following: -// redis : -// command: -// - sh -// - -c -// args: -// - "redis-cli -h $(KB_ACCOUNT_ENDPOINT) $(KB_ACCOUNT_STATEMENT)" -func buildCommand(info *engine.ConnectionInfo) []string { - result := make([]string, 0) - var extraCmd string - // prepare commands - if len(info.Command) == 1 { - // append [sh -c] - result = append(result, "sh", "-c") - extraCmd = info.Command[0] - } else { - result = append(result, info.Command...) +// getOneHeadlessEndpoint gets cluster headlessEndpoint from secrets +func getOneHeadlessEndpoint(clusterDef *appsv1alpha1.ClusterDefinition, secrets *corev1.SecretList) string { + if len(secrets.Items) == 0 { + return "" } - // prepare args - args := buildArgs(info.Args, extraCmd) - result = append(result, args) - return result -} - -func buildArgs(args []string, extraCmd string) string { - result := make([]string, 0) - if len(extraCmd) > 0 { - result = append(result, extraCmd) - } - for i := 0; i < len(args); i++ { - arg := args[i] - // skip command - if arg == "-c" || arg == "-e" { - i++ - continue - } - - arg = strings.Replace(arg, "$(KB_ACCOUNT_ENDPOINT)", "127.0.0.1", 1) - arg = strings.Replace(arg, "$(KB_ACCOUNT_STATEMENT)", "", 1) - arg = strings.Replace(arg, "(", "", 1) - arg = strings.Replace(arg, ")", "", 1) - result = append(result, strings.TrimSpace(arg)) + val, ok := secrets.Items[0].Data["headlessEndpoint"] + if !ok { + return "" } - return strings.Join(result, " ") + return string(val) } diff --git a/internal/cli/cmd/cluster/connect_test.go b/internal/cli/cmd/cluster/connect_test.go index 01ab437b0..bd6564aa1 100644 --- a/internal/cli/cmd/cluster/connect_test.go +++ b/internal/cli/cmd/cluster/connect_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -31,7 +34,6 @@ import ( clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" - "github.com/apecloud/kubeblocks/internal/cli/engine" "github.com/apecloud/kubeblocks/internal/cli/exec" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" @@ -84,33 +86,53 @@ var _ = Describe("connection", func() { Expect(cmd).ShouldNot(BeNil()) }) - It("connection", func() { + It("validate", func() { o := &ConnectOptions{ExecOptions: exec.NewExecOptions(tf, streams)} By("specified more than one cluster") - Expect(o.connect([]string{"c1", "c2"})).Should(HaveOccurred()) + Expect(o.validate([]string{"c1", "c2"})).Should(HaveOccurred()) By("without cluster name") - Expect(o.connect(nil)).Should(HaveOccurred()) + Expect(o.validate(nil)).Should(HaveOccurred()) + + Expect(o.validate([]string{clusterName})).Should(Succeed()) + + // set instance name and cluster name, should fail + o.PodName = "test-pod-0" + Expect(o.validate([]string{clusterName})).Should(HaveOccurred()) + o.componentName = "test-component" + Expect(o.validate([]string{})).Should(HaveOccurred()) + + // unset pod name + o.PodName = "" + Expect(o.validate([]string{clusterName})).Should(Succeed()) + // unset component name + o.componentName = "" + Expect(o.validate([]string{clusterName})).Should(Succeed()) + }) - By("specify cluster name") - Expect(o.ExecOptions.Complete()).Should(Succeed()) - _ = o.connect([]string{clusterName}) + It("complete by cluster name", func() { + o := &ConnectOptions{ExecOptions: exec.NewExecOptions(tf, streams)} + Expect(o.validate([]string{clusterName})).Should(Succeed()) + Expect(o.complete()).Should(Succeed()) Expect(o.Pod).ShouldNot(BeNil()) }) - It("show example", func() { + It("complete by pod name", func() { o := &ConnectOptions{ExecOptions: exec.NewExecOptions(tf, streams)} - Expect(o.ExecOptions.Complete()).Should(Succeed()) - - By("without args") - Expect(o.runShowExample(nil)).Should(HaveOccurred()) + o.PodName = "test-pod-0" + Expect(o.validate([]string{})).Should(Succeed()) + Expect(o.complete()).Should(Succeed()) + Expect(o.Pod).ShouldNot(BeNil()) + }) - By("specify more than one cluster") - Expect(o.runShowExample([]string{"c1", "c2"})).Should(HaveOccurred()) + It("show example", func() { + o := &ConnectOptions{ExecOptions: exec.NewExecOptions(tf, streams)} + Expect(o.validate([]string{clusterName})).Should(Succeed()) + Expect(o.complete()).Should(Succeed()) By("specify one cluster") - Expect(o.runShowExample([]string{clusterName})).Should(Succeed()) + Expect(o.runShowExample()).Should(Succeed()) }) It("getUserAndPassword", func() { @@ -131,41 +153,6 @@ var _ = Describe("connection", func() { Expect(u).Should(Equal(user)) Expect(p).Should(Equal(password)) }) - - It("build connect command", func() { - type argsCases struct { - command []string - args []string - expect []string - } - - testCases := []argsCases{ - { - command: []string{"mysql"}, - args: []string{"-h$(KB_ACCOUNT_ENDPOINT)", "-e", "$(KB_ACCOUNT_STATEMENT)"}, - expect: []string{"sh", "-c", "mysql -h127.0.0.1"}, - }, - { - command: []string{"psql"}, - args: []string{"-h$(KB_ACCOUNT_ENDPOINT)", "-c", "$(KB_ACCOUNT_STATEMENT)"}, - expect: []string{"sh", "-c", "psql -h127.0.0.1"}, - }, - { - command: []string{"sh", "-c"}, - args: []string{"redis-cli -h $(KB_ACCOUNT_ENDPOINT) $(KB_ACCOUNT_STATEMENT)"}, - expect: []string{"sh", "-c", "redis-cli -h 127.0.0.1"}, - }, - } - - for _, testCase := range testCases { - info := &engine.ConnectionInfo{ - Command: testCase.command, - Args: testCase.args, - } - result := buildCommand(info) - Expect(result).Should(BeEquivalentTo(testCase.expect)) - } - }) }) func mockPod() *corev1.Pod { diff --git a/internal/cli/cmd/cluster/create.go b/internal/cli/cmd/cluster/create.go old mode 100644 new mode 100755 index 03ae63a02..2c81c6c87 --- a/internal/cli/cmd/cluster/create.go +++ b/internal/cli/cmd/cluster/create.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -29,6 +32,7 @@ import ( "github.com/ghodss/yaml" "github.com/spf13/cobra" "github.com/spf13/viper" + "golang.org/x/exp/maps" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" @@ -36,13 +40,18 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" + corev1ac "k8s.io/client-go/applyconfigurations/core/v1" + rbacv1ac "k8s.io/client-go/applyconfigurations/rbac/v1" "k8s.io/client-go/dynamic" + "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" utilcomp "k8s.io/kubectl/pkg/util/completion" + "k8s.io/kubectl/pkg/util/storage" "k8s.io/kubectl/pkg/util/templates" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/class" "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/printer" @@ -55,48 +64,71 @@ var clusterCreateExample = templates.Examples(` # Create a cluster with cluster definition apecloud-mysql and cluster version ac-mysql-8.0.30 kbcli cluster create mycluster --cluster-definition apecloud-mysql --cluster-version ac-mysql-8.0.30 - # --cluster-definition is required, if --cluster-version is not specified, will use the most recently created version + # --cluster-definition is required, if --cluster-version is not specified, pick the most recently created version kbcli cluster create mycluster --cluster-definition apecloud-mysql - # Create a cluster and set termination policy DoNotTerminate that will prevent the cluster from being deleted + # Output resource information in YAML format, without creation of resources. + kbcli cluster create mycluster --cluster-definition apecloud-mysql --dry-run -o yaml + + # Output resource information in YAML format, the information will be sent to the server + # but the resources will not be actually created. + kbcli cluster create mycluster --cluster-definition apecloud-mysql --dry-run=server -o yaml + + # Create a cluster and set termination policy DoNotTerminate that prevents the cluster from being deleted kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy DoNotTerminate - # In scenarios where you want to delete resources such as statements, deployments, services, pdb, but keep PVCs + # Delete resources such as statefulsets, deployments, services, pdb, but keep PVCs # when deleting the cluster, use termination policy Halt kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy Halt - # In scenarios where you want to delete resource such as statements, deployments, services, pdb, and including + # Delete resource such as statefulsets, deployments, services, pdb, and including # PVCs when deleting the cluster, use termination policy Delete kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy Delete - # In scenarios where you want to delete all resources including all snapshots and snapshot data when deleting + # Delete all resources including all snapshots and snapshot data when deleting # the cluster, use termination policy WipeOut kbcli cluster create mycluster --cluster-definition apecloud-mysql --termination-policy WipeOut # Create a cluster and set cpu to 1 core, memory to 1Gi, storage size to 20Gi and replicas to 3 kbcli cluster create mycluster --cluster-definition apecloud-mysql --set cpu=1,memory=1Gi,storage=20Gi,replicas=3 - # Create a cluster and set class to general-1c4g - kbcli cluster create myclsuter --cluster-definition apecloud-mysql --set class=general-1c4g + # Create a cluster and set storageClass to csi-hostpath-sc, if storageClass is not specified, + # the default storage class will be used + kbcli cluster create mycluster --cluster-definition apecloud-mysql --set storageClass=csi-hostpath-sc + + # Create a cluster and set the class to general-1c1g + # run "kbcli class list --cluster-definition=cluster-definition-name" to get the class list + kbcli cluster create mycluster --cluster-definition apecloud-mysql --set class=general-1c1g + + # Create a cluster with replicationSet workloadType and set switchPolicy to Noop + kbcli cluster create mycluster --cluster-definition postgresql --set switchPolicy=Noop + + # Create a cluster with more than one component, use "--set type=component-name" to specify the component, + # if not specified, the main component will be used, run "kbcli cd list-components CLUSTER-DEFINITION-NAME" + # to show the components in the cluster definition + kbcli cluster create mycluster --cluster-definition redis --set type=redis,cpu=1 --set type=redis-sentinel,cpu=200m # Create a cluster and use a URL to set cluster resource - kbcli cluster create mycluster --cluster-definition apecloud-mysql --set-file https://kubeblocks.io/yamls/my.yaml + kbcli cluster create mycluster --cluster-definition apecloud-mysql \ + --set-file https://kubeblocks.io/yamls/apecloud-mysql.yaml # Create a cluster and load cluster resource set from stdin cat << EOF | kbcli cluster create mycluster --cluster-definition apecloud-mysql --set-file - - name: my-test ... - # Create a cluster forced to scatter by node - kbcli cluster create --cluster-definition apecloud-mysql --topology-keys kubernetes.io/hostname --pod-anti-affinity Required + # Create a cluster scattered by nodes + kbcli cluster create --cluster-definition apecloud-mysql --topology-keys kubernetes.io/hostname \ + --pod-anti-affinity Required # Create a cluster in specific labels nodes - kbcli cluster create --cluster-definition apecloud-mysql --node-labels '"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"' + kbcli cluster create --cluster-definition apecloud-mysql \ + --node-labels '"topology.kubernetes.io/zone=us-east-1a","disktype=ssd,essd"' # Create a Cluster with two tolerations - kbcli cluster create --cluster-definition apecloud-mysql --tolerations '"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + kbcli cluster create --cluster-definition apecloud-mysql --tolerations \ '"engineType=mongo:NoSchedule","diskType=ssd:NoSchedule"' # Create a cluster, with each pod runs on their own dedicated node - kbcli cluster create --tenancy=DedicatedNode + kbcli cluster create --cluster-definition apecloud-mysql --tenancy=DedicatedNode `) const ( @@ -107,13 +139,15 @@ const ( type setKey string const ( - keyType setKey = "type" - keyCPU setKey = "cpu" - keyClass setKey = "class" - keyMemory setKey = "memory" - keyReplicas setKey = "replicas" - keyStorage setKey = "storage" - keyUnknown setKey = "unknown" + keyType setKey = "type" + keyCPU setKey = "cpu" + keyClass setKey = "class" + keyMemory setKey = "memory" + keyReplicas setKey = "replicas" + keyStorage setKey = "storage" + keyStorageClass setKey = "storageClass" + keySwitchPolicy setKey = "switchPolicy" + keyUnknown setKey = "unknown" ) type envSet struct { @@ -156,10 +190,61 @@ type CreateOptions struct { SetFile string `json:"-"` Values []string `json:"-"` + shouldCreateDependencies bool `json:"-"` + // backup name to restore in creation Backup string `json:"backup,omitempty"` UpdatableFlags - create.BaseOptions + create.CreateOptions `json:"-"` +} + +func NewCreateCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewCreateOptions(f, streams) + cmd := &cobra.Command{ + Use: "create [NAME]", + Short: "Create a cluster.", + Example: clusterCreateExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli cd list\" to show all available cluster definitions") + cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Specify cluster version, run \"kbcli cv list\" to show all available cluster versions, use the latest version if not specified") + cmd.Flags().StringVarP(&o.SetFile, "set-file", "f", "", "Use yaml file, URL, or stdin to set the cluster resource") + cmd.Flags().StringArrayVar(&o.Values, "set", []string{}, "Set the cluster resource including cpu, memory, replicas and storage, or just specify the class, each set corresponds to a component.(e.g. --set cpu=1,memory=1Gi,replicas=3,storage=20Gi or --set class=general-1c1g)") + cmd.Flags().StringVar(&o.Backup, "backup", "", "Set a source backup to restore data") + cmd.Flags().BoolVar(&o.EditBeforeCreate, "edit", o.EditBeforeCreate, "Edit the API resource before creating") + cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`) + cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged" + + // add updatable flags + o.UpdatableFlags.addFlags(cmd) + + // add print flags + printer.AddOutputFlagForCreate(cmd, &o.Format) + + // register flag completion func + registerFlagCompletionFunc(cmd, f) + + return cmd +} + +func NewCreateOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *CreateOptions { + o := &CreateOptions{CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateName, + GVR: types.ClusterGVR(), + }} + o.CreateOptions.Options = o + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.CreateDependencies = o.CreateDependencies + return o } func setMonitor(monitor bool, components []map[string]interface{}) { @@ -208,26 +293,54 @@ func setBackup(o *CreateOptions, components []map[string]interface{}) error { func (o *CreateOptions) Validate() error { if o.ClusterDefRef == "" { - return fmt.Errorf("a valid cluster definition is needed, use --cluster-definition to specify one, run \"kbcli cluster-definition list\" to show all cluster definition") + return fmt.Errorf("a valid cluster definition is needed, use --cluster-definition to specify one, run \"kbcli clusterdefinition list\" to show all cluster definitions") } if o.TerminationPolicy == "" { return fmt.Errorf("a valid termination policy is needed, use --termination-policy to specify one of: DoNotTerminate, Halt, Delete, WipeOut") } - if o.ClusterVersionRef == "" { - version, err := cluster.GetLatestVersion(o.Dynamic, o.ClusterDefRef) - if err != nil { - return err - } - o.ClusterVersionRef = version - printer.Warning(o.Out, "cluster version is not specified, use the recently created ClusterVersion %s\n", o.ClusterVersionRef) + if err := o.validateClusterVersion(); err != nil { + return err } if len(o.Values) > 0 && len(o.SetFile) > 0 { return fmt.Errorf("does not support --set and --set-file being specified at the same time") } + if len(o.Name) > 16 { + return fmt.Errorf("cluster name should be less than 16 characters") + } + + return nil +} + +func (o *CreateOptions) Complete() error { + var ( + compByte []byte + cls *appsv1alpha1.Cluster + clusterCompSpecs []appsv1alpha1.ClusterComponentSpec + err error + ) + + if len(o.SetFile) > 0 { + if compByte, err = MultipleSourceComponents(o.SetFile, o.IOStreams.In); err != nil { + return err + } + if compByte, err = yaml.YAMLToJSON(compByte); err != nil { + return err + } + + // compatible with old file format that only specifies the components + if err = json.Unmarshal(compByte, &cls); err != nil { + if clusterCompSpecs, err = parseClusterComponentSpec(compByte); err != nil { + return err + } + } else { + clusterCompSpecs = cls.Spec.ComponentSpecs + } + } + // if name is not specified, generate a random cluster name if o.Name == "" { name, err := generateClusterName(o.Dynamic, o.Namespace) @@ -239,107 +352,207 @@ func (o *CreateOptions) Validate() error { } o.Name = name } - return nil -} -func (o *CreateOptions) Complete() error { - components, err := o.buildComponents() + // build annotation + o.buildAnnotation(cls) + + // build cluster definition + if err := o.buildClusterDef(cls); err != nil { + return err + } + + // build cluster version + o.buildClusterVersion(cls) + + // build components + components, err := o.buildComponents(clusterCompSpecs) if err != nil { return err } setMonitor(o.Monitor, components) - if err := setBackup(o, components); err != nil { + if err = setBackup(o, components); err != nil { return err } o.ComponentSpecs = components // TolerationsRaw looks like `["key=engineType,value=mongo,operator=Equal,effect=NoSchedule"]` after parsing by cmd - tolerations := buildTolerations(o.TolerationsRaw) + tolerations, err := util.BuildTolerations(o.TolerationsRaw) + if err != nil { + return err + } if len(tolerations) > 0 { o.Tolerations = tolerations } - return nil + + // validate default storageClassName + return validateStorageClass(o.Dynamic, o.ComponentSpecs) +} + +func (o *CreateOptions) CleanUp() error { + if o.Client == nil { + return nil + } + + return deleteDependencies(o.Client, o.Namespace, o.Name) } -// buildComponents build components from file or set values -func (o *CreateOptions) buildComponents() ([]map[string]interface{}, error) { +// buildComponents builds components from file or set values +func (o *CreateOptions) buildComponents(clusterCompSpecs []appsv1alpha1.ClusterComponentSpec) ([]map[string]interface{}, error) { var ( - componentByte []byte - err error + err error + cd *appsv1alpha1.ClusterDefinition + compSpecs []*appsv1alpha1.ClusterComponentSpec ) - // build components from file - components := o.ComponentSpecs - if len(o.SetFile) > 0 { - if componentByte, err = MultipleSourceComponents(o.SetFile, o.IOStreams.In); err != nil { - return nil, err - } - if componentByte, err = yaml.YAMLToJSON(componentByte); err != nil { - return nil, err - } - if err = json.Unmarshal(componentByte, &components); err != nil { - return nil, err - } - return components, nil + compClasses, err := class.ListClassesByClusterDefinition(o.Dynamic, o.ClusterDefRef) + if err != nil { + return nil, err } - // build components from set values or environment variables - if len(components) == 0 { - cd, err := cluster.GetClusterDefByName(o.Dynamic, o.ClusterDefRef) + cd, err = cluster.GetClusterDefByName(o.Dynamic, o.ClusterDefRef) + if err != nil { + return nil, err + } + + if clusterCompSpecs != nil { + for _, comp := range clusterCompSpecs { + compSpecs = append(compSpecs, &comp) + } + } else { + // build components from set values or environment variables + compSets, err := buildCompSetsMap(o.Values, cd) if err != nil { return nil, err } - compSets, err := buildCompSetsMap(o.Values, cd) + compSpecs, err = buildClusterComp(cd, compSets, compClasses) if err != nil { return nil, err } + } - componentObjs, err := buildClusterComp(cd, compSets) - if err != nil { + var comps []map[string]interface{} + for _, compSpec := range compSpecs { + // validate component classes + if _, err = class.ValidateComponentClass(compSpec, compClasses); err != nil { return nil, err } - for _, compObj := range componentObjs { - comp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(compObj) - if err != nil { - return nil, err - } - components = append(components, comp) + + // create component dependencies + if err = o.buildDependenciesFn(cd, compSpec); err != nil { + return nil, err } - if err = o.buildClassMappings(componentObjs, compSets); err != nil { + comp, err := runtime.DefaultUnstructuredConverter.ToUnstructured(compSpec) + if err != nil { return nil, err } + comps = append(comps, comp) } - return components, nil + return comps, nil } -func (o *CreateOptions) buildClassMappings(components []*appsv1alpha1.ClusterComponentSpec, setsMap map[string]map[setKey]string) error { - classMappings := make(map[string]string) - for _, comp := range components { - sets, ok := setsMap[comp.ComponentDefRef] - if !ok { - continue - } - class, ok := sets[keyClass] - if !ok { - continue - } - classMappings[comp.Name] = class +const ( + saNamePrefix = "kb-sa-" + roleNamePrefix = "kb-role-" + roleBindingNamePrefix = "kb-rolebinding-" +) + +// buildDependenciesFn creates dependencies function for components, e.g. postgresql depends on +// a service account, a role and a rolebinding +func (o *CreateOptions) buildDependenciesFn(cd *appsv1alpha1.ClusterDefinition, + compSpec *appsv1alpha1.ClusterComponentSpec) error { + + // set component service account name + compSpec.ServiceAccountName = saNamePrefix + o.Name + o.shouldCreateDependencies = true + return nil +} + +func (o *CreateOptions) CreateDependencies(dryRun []string) error { + var ( + saName = saNamePrefix + o.Name + roleName = roleNamePrefix + o.Name + roleBindingName = roleBindingNamePrefix + o.Name + ) + + if !o.shouldCreateDependencies { + return nil } - bytes, err := json.Marshal(classMappings) - if err != nil { + + klog.V(1).Infof("create dependencies for cluster %s", o.Name) + // create service account + labels := buildResourceLabels(o.Name) + applyOptions := metav1.ApplyOptions{FieldManager: "kbcli", DryRun: dryRun} + sa := corev1ac.ServiceAccount(saName, o.Namespace).WithLabels(labels) + + klog.V(1).Infof("create service account %s", saName) + if _, err := o.Client.CoreV1().ServiceAccounts(o.Namespace).Apply(context.TODO(), sa, applyOptions); err != nil { return err } - if o.Annotations == nil { - o.Annotations = make(map[string]string) + + // create role + klog.V(1).Infof("create role %s", roleName) + role := rbacv1ac.Role(roleName, o.Namespace).WithRules([]*rbacv1ac.PolicyRuleApplyConfiguration{ + { + APIGroups: []string{""}, + Resources: []string{"events"}, + Verbs: []string{"create"}, + }, + }...).WithLabels(labels) + + // postgresql need more rules for patroni + if ok, err := o.isPostgresqlCluster(); err != nil { + return err + } else if ok { + rules := []rbacv1ac.PolicyRuleApplyConfiguration{ + { + APIGroups: []string{""}, + Resources: []string{"configmaps"}, + Verbs: []string{"create", "get", "list", "patch", "update", "watch", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"endpoints"}, + Verbs: []string{"create", "get", "list", "patch", "update", "watch", "delete"}, + }, + { + APIGroups: []string{""}, + Resources: []string{"pods"}, + Verbs: []string{"get", "list", "patch", "update", "watch"}, + }, + } + role.Rules = append(role.Rules, rules...) } - o.Annotations[types.ComponentClassAnnotationKey] = string(bytes) - return nil + + if _, err := o.Client.RbacV1().Roles(o.Namespace).Apply(context.TODO(), role, applyOptions); err != nil { + return err + } + + // create role binding + rbacAPIGroup := "rbac.authorization.k8s.io" + rbacKind := "Role" + saKind := "ServiceAccount" + roleBinding := rbacv1ac.RoleBinding(roleBindingName, o.Namespace).WithLabels(labels). + WithSubjects([]*rbacv1ac.SubjectApplyConfiguration{ + { + Kind: &saKind, + Name: &saName, + Namespace: &o.Namespace, + }, + }...). + WithRoleRef(&rbacv1ac.RoleRefApplyConfiguration{ + APIGroup: &rbacAPIGroup, + Kind: &rbacKind, + Name: &roleName, + }) + klog.V(1).Infof("create role binding %s", roleBindingName) + _, err := o.Client.RbacV1().RoleBindings(o.Namespace).Apply(context.TODO(), roleBinding, applyOptions) + return err } -// MultipleSourceComponents get component data from multiple source, such as stdin, URI and local file +// MultipleSourceComponents gets component data from multiple source, such as stdin, URI and local file func MultipleSourceComponents(fileName string, in io.Reader) ([]byte, error) { var data io.Reader switch { @@ -363,41 +576,6 @@ func MultipleSourceComponents(fileName string, in io.Reader) ([]byte, error) { return io.ReadAll(data) } -func NewCreateCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := &CreateOptions{BaseOptions: create.BaseOptions{IOStreams: streams}} - inputs := create.Inputs{ - Use: "create [CLUSTER_NAME]", - Short: "Create a cluster.", - Example: clusterCreateExample, - CueTemplateName: CueTemplateName, - ResourceName: types.ResourceClusters, - BaseOptionsObj: &o.BaseOptions, - Options: o, - Factory: f, - Validate: o.Validate, - Complete: o.Complete, - PreCreate: o.PreCreate, - BuildFlags: func(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.ClusterDefRef, "cluster-definition", "", "Specify cluster definition, run \"kbcli cd list\" to show all available cluster definitions") - cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Specify cluster version, run \"kbcli cv list\" to show all available cluster versions, use the latest version if not specified") - cmd.Flags().StringVarP(&o.SetFile, "set-file", "f", "", "Use yaml file, URL, or stdin to set the cluster resource") - cmd.Flags().StringArrayVar(&o.Values, "set", []string{}, "Set the cluster resource including cpu, memory, replicas and storage, each set corresponds to a component.(e.g. --set cpu=1,memory=1Gi,replicas=3,storage=20Gi)") - cmd.Flags().StringVar(&o.Backup, "backup", "", "Set a source backup to restore data") - - // add updatable flags - o.UpdatableFlags.addFlags(cmd) - - // set required flag - util.CheckErr(cmd.MarkFlagRequired("cluster-definition")) - - // register flag completion func - registerFlagCompletionFunc(cmd, f) - }, - } - - return create.BuildCommand(inputs) -} - func registerFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { util.CheckErr(cmd.RegisterFlagCompletionFunc( "cluster-definition", @@ -407,11 +585,34 @@ func registerFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { util.CheckErr(cmd.RegisterFlagCompletionFunc( "cluster-version", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return utilcomp.CompGetResource(f, cmd, util.GVRToString(types.ClusterVersionGVR()), toComplete), cobra.ShellCompDirectiveNoFileComp + var clusterVersion []string + clusterDefinition, err := cmd.Flags().GetString("cluster-definition") + if clusterDefinition == "" || err != nil { + clusterVersion = utilcomp.CompGetResource(f, cmd, util.GVRToString(types.ClusterVersionGVR()), toComplete) + } else { + label := fmt.Sprintf("%s=%s", constant.ClusterDefLabelKey, clusterDefinition) + clusterVersion = util.CompGetResourceWithLabels(f, cmd, util.GVRToString(types.ClusterVersionGVR()), []string{label}, toComplete) + } + return clusterVersion, cobra.ShellCompDirectiveNoFileComp + })) + + var formatsWithDesc = map[string]string{ + "JSON": "Output result in JSON format", + "YAML": "Output result in YAML format", + } + util.CheckErr(cmd.RegisterFlagCompletionFunc("output", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + for format, desc := range formatsWithDesc { + if strings.HasPrefix(format, toComplete) { + names = append(names, fmt.Sprintf("%s\t%s", format, desc)) + } + } + return names, cobra.ShellCompDirectiveNoFileComp })) } -// PreCreate before commit yaml to k8s, make changes on Unstructured yaml +// PreCreate before saving yaml to k8s, makes changes on Unstructured yaml func (o *CreateOptions) PreCreate(obj *unstructured.Unstructured) error { if !o.EnableAllLogs { // EnableAllLogs is false, nothing will change @@ -435,7 +636,41 @@ func (o *CreateOptions) PreCreate(obj *unstructured.Unstructured) error { return nil } -// setEnableAllLog set enable all logs, and ignore enabledLogs of component level. +func (o *CreateOptions) isPostgresqlCluster() (bool, error) { + cd, err := cluster.GetClusterDefByName(o.Dynamic, o.ClusterDefRef) + if err != nil { + return false, err + } + + var compDef *appsv1alpha1.ClusterComponentDefinition + if cd.Spec.Type != "postgresql" { + return false, nil + } + + // get cluster component definition + if len(o.ComponentSpecs) == 0 { + return false, fmt.Errorf("find no cluster componnet") + } + compSpec := o.ComponentSpecs[0] + for i, def := range cd.Spec.ComponentDefs { + compDefRef := compSpec["componentDefRef"] + if compDefRef != nil && def.Name == compDefRef.(string) { + compDef = &cd.Spec.ComponentDefs[i] + } + } + + if compDef == nil { + return false, fmt.Errorf("failed to find component definition for componnet %v", compSpec["Name"]) + } + + // for postgresql, we need to create a service account, a role and a rolebinding + if compDef.CharacterType != "postgresql" { + return false, nil + } + return true, nil +} + +// setEnableAllLog sets enable all logs, and ignore enabledLogs of component level. func setEnableAllLogs(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition) { for idx, comCluster := range c.Spec.ComponentSpecs { for _, com := range cd.Spec.ComponentDefs { @@ -451,8 +686,11 @@ func setEnableAllLogs(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinitio } } -func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map[setKey]string) ([]*appsv1alpha1.ClusterComponentSpec, error) { - getVal := func(key setKey, sets map[setKey]string) string { +func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map[setKey]string, + componentClasses map[string]map[string]*appsv1alpha1.ComponentClassInstance) ([]*appsv1alpha1.ClusterComponentSpec, error) { + // get value from set values and environment variables, the second return value is + // true if the value is from environment variables + getVal := func(c *appsv1alpha1.ClusterComponentDefinition, key setKey, sets map[setKey]string) string { // get value from set values if sets != nil { if v := sets[key]; len(v) > 0 { @@ -460,6 +698,34 @@ func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map } } + // HACK: if user does not set by command flag, for replicationSet workload, + // set replicas to 2, for redis sentinel, set replicas to 3, cpu and memory + // to 200M and 200Mi + // TODO: use more graceful way to set default value + if c.WorkloadType == appsv1alpha1.Replication { + if key == keyReplicas { + return "2" + } + } + + // the default replicas is 3 if not set by command flag, for Consensus workload + if c.WorkloadType == appsv1alpha1.Consensus { + if key == keyReplicas { + return "3" + } + } + + if c.CharacterType == "redis" && c.Name == "redis-sentinel" { + switch key { + case keyReplicas: + return "3" + case keyCPU: + return "200m" + case keyMemory: + return "200Mi" + } + } + // get value from environment variables env := setKeyEnvMap[key] val := viper.GetString(env.name) @@ -469,6 +735,27 @@ func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map return val } + buildSwitchPolicy := func(c *appsv1alpha1.ClusterComponentDefinition, compObj *appsv1alpha1.ClusterComponentSpec, sets map[setKey]string) error { + if c.WorkloadType != appsv1alpha1.Replication { + return nil + } + var switchPolicyType appsv1alpha1.SwitchPolicyType + switch getVal(c, keySwitchPolicy, sets) { + case "Noop", "": + switchPolicyType = appsv1alpha1.Noop + case "MaximumAvailability": + switchPolicyType = appsv1alpha1.MaximumAvailability + case "MaximumPerformance": + switchPolicyType = appsv1alpha1.MaximumDataProtection + default: + return fmt.Errorf("switchPolicy is illegal, only support Noop, MaximumAvailability, MaximumPerformance") + } + compObj.SwitchPolicy = &appsv1alpha1.ClusterSwitchPolicy{ + Type: switchPolicyType, + } + return nil + } + var comps []*appsv1alpha1.ClusterComponentSpec for _, c := range cd.Spec.ComponentDefs { sets := map[setKey]string{} @@ -477,37 +764,64 @@ func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map } // get replicas - setReplicas, err := strconv.Atoi(getVal(keyReplicas, sets)) + setReplicas, err := strconv.Atoi(getVal(&c, keyReplicas, sets)) if err != nil { return nil, fmt.Errorf("repicas is illegal " + err.Error()) } replicas := int32(setReplicas) - resourceList := corev1.ResourceList{ - corev1.ResourceCPU: resource.MustParse(getVal(keyCPU, sets)), - corev1.ResourceMemory: resource.MustParse(getVal(keyMemory, sets)), - } compObj := &appsv1alpha1.ClusterComponentSpec{ Name: c.Name, ComponentDefRef: c.Name, Replicas: replicas, - Resources: corev1.ResourceRequirements{ - Requests: resourceList, - Limits: resourceList, - }, - VolumeClaimTemplates: []appsv1alpha1.ClusterComponentVolumeClaimTemplate{{ - Name: "data", - Spec: appsv1alpha1.PersistentVolumeClaimSpec{ - AccessModes: []corev1.PersistentVolumeAccessMode{ - corev1.ReadWriteOnce, - }, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: resource.MustParse(getVal(keyStorage, sets)), - }, + } + + // class has higher priority than other resource related parameters + resourceList := make(corev1.ResourceList) + if _, ok := componentClasses[c.Name]; ok { + if className := getVal(&c, keyClass, sets); className != "" { + compObj.ClassDefRef = &appsv1alpha1.ClassDefRef{Class: className} + } else { + if cpu, ok := sets[keyCPU]; ok { + resourceList[corev1.ResourceCPU] = resource.MustParse(cpu) + } + if mem, ok := sets[keyMemory]; ok { + resourceList[corev1.ResourceMemory] = resource.MustParse(mem) + } + } + } else { + if className := getVal(&c, keyClass, sets); className != "" { + return nil, fmt.Errorf("can not find class %s for component type %s", className, c.Name) + } + resourceList = corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse(getVal(&c, keyCPU, sets)), + corev1.ResourceMemory: resource.MustParse(getVal(&c, keyMemory, sets)), + } + } + compObj.Resources = corev1.ResourceRequirements{ + Requests: resourceList, + Limits: resourceList, + } + compObj.VolumeClaimTemplates = []appsv1alpha1.ClusterComponentVolumeClaimTemplate{{ + Name: "data", + Spec: appsv1alpha1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse(getVal(&c, keyStorage, sets)), }, }, - }}, + }, + }} + storageClass := getVal(&c, keyStorageClass, sets) + if len(storageClass) != 0 { + // now the clusterdefinition components mostly have only one VolumeClaimTemplates in default + compObj.VolumeClaimTemplates[0].Spec.StorageClassName = &storageClass + } + if err = buildSwitchPolicy(&c, compObj, sets); err != nil { + return nil, err } comps = append(comps, compObj) } @@ -518,7 +832,7 @@ func buildClusterComp(cd *appsv1alpha1.ClusterDefinition, setsMap map[string]map // specified in the set, use the cluster definition default component name. func buildCompSetsMap(values []string, cd *appsv1alpha1.ClusterDefinition) (map[string]map[setKey]string, error) { allSets := map[string]map[setKey]string{} - keys := []string{string(keyCPU), string(keyType), string(keyStorage), string(keyMemory), string(keyReplicas), string(keyClass)} + keys := []string{string(keyCPU), string(keyType), string(keyStorage), string(keyMemory), string(keyReplicas), string(keyClass), string(keyStorageClass), string(keySwitchPolicy)} parseKey := func(key string) setKey { for _, k := range keys { if strings.EqualFold(k, key) { @@ -566,6 +880,18 @@ func buildCompSetsMap(values []string, cd *appsv1alpha1.ClusterDefinition) (map[ return nil, err } compDefName = name + } else { + // check the type is a valid component definition name + valid := false + for _, c := range cd.Spec.ComponentDefs { + if c.Name == compDefName { + valid = true + break + } + } + if !valid { + return nil, fmt.Errorf("the type \"%s\" is not a valid component definition name", compDefName) + } } // if already set by other value, later values override earlier values @@ -580,20 +906,7 @@ func buildCompSetsMap(values []string, cd *appsv1alpha1.ClusterDefinition) (map[ return allSets, nil } -func buildTolerations(raw []string) []interface{} { - tolerations := make([]interface{}, 0) - for _, tolerationRaw := range raw { - toleration := map[string]interface{}{} - for _, entries := range strings.Split(tolerationRaw, ",") { - parts := strings.SplitN(entries, "=", 2) - toleration[strings.TrimSpace(parts[0])] = strings.TrimSpace(parts[1]) - } - tolerations = append(tolerations, toleration) - } - return tolerations -} - -// generateClusterName generate a random cluster name that does not exist +// generateClusterName generates a random cluster name that does not exist func generateClusterName(dynamic dynamic.Interface, namespace string) (string, error) { var name string // retry 10 times @@ -614,11 +927,11 @@ func generateClusterName(dynamic dynamic.Interface, namespace string) (string, e func (f *UpdatableFlags) addFlags(cmd *cobra.Command) { cmd.Flags().StringVar(&f.PodAntiAffinity, "pod-anti-affinity", "Preferred", "Pod anti-affinity type, one of: (Preferred, Required)") cmd.Flags().BoolVar(&f.Monitor, "monitor", true, "Set monitor enabled and inject metrics exporter") - cmd.Flags().BoolVar(&f.EnableAllLogs, "enable-all-logs", true, "Enable advanced application all log extraction, and true will ignore enabledLogs of component level") + cmd.Flags().BoolVar(&f.EnableAllLogs, "enable-all-logs", false, "Enable advanced application all log extraction, set to true will ignore enabledLogs of component level, default is false") cmd.Flags().StringVar(&f.TerminationPolicy, "termination-policy", "Delete", "Termination policy, one of: (DoNotTerminate, Halt, Delete, WipeOut)") cmd.Flags().StringArrayVar(&f.TopologyKeys, "topology-keys", nil, "Topology keys for affinity") cmd.Flags().StringToStringVar(&f.NodeLabels, "node-labels", nil, "Node label selector") - cmd.Flags().StringSliceVar(&f.TolerationsRaw, "tolerations", nil, `Tolerations for cluster, such as '"key=engineType,value=mongo,operator=Equal,effect=NoSchedule"'`) + cmd.Flags().StringSliceVar(&f.TolerationsRaw, "tolerations", nil, `Tolerations for cluster, such as "key=value:effect, key:effect", for example '"engineType=mongo:NoSchedule", "diskType:NoSchedule"'`) cmd.Flags().StringVar(&f.Tenancy, "tenancy", "SharedNode", "Tenancy options, one of: (SharedNode, DedicatedNode)") util.CheckErr(cmd.RegisterFlagCompletionFunc( @@ -648,3 +961,186 @@ func (f *UpdatableFlags) addFlags(cmd *cobra.Command) { }, cobra.ShellCompDirectiveNoFileComp })) } + +// validateStorageClass checks the existence of declared StorageClasses in volume claim templates, +// if not set, check the existence of the default StorageClasses +func validateStorageClass(dynamic dynamic.Interface, components []map[string]interface{}) error { + existedStorageClasses, existedDefault, err := getStorageClasses(dynamic) + if err != nil { + return err + } + for _, comp := range components { + compObj := appsv1alpha1.ClusterComponentSpec{} + err = runtime.DefaultUnstructuredConverter.FromUnstructured(comp, &compObj) + if err != nil { + return err + } + for _, vct := range compObj.VolumeClaimTemplates { + name := vct.Spec.StorageClassName + if name != nil { + // validate the specified StorageClass whether exist + if _, ok := existedStorageClasses[*name]; !ok { + return fmt.Errorf("failed to find the specified storageClass \"%s\"", *name) + } + } else if !existedDefault { + // validate the default StorageClass + return fmt.Errorf("failed to find the default storageClass, use '--set storageClass=NAME' to set it") + } + } + } + return nil +} + +// getStorageClasses returns all StorageClasses in K8S and return true if the cluster have a default StorageClasses +func getStorageClasses(dynamic dynamic.Interface) (map[string]struct{}, bool, error) { + gvr := types.StorageClassGVR() + allStorageClasses := make(map[string]struct{}) + existedDefault := false + list, err := dynamic.Resource(gvr).List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, false, err + } + for _, item := range list.Items { + allStorageClasses[item.GetName()] = struct{}{} + annotations := item.GetAnnotations() + if !existedDefault && annotations != nil && (annotations[storage.IsDefaultStorageClassAnnotation] == annotationTrueValue || annotations[storage.BetaIsDefaultStorageClassAnnotation] == annotationTrueValue) { + existedDefault = true + } + } + return allStorageClasses, existedDefault, nil +} + +// validateClusterVersion checks the existence of declared cluster version, +// if not set, check the existence of default cluster version +func (o *CreateOptions) validateClusterVersion() error { + existedClusterVersions, defaultVersion, existedDefault, err := getClusterVersions(o.Dynamic, o.ClusterDefRef) + if err != nil { + return err + } + + dryRun, err := o.GetDryRunStrategy() + if err != nil { + return err + } + + printCvInfo := func(cv string) { + // if dryRun is set, run in quiet mode, avoid to output yaml file with the info + if dryRun != create.DryRunNone { + return + } + fmt.Fprintf(o.Out, "Info: --cluster-version is not specified, ClusterVersion %s is applied by default\n", cv) + } + + switch { + case o.ClusterVersionRef != "": + if _, ok := existedClusterVersions[o.ClusterVersionRef]; !ok { + return fmt.Errorf("failed to find the specified cluster version \"%s\"", o.ClusterVersionRef) + } + case !existedDefault: + // if default version is not set and there is only one version, pick it + if len(existedClusterVersions) == 1 { + o.ClusterVersionRef = maps.Keys(existedClusterVersions)[0] + printCvInfo(o.ClusterVersionRef) + } else { + return fmt.Errorf("failed to find the default cluster version, use '--cluster-version ClusterVersion' to set it") + } + case existedDefault: + // TODO: achieve this in operator + if existedDefault { + o.ClusterVersionRef = defaultVersion + printCvInfo(o.ClusterVersionRef) + } + } + + return nil +} + +// getClusterVersions returns all cluster versions in K8S and return true if the cluster has a default cluster version +func getClusterVersions(dynamic dynamic.Interface, clusterDef string) (map[string]struct{}, string, bool, error) { + allClusterVersions := make(map[string]struct{}) + existedDefault := false + defaultVersion := "" + list, err := dynamic.Resource(types.ClusterVersionGVR()).List(context.Background(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", constant.ClusterDefLabelKey, clusterDef), + }) + if err != nil { + return nil, defaultVersion, false, err + } + for _, item := range list.Items { + allClusterVersions[item.GetName()] = struct{}{} + annotations := item.GetAnnotations() + if annotations != nil && annotations[constant.DefaultClusterVersionAnnotationKey] == annotationTrueValue { + if existedDefault { + return nil, defaultVersion, existedDefault, fmt.Errorf("clusterDef %s has more than one default cluster version", clusterDef) + } + existedDefault = true + defaultVersion = item.GetName() + } + } + return allClusterVersions, defaultVersion, existedDefault, nil +} + +func buildResourceLabels(clusterName string) map[string]string { + return map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.AppManagedByLabelKey: "kbcli", + } +} + +// build the cluster definition +// if the cluster definition is not specified, pick the cluster definition in the cluster component +// if neither of them is specified, return an error +func (o *CreateOptions) buildClusterDef(cls *appsv1alpha1.Cluster) error { + if o.ClusterDefRef != "" { + return nil + } + + if cls != nil && cls.Spec.ClusterDefRef != "" { + o.ClusterDefRef = cls.Spec.ClusterDefRef + return nil + } + + return fmt.Errorf("a valid cluster definition is needed, use --cluster-definition to specify one, run \"kbcli clusterdefinition list\" to show all cluster definitions") +} + +// build the cluster version +// if the cluster version is not specified, pick the cluster version in the cluster component +// if neither of them is specified, pick default cluster version +func (o *CreateOptions) buildClusterVersion(cls *appsv1alpha1.Cluster) { + if o.ClusterVersionRef != "" { + return + } + + if cls != nil && cls.Spec.ClusterVersionRef != "" { + o.ClusterVersionRef = cls.Spec.ClusterVersionRef + } +} + +func (o *CreateOptions) buildAnnotation(cls *appsv1alpha1.Cluster) { + if cls == nil { + return + } + + if o.Annotations == nil { + o.Annotations = cls.Annotations + } +} + +// parse the cluster component spec +// compatible with old file format that only specifies the components +func parseClusterComponentSpec(compByte []byte) ([]appsv1alpha1.ClusterComponentSpec, error) { + var compSpecs []appsv1alpha1.ClusterComponentSpec + var comps []map[string]interface{} + if err := json.Unmarshal(compByte, &comps); err != nil { + return nil, err + } + for _, comp := range comps { + var compSpec appsv1alpha1.ClusterComponentSpec + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(comp, &compSpec); err != nil { + return nil, err + } + compSpecs = append(compSpecs, compSpec) + } + + return compSpecs, nil +} diff --git a/internal/cli/cmd/cluster/create_test.go b/internal/cli/cmd/cluster/create_test.go index d2916ec2b..0f5447273 100644 --- a/internal/cli/cmd/cluster/create_test.go +++ b/internal/cli/cmd/cluster/create_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -34,6 +37,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/util" ) func generateComponents(component appsv1alpha1.ClusterComponentSpec, count int) []map[string]interface{} { @@ -55,6 +59,8 @@ func getResource(res corev1.ResourceRequirements, name corev1.ResourceName) inte } var _ = Describe("create", func() { + var componentClasses map[string]map[string]*appsv1alpha1.ComponentClassInstance + Context("setMonitor", func() { var components []map[string]interface{} BeforeEach(func() { @@ -133,11 +139,11 @@ var _ = Describe("create", func() { }) }) - checkComponent := func(comps []*appsv1alpha1.ClusterComponentSpec, storage string, replicas int32, cpu string, memory string) { + checkComponent := func(comps []*appsv1alpha1.ClusterComponentSpec, storage string, replicas int32, cpu string, memory string, storageClassName string, compIndex int) { Expect(comps).ShouldNot(BeNil()) - Expect(len(comps)).Should(Equal(2)) + Expect(len(comps)).Should(BeNumerically(">=", compIndex)) - comp := comps[0] + comp := comps[compIndex] Expect(getResource(comp.VolumeClaimTemplates[0].Spec.Resources, corev1.ResourceStorage)).Should(Equal(storage)) Expect(comp.Replicas).Should(BeEquivalentTo(replicas)) @@ -145,14 +151,21 @@ var _ = Describe("create", func() { Expect(resources).ShouldNot(BeNil()) Expect(getResource(resources, corev1.ResourceCPU)).Should(Equal(cpu)) Expect(getResource(resources, corev1.ResourceMemory)).Should(Equal(memory)) + + if storageClassName == "" { + Expect(comp.VolumeClaimTemplates[0].Spec.StorageClassName).Should(BeNil()) + } else { + Expect(*comp.VolumeClaimTemplates[0].Spec.StorageClassName).Should(Equal(storageClassName)) + } + } It("build default cluster component without environment", func() { dynamic := testing.FakeDynamicClient(testing.FakeClusterDef()) cd, _ := cluster.GetClusterDefByName(dynamic, testing.ClusterDefName) - comps, err := buildClusterComp(cd, nil) + comps, err := buildClusterComp(cd, nil, componentClasses) Expect(err).ShouldNot(HaveOccurred()) - checkComponent(comps, "20Gi", 1, "1", "1Gi") + checkComponent(comps, "20Gi", 1, "1", "1Gi", "", 0) }) It("build default cluster component with environment", func() { @@ -162,9 +175,9 @@ var _ = Describe("create", func() { viper.Set("CLUSTER_DEFAULT_MEMORY", "2Gi") dynamic := testing.FakeDynamicClient(testing.FakeClusterDef()) cd, _ := cluster.GetClusterDefByName(dynamic, testing.ClusterDefName) - comps, err := buildClusterComp(cd, nil) + comps, err := buildClusterComp(cd, nil, componentClasses) Expect(err).ShouldNot(HaveOccurred()) - checkComponent(comps, "5Gi", 1, "2", "2Gi") + checkComponent(comps, "5Gi", 1, "2", "2Gi", "", 0) }) It("build cluster component with set values", func() { @@ -172,15 +185,49 @@ var _ = Describe("create", func() { cd, _ := cluster.GetClusterDefByName(dynamic, testing.ClusterDefName) setsMap := map[string]map[setKey]string{ testing.ComponentDefName: { - keyCPU: "10", - keyMemory: "2Gi", - keyStorage: "10Gi", - keyReplicas: "10", + keyCPU: "10", + keyMemory: "2Gi", + keyStorage: "10Gi", + keyReplicas: "10", + keyStorageClass: "test", }, } - comps, err := buildClusterComp(cd, setsMap) + comps, err := buildClusterComp(cd, setsMap, componentClasses) Expect(err).Should(Succeed()) - checkComponent(comps, "10Gi", 10, "10", "2Gi") + checkComponent(comps, "10Gi", 10, "10", "2Gi", "test", 0) + + setsMap[testing.ComponentDefName][keySwitchPolicy] = "invalid" + cd.Spec.ComponentDefs[0].WorkloadType = appsv1alpha1.Replication + _, err = buildClusterComp(cd, setsMap, componentClasses) + Expect(err).Should(HaveOccurred()) + }) + + It("build multiple cluster component with set values", func() { + dynamic := testing.FakeDynamicClient(testing.FakeClusterDef()) + cd, _ := cluster.GetClusterDefByName(dynamic, testing.ClusterDefName) + setsMap := map[string]map[setKey]string{ + testing.ComponentDefName: { + keyCPU: "10", + keyMemory: "2Gi", + keyStorage: "10Gi", + keyReplicas: "10", + keyStorageClass: "test", + }, testing.ExtraComponentDefName: { + keyCPU: "5", + keyMemory: "1Gi", + keyStorage: "5Gi", + keyReplicas: "5", + keyStorageClass: "test-other", + }, + } + comps, err := buildClusterComp(cd, setsMap, componentClasses) + Expect(err).Should(Succeed()) + checkComponent(comps, "10Gi", 10, "10", "2Gi", "test", 0) + checkComponent(comps, "5Gi", 5, "5", "1Gi", "test-other", 1) + setsMap[testing.ComponentDefName][keySwitchPolicy] = "invalid" + cd.Spec.ComponentDefs[0].WorkloadType = appsv1alpha1.Replication + _, err = buildClusterComp(cd, setsMap, componentClasses) + Expect(err).Should(HaveOccurred()) }) It("build component and set values map", func() { @@ -189,7 +236,8 @@ var _ = Describe("create", func() { var comps []appsv1alpha1.ClusterComponentDefinition for _, n := range compDefNames { comp := appsv1alpha1.ClusterComponentDefinition{ - Name: n, + Name: n, + WorkloadType: appsv1alpha1.Replication, } comps = append(comps, comp) } @@ -286,8 +334,8 @@ var _ = Describe("create", func() { true, }, { - []string{"type=comp1,cpu=1,memory=2Gi,class=general-2c4g", "type=comp2,storage=10Gi,cpu=2,class=mo-1c8g"}, - []string{"my-comp"}, + []string{"type=comp1,cpu=1,memory=2Gi,class=general-2c4g", "type=comp2,storage=10Gi,cpu=2,class=mo-1c8g,replicas=3"}, + []string{"comp1", "comp2"}, map[string]map[setKey]string{ "comp1": { keyType: "comp1", @@ -296,10 +344,31 @@ var _ = Describe("create", func() { keyClass: "general-2c4g", }, "comp2": { - keyType: "comp2", - keyCPU: "2", - keyStorage: "10Gi", - keyClass: "mo-1c8g", + keyType: "comp2", + keyCPU: "2", + keyStorage: "10Gi", + keyClass: "mo-1c8g", + keyReplicas: "3", + }, + }, + true, + }, + { + []string{"switchPolicy=MaximumAvailability"}, + []string{"my-comp"}, + map[string]map[setKey]string{ + "my-comp": { + keySwitchPolicy: "MaximumAvailability", + }, + }, + true, + }, + { + []string{"storageClass=test"}, + []string{"my-comp"}, + map[string]map[setKey]string{ + "my-comp": { + keyStorageClass: "test", }, }, true, @@ -319,8 +388,9 @@ var _ = Describe("create", func() { }) It("build tolerations", func() { - raw := []string{"key=engineType,value=mongo,operator=Equal,effect=NoSchedule"} - res := buildTolerations(raw) + raw := []string{"engineType=mongo:NoSchedule"} + res, err := util.BuildTolerations(raw) + Expect(err).Should(BeNil()) Expect(len(res)).Should(Equal(1)) }) @@ -349,7 +419,7 @@ var _ = Describe("create", func() { Expect(setBackup(o, components).Error()).Should(ContainSubstring("is not completed")) By("test backup is completed") - mockBackupInfo(dynamic, backupName, clusterName) + mockBackupInfo(dynamic, backupName, clusterName, nil) Expect(setBackup(o, components)).Should(Succeed()) }) }) diff --git a/internal/cli/cmd/cluster/dataprotection.go b/internal/cli/cmd/cluster/dataprotection.go index 1ac44056e..8da27f899 100644 --- a/internal/cli/cmd/cluster/dataprotection.go +++ b/internal/cli/cmd/cluster/dataprotection.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -29,7 +32,6 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/apimachinery/pkg/util/json" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -42,6 +44,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/delete" + "github.com/apecloud/kubeblocks/internal/cli/edit" "github.com/apecloud/kubeblocks/internal/cli/list" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" @@ -50,119 +53,78 @@ import ( ) var ( + listBackupPolicyExample = templates.Examples(` + # list all backup policies + kbcli cluster list-backup-policy + + # using short cmd to list backup policy of the specified cluster + kbcli cluster list-bp mycluster + `) + editExample = templates.Examples(` + # edit backup policy + kbcli cluster edit-backup-policy + + # using short cmd to edit backup policy + kbcli cluster edit-bp + `) createBackupExample = templates.Examples(` - # create a backup - kbcli cluster backup cluster-name + # create a backup, the default type is snapshot. + kbcli cluster backup mycluster + + # create a snapshot backup + # create a snapshot of the cluster's persistent volume for backup + kbcli cluster backup mycluster --type snapshot + + # create a datafile backup + # backup all files under the data directory and save them to the specified storage, only full backup is supported now. + kbcli cluster backup mycluster --type datafile + + # create a backup with specified backup policy + kbcli cluster backup mycluster --policy `) listBackupExample = templates.Examples(` - # list all backup + # list all backups kbcli cluster list-backups `) deleteBackupExample = templates.Examples(` # delete a backup named backup-name kbcli cluster delete-backup cluster-name --name backup-name `) - listRestoreExample = templates.Examples(` - # list all restore - kbcli cluster list-restore - `) - deleteRestoreExample = templates.Examples(` - # delete a restore named restore-name - kbcli cluster delete-restore cluster-name --name restore-name - `) createRestoreExample = templates.Examples(` # restore a new cluster from a backup kbcli cluster restore new-cluster-name --backup backup-name + + # restore a new cluster from point in time + kbcli cluster restore new-cluster-name --restore-to-time "Apr 13,2023 18:40:35 UTC+0800" --source-cluster mycluster `) ) +const annotationTrueValue = "true" + type CreateBackupOptions struct { BackupType string `json:"backupType"` BackupName string `json:"backupName"` Role string `json:"role,omitempty"` BackupPolicy string `json:"backupPolicy"` - TTL string `json:"ttl,omitempty"` - create.BaseOptions -} -type CreateBackupPolicyOptions struct { - ClusterName string `json:"clusterName,omitempty"` - TTL string `json:"ttl,omitempty"` - ConnectionSecret string `json:"connectionSecret,omitempty"` - PolicyTemplate string `json:"policyTemplate,omitempty"` - Role string `json:"role,omitempty"` - create.BaseOptions + create.CreateOptions `json:"-"` } -type CreateVolumeSnapshotClassOptions struct { - Driver string `json:"driver"` - Name string `json:"name"` - create.BaseOptions +type ListBackupOptions struct { + *list.ListOptions + BackupName string } -func (o *CreateVolumeSnapshotClassOptions) Complete() error { - objs, err := o.Dynamic. - Resource(types.StorageClassGVR()). - List(context.TODO(), metav1.ListOptions{}) - if err != nil { +func (o *CreateBackupOptions) CompleteBackup() error { + if err := o.Complete(); err != nil { return err } - for _, sc := range objs.Items { - annotations := sc.GetAnnotations() - if annotations == nil { - continue - } - if annotations["storageclass.kubernetes.io/is-default-class"] == "true" { - o.Driver, _, _ = unstructured.NestedString(sc.Object, "provisioner") - o.Name = "default-vsc" - } - } - // warning if not found default storage class - if o.Driver == "" { - return fmt.Errorf("no default StorageClass found, snapshot-controller may not work") - } - return nil -} - -func (o *CreateVolumeSnapshotClassOptions) Create() error { - objs, err := o.Dynamic. - Resource(types.VolumeSnapshotClassGVR()). - List(context.TODO(), metav1.ListOptions{}) - if err != nil { - return err - } - for _, vsc := range objs.Items { - annotations := vsc.GetAnnotations() - if annotations == nil { - continue - } - // skip creation if default volumesnapshotclass exists. - if annotations["snapshot.storage.kubernetes.io/is-default-class"] == "true" { - return nil - } - } - - inputs := create.Inputs{ - CueTemplateName: "volumesnapshotclass_template.cue", - ResourceName: "volumesnapshotclasses", - Group: "snapshot.storage.k8s.io", - Version: types.K8sCoreAPIVersion, - BaseOptionsObj: &o.BaseOptions, - Options: o, - } - if err := o.BaseOptions.Run(inputs); err != nil { - return err - } - return nil -} - -func (o *CreateBackupOptions) Complete() error { // generate backupName if len(o.BackupName) == 0 { o.BackupName = strings.Join([]string{"backup", o.Namespace, o.Name, time.Now().Format("20060102150405")}, "-") } - return nil + return o.CreateOptions.Complete() } func (o *CreateBackupOptions) Validate() error { @@ -170,142 +132,97 @@ func (o *CreateBackupOptions) Validate() error { if o.Name == "" { return fmt.Errorf("missing cluster name") } - - connectionSecret, err := o.getConnectionSecret() - if err != nil { - return err - } - - backupPolicyTemplate, err := o.getDefaultBackupPolicyTemplate() - if err != nil { - return err + if o.BackupPolicy == "" { + return o.completeDefaultBackupPolicy() } + // check if backup policy exists + _, err := o.Dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.Namespace).Get(context.TODO(), o.BackupPolicy, metav1.GetOptions{}) + // TODO: check if pvc exists + return err +} - role, err := o.getDefaultRole() +// completeDefaultBackupPolicy completes the default backup policy. +func (o *CreateBackupOptions) completeDefaultBackupPolicy() error { + defaultBackupPolicyName, err := o.getDefaultBackupPolicy() if err != nil { return err } - // apply backup policy - policyOptions := CreateBackupPolicyOptions{ - TTL: o.TTL, - ClusterName: o.Name, - ConnectionSecret: connectionSecret, - PolicyTemplate: backupPolicyTemplate, - BaseOptions: o.BaseOptions, - } - if role != "" { - policyOptions.Role = role - } - policyOptions.Name = "backup-policy-" + o.Namespace + "-" + o.Name - inputs := create.Inputs{ - CueTemplateName: "backuppolicy_template.cue", - ResourceName: types.ResourceBackupPolicies, - Group: types.DPAPIGroup, - Version: types.DPAPIVersion, - BaseOptionsObj: &policyOptions.BaseOptions, - Options: policyOptions, - } - - // cluster backup do 2 following things: - // 1. create or apply the backupPolicy, cause backupJob reference to a backupPolicy, - // and backupPolicy reference to the cluster. - // so it need apply the backupPolicy after the first backupPolicy created. - // 2. create a backupJob. - if err := policyOptions.BaseOptions.Run(inputs); err != nil && !apierrors.IsAlreadyExists(err) { - return err - } - o.BackupPolicy = policyOptions.Name - + o.BackupPolicy = defaultBackupPolicyName return nil } -func (o *CreateBackupOptions) getConnectionSecret() (string, error) { - // find secret from cluster label - opts := metav1.ListOptions{ - LabelSelector: fmt.Sprintf("%s=%s,%s=%s", - constant.AppInstanceLabelKey, o.Name, - constant.AppManagedByLabelKey, constant.AppName), - } - gvr := schema.GroupVersionResource{Version: "v1", Resource: "secrets"} - secretObjs, err := o.Dynamic.Resource(gvr).Namespace(o.Namespace).List(context.TODO(), opts) - if err != nil { - return "", err - } - if len(secretObjs.Items) == 0 { - return "", fmt.Errorf("not found connection credential for cluster %s", o.Name) - } - return secretObjs.Items[0].GetName(), nil -} - -func (o *CreateBackupOptions) getDefaultBackupPolicyTemplate() (string, error) { +func (o *CreateBackupOptions) getDefaultBackupPolicy() (string, error) { clusterObj, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) if err != nil { return "", err } - // find backupPolicyTemplate from cluster label + // TODO: support multiple components backup, add --componentDef flag opts := metav1.ListOptions{ LabelSelector: fmt.Sprintf("%s=%s", - constant.ClusterDefLabelKey, clusterObj.GetLabels()[constant.ClusterDefLabelKey]), + constant.AppInstanceLabelKey, clusterObj.GetName()), } objs, err := o.Dynamic. - Resource(types.BackupPolicyTemplateGVR()). + Resource(types.BackupPolicyGVR()).Namespace(o.Namespace). List(context.TODO(), opts) if err != nil { return "", err } if len(objs.Items) == 0 { - return "", fmt.Errorf("not found any backupPolicyTemplate for cluster %s", o.Name) + return "", fmt.Errorf(`not found any backup policy for cluster "%s"`, o.Name) } - return objs.Items[0].GetName(), nil -} - -func (o *CreateBackupOptions) getDefaultRole() (string, error) { - gvr := schema.GroupVersionResource{Version: "v1", Resource: "pods"} - opts := metav1.ListOptions{ - LabelSelector: fmt.Sprintf("%s=%s", - constant.AppInstanceLabelKey, o.Name), - } - objs, err := o.Dynamic.Resource(gvr).Namespace(o.Namespace).List(context.TODO(), opts) - if err != nil { - return "", err + var defaultBackupPolicies []unstructured.Unstructured + for _, obj := range objs.Items { + if obj.GetAnnotations()[constant.DefaultBackupPolicyAnnotationKey] == annotationTrueValue { + defaultBackupPolicies = append(defaultBackupPolicies, obj) + } } - if len(objs.Items) == 0 { - return "", fmt.Errorf("not found any pods for cluster %s", o.Name) + if len(defaultBackupPolicies) == 0 { + return "", fmt.Errorf(`not found any default backup policy for cluster "%s"`, o.Name) } - pod := objs.Items[0] - // TODO(dsj):(hack fix) apecloud-mysql just support backup snapshot on the leader pod, - // backup snapshot on the follower will be support at the next version. - if o.BackupType == "snapshot" && pod.GetLabels()[constant.WorkloadTypeLabelKey] == string(appsv1alpha1.Consensus) { - return "leader", nil + if len(defaultBackupPolicies) > 1 { + return "", fmt.Errorf(`cluster "%s" has multiple default backup policies`, o.Name) } - return "", nil + return defaultBackupPolicies[0].GetName(), nil } func NewCreateBackupCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := &CreateBackupOptions{BaseOptions: create.BaseOptions{IOStreams: streams}} - inputs := create.Inputs{ - Use: "backup", - Short: "Create a backup.", - Example: createBackupExample, - CueTemplateName: "backup_template.cue", - ResourceName: types.ResourceBackups, - Group: types.DPAPIGroup, - Version: types.DPAPIVersion, - BaseOptionsObj: &o.BaseOptions, - Options: o, - Factory: f, - Complete: o.Complete, - Validate: o.Validate, - ResourceNameGVRForCompletion: types.ClusterGVR(), - BuildFlags: func(cmd *cobra.Command) { - cmd.Flags().StringVar(&o.BackupType, "backup-type", "snapshot", "Backup type") - cmd.Flags().StringVar(&o.BackupName, "backup-name", "", "Backup name") - cmd.Flags().StringVar(&o.Role, "role", "", "backup on cluster role") - cmd.Flags().StringVar(&o.TTL, "ttl", "168h0m0s", "Time to live") + customOutPut := func(opt *create.CreateOptions) { + output := fmt.Sprintf("Backup %s created successfully, you can view the progress:", opt.Name) + printer.PrintLine(output) + nextLine := fmt.Sprintf("\tkbcli cluster list-backups --name=%s -n %s", opt.Name, opt.Namespace) + printer.PrintLine(nextLine) + } + + o := &CreateBackupOptions{ + CreateOptions: create.CreateOptions{ + IOStreams: streams, + Factory: f, + GVR: types.BackupGVR(), + CueTemplateName: "backup_template.cue", + CustomOutPut: customOutPut, }, } - return create.BuildCommand(inputs) + o.CreateOptions.Options = o + + cmd := &cobra.Command{ + Use: "backup NAME", + Short: "Create a backup for the cluster.", + Example: createBackupExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CompleteBackup()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + cmd.Flags().StringVar(&o.BackupType, "type", "snapshot", "Backup type") + cmd.Flags().StringVar(&o.BackupName, "name", "", "Backup name") + cmd.Flags().StringVar(&o.BackupPolicy, "policy", "", "Backup policy name, this flag will be ignored when backup-type is snapshot") + + return cmd } // getClusterNameMap get cluster list by namespace and convert to map. @@ -321,7 +238,7 @@ func getClusterNameMap(dClient dynamic.Interface, o *list.ListOptions) (map[stri return clusterMap, nil } -func printBackupList(o *list.ListOptions) error { +func printBackupList(o ListBackupOptions) error { dynamic, err := o.Factory.DynamicClient() if err != nil { return err @@ -339,7 +256,7 @@ func printBackupList(o *list.ListOptions) error { return nil } - clusterNameMap, err := getClusterNameMap(dynamic, o) + clusterNameMap, err := getClusterNameMap(dynamic, o.ListOptions) if err != nil { return err } @@ -347,7 +264,7 @@ func printBackupList(o *list.ListOptions) error { // sort the unstructured objects with the creationTimestamp in positive order sort.Sort(unstructuredList(backupList.Items)) tbl := printer.NewTablePrinter(o.Out) - tbl.SetHeader("NAME", "CLUSTER", "TYPE", "STATUS", "TOTAL-SIZE", "DURATION", "CREATE-TIME", "COMPLETION-TIME") + tbl.SetHeader("NAME", "CLUSTER", "TYPE", "STATUS", "TOTAL-SIZE", "DURATION", "CREATE-TIME", "COMPLETION-TIME", "EXPIRATION") for _, obj := range backupList.Items { backup := &dataprotectionv1alpha1.Backup{} if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, backup); err != nil { @@ -361,15 +278,23 @@ func printBackupList(o *list.ListOptions) error { if backup.Status.Duration != nil { durationStr = duration.HumanDuration(backup.Status.Duration.Duration) } + if len(o.BackupName) > 0 { + if o.BackupName == obj.GetName() { + tbl.AddRow(backup.Name, clusterName, backup.Spec.BackupType, backup.Status.Phase, backup.Status.TotalSize, + durationStr, util.TimeFormat(&backup.CreationTimestamp), util.TimeFormat(backup.Status.CompletionTimestamp)) + } + continue + } tbl.AddRow(backup.Name, clusterName, backup.Spec.BackupType, backup.Status.Phase, backup.Status.TotalSize, - durationStr, util.TimeFormat(&backup.CreationTimestamp), util.TimeFormat(backup.Status.CompletionTimestamp)) + durationStr, util.TimeFormat(&backup.CreationTimestamp), util.TimeFormat(backup.Status.CompletionTimestamp), + util.TimeFormat(backup.Status.Expiration)) } tbl.Print() return nil } func NewListBackupCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := list.NewListOptions(f, streams, types.BackupGVR()) + o := &ListBackupOptions{ListOptions: list.NewListOptions(f, streams, types.BackupGVR())} cmd := &cobra.Command{ Use: "list-backups", Short: "List backups.", @@ -380,10 +305,11 @@ func NewListBackupCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *c o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) o.Names = nil util.CheckErr(o.Complete()) - util.CheckErr(printBackupList(o)) + util.CheckErr(printBackupList(*o)) }, } o.AddFlags(cmd) + cmd.Flags().StringVar(&o.BackupName, "name", "", "The backup name to get the details.") return cmd } @@ -404,7 +330,7 @@ func NewDeleteBackupCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) return cmd } -// completeForDeleteBackup complete cmd for delete backup +// completeForDeleteBackup completes cmd for delete backup func completeForDeleteBackup(o *delete.DeleteOptions, args []string) error { if len(args) == 0 { return errors.New("Missing cluster name") @@ -416,8 +342,8 @@ func completeForDeleteBackup(o *delete.DeleteOptions, args []string) error { return errors.New("Missing --name as backup name.") } if o.Force && len(o.Names) == 0 { - // do force action, if specified --force and not specified --name, all backups with the cluster will be deleted - // if no specify backup name and cluster name is specified. it will delete all backups with the cluster + // do force action, for --force and --name unset, delete all backups of the cluster + // if backup name unset and cluster name set, delete all backups of the cluster o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) o.ConfirmedNames = args } @@ -428,7 +354,13 @@ func completeForDeleteBackup(o *delete.DeleteOptions, args []string) error { type CreateRestoreOptions struct { // backup name to restore in creation Backup string `json:"backup,omitempty"` - create.BaseOptions + + // point in time recovery args + RestoreTime *time.Time `json:"restoreTime,omitempty"` + RestoreTimeStr string `json:"restoreTimeStr,omitempty"` + SourceCluster string `json:"sourceCluster,omitempty"` + + create.CreateOptions `json:"-"` } func (o *CreateRestoreOptions) getClusterObject(backup *dataprotectionv1alpha1.Backup) (*appsv1alpha1.Cluster, error) { @@ -438,7 +370,7 @@ func (o *CreateRestoreOptions) getClusterObject(backup *dataprotectionv1alpha1.B return nil, err } if apierrors.IsNotFound(err) { - // if the source cluster does not exist, obtain it from the cluster snapshot of the backup. + // if the source cluster does not exist, get it from the cluster snapshot of the backup. clusterString, ok := backup.Annotations[constant.ClusterSnapshotAnnotationKey] if !ok { return nil, fmt.Errorf("source cluster: %s not found", clusterName) @@ -449,7 +381,16 @@ func (o *CreateRestoreOptions) getClusterObject(backup *dataprotectionv1alpha1.B } func (o *CreateRestoreOptions) Run() error { - // get backup job + if o.Backup != "" { + return o.runRestoreFromBackup() + } else if o.RestoreTime != nil { + return o.runPITR() + } + return nil +} + +func (o *CreateRestoreOptions) runRestoreFromBackup() error { + // get backup backup := &dataprotectionv1alpha1.Backup{} if err := cluster.GetK8SClientObject(o.Dynamic, backup, types.BackupGVR(), o.Namespace, o.Backup); err != nil { return err @@ -461,21 +402,25 @@ func (o *CreateRestoreOptions) Run() error { return errors.Errorf(`missing source cluster in backup "%s", "app.kubernetes.io/instance" is empty in labels.`, o.Backup) } // get the cluster object and set the annotation for restore - cluster, err := o.getClusterObject(backup) + clusterObj, err := o.getClusterObject(backup) if err != nil { return err } - restoreAnnotation, err := getRestoreFromBackupAnnotation(backup, len(cluster.Spec.ComponentSpecs), cluster.Spec.ComponentSpecs[0].Name) + restoreAnnotation, err := getRestoreFromBackupAnnotation(backup, len(clusterObj.Spec.ComponentSpecs), clusterObj.Spec.ComponentSpecs[0].Name) if err != nil { return err } - cluster.Status = appsv1alpha1.ClusterStatus{} - cluster.ObjectMeta = metav1.ObjectMeta{ - Namespace: cluster.Namespace, + clusterObj.ObjectMeta = metav1.ObjectMeta{ + Namespace: clusterObj.Namespace, Name: o.Name, Annotations: map[string]string{constant.RestoreFromBackUpAnnotationKey: restoreAnnotation}, } + return o.createCluster(clusterObj) +} + +func (o *CreateRestoreOptions) createCluster(cluster *appsv1alpha1.Cluster) error { clusterGVR := types.ClusterGVR() + cluster.Status = appsv1alpha1.ClusterStatus{} cluster.TypeMeta = metav1.TypeMeta{ Kind: types.KindCluster, APIVersion: clusterGVR.Group + "/" + clusterGVR.Version, @@ -495,7 +440,89 @@ func (o *CreateRestoreOptions) Run() error { return nil } +func (o *CreateRestoreOptions) runPITR() error { + objs, err := o.Dynamic.Resource(types.BackupGVR()).Namespace(o.Namespace). + List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", + constant.AppInstanceLabelKey, o.SourceCluster), + }) + if err != nil { + return err + } + backup := &dataprotectionv1alpha1.Backup{} + + // no need to check items len because it is validated by o.validateRestoreTime(). + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(objs.Items[0].Object, backup); err != nil { + return err + } + // TODO: use opsRequest to create cluster. + // get the cluster object and set the annotation for restore + clusterObj, err := o.getClusterObject(backup) + if err != nil { + return err + } + clusterObj.ObjectMeta = metav1.ObjectMeta{ + Namespace: clusterObj.Namespace, + Name: o.Name, + Annotations: map[string]string{ + // TODO: use constant annotation key + "kubeblocks.io/restore-from-time": o.RestoreTime.Format(time.RFC3339), + "kubeblocks.io/restore-from-source-cluster": o.SourceCluster, + }, + } + return o.createCluster(clusterObj) +} + +func isTimeInRange(t time.Time, start time.Time, end time.Time) bool { + return !t.Before(start) && !t.After(end) +} + +func (o *CreateRestoreOptions) validateRestoreTime() error { + if o.RestoreTimeStr == "" && o.SourceCluster == "" { + return nil + } + if o.RestoreTimeStr == "" && o.SourceCluster == "" { + return fmt.Errorf("--source-cluster must be specified if specified --restore-to-time") + } + restoreTime, err := util.TimeParse(o.RestoreTimeStr, time.Second) + if err != nil { + return err + } + o.RestoreTime = &restoreTime + objs, err := o.Dynamic.Resource(types.BackupGVR()).Namespace(o.Namespace). + List(context.TODO(), metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", + constant.AppInstanceLabelKey, o.SourceCluster), + }) + if err != nil { + return err + } + backups := make([]dataprotectionv1alpha1.Backup, 0) + for _, i := range objs.Items { + obj := dataprotectionv1alpha1.Backup{} + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(i.Object, &obj); err != nil { + return err + } + backups = append(backups, obj) + } + recoverableTime := dataprotectionv1alpha1.GetRecoverableTimeRange(backups) + for _, i := range recoverableTime { + if isTimeInRange(restoreTime, i.StartTime.Time, i.StopTime.Time) { + return nil + } + } + return fmt.Errorf("restore-to-time is out of time range, you can view the recoverable time: \n"+ + "\tkbcli cluster describe %s -n %s", o.SourceCluster, o.Namespace) +} + func (o *CreateRestoreOptions) Validate() error { + if o.Backup == "" && o.RestoreTimeStr == "" { + return fmt.Errorf("must be specified one of the --backup or --restore-to-time") + } + if err := o.validateRestoreTime(); err != nil { + return err + } + if o.Name == "" { name, err := generateClusterName(o.Dynamic, o.Namespace) if err != nil { @@ -506,88 +533,107 @@ func (o *CreateRestoreOptions) Validate() error { } o.Name = name } - if o.Backup == "" { - return fmt.Errorf("backup name should be specified by --backup") - } return nil } func NewCreateRestoreCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := &CreateRestoreOptions{} - o.IOStreams = streams - inputs := create.Inputs{ - BaseOptionsObj: &create.BaseOptions{IOStreams: streams}, - Options: o, - Factory: f, + o.CreateOptions = create.CreateOptions{ + IOStreams: streams, + Factory: f, + Options: o, } + cmd := &cobra.Command{ Use: "restore", Short: "Restore a new cluster from backup.", Example: createRestoreExample, ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(o.Complete(inputs, args)) + o.Args = args + util.CheckErr(o.Complete()) util.CheckErr(o.Validate()) util.CheckErr(o.Run()) }, } cmd.Flags().StringVar(&o.Backup, "backup", "", "Backup name") + cmd.Flags().StringVar(&o.RestoreTimeStr, "restore-to-time", "", "point in time recovery(PITR)") + cmd.Flags().StringVar(&o.SourceCluster, "source-cluster", "", "source cluster name") return cmd } -func NewListRestoreCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := list.NewListOptions(f, streams, types.RestoreJobGVR()) +func NewListBackupPolicyCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := list.NewListOptions(f, streams, types.BackupPolicyGVR()) cmd := &cobra.Command{ - Use: "list-restores", - Short: "List all restore jobs.", - Aliases: []string{"ls-restores"}, - Example: listRestoreExample, + Use: "list-backup-policy", + Short: "List backups policies.", + Aliases: []string{"list-bp"}, + Example: listBackupPolicyExample, ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), Run: func(cmd *cobra.Command, args []string) { o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) o.Names = nil - _, err := o.Run() - util.CheckErr(err) + util.CheckErr(o.Complete()) + util.CheckErr(printBackupPolicyList(*o)) }, } o.AddFlags(cmd) return cmd } -func NewDeleteRestoreCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := delete.NewDeleteOptions(f, streams, types.RestoreJobGVR()) +// printBackupPolicyList prints the backup policy list. +func printBackupPolicyList(o list.ListOptions) error { + dynamic, err := o.Factory.DynamicClient() + if err != nil { + return err + } + backupPolicyList, err := dynamic.Resource(types.BackupPolicyGVR()).Namespace(o.Namespace).List(context.TODO(), metav1.ListOptions{ + LabelSelector: o.LabelSelector, + FieldSelector: o.FieldSelector, + }) + if err != nil { + return err + } + + if len(backupPolicyList.Items) == 0 { + o.PrintNotFoundResources() + return nil + } + + tbl := printer.NewTablePrinter(o.Out) + tbl.SetHeader("NAME", "DEFAULT", "CLUSTER", "CREATE-TIME", "STATUS") + for _, obj := range backupPolicyList.Items { + defaultPolicy, ok := obj.GetAnnotations()[constant.DefaultBackupPolicyAnnotationKey] + backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, backupPolicy); err != nil { + return err + } + if !ok { + defaultPolicy = "false" + } + createTime := obj.GetCreationTimestamp() + tbl.AddRow(obj.GetName(), defaultPolicy, obj.GetLabels()[constant.AppInstanceLabelKey], + util.TimeFormat(&createTime), backupPolicy.Status.Phase) + } + tbl.Print() + return nil +} + +func NewEditBackupPolicyCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := edit.NewEditOptions(f, streams, types.BackupPolicyGVR()) cmd := &cobra.Command{ - Use: "delete-restore", - Short: "Delete a restore job.", - Example: deleteRestoreExample, - ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Use: "edit-backup-policy", + DisableFlagsInUseLine: true, + Aliases: []string{"edit-bp"}, + Short: "Edit backup policy", + Example: editExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.BackupPolicyGVR()), Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(completeForDeleteRestore(o, args)) - util.CheckErr(o.Run()) + cmdutil.CheckErr(o.Complete(cmd, args)) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) }, } - cmd.Flags().StringSliceVar(&o.Names, "name", []string{}, "Restore names") o.AddFlags(cmd) return cmd } - -// completeForDeleteRestore complete cmd for delete restore -func completeForDeleteRestore(o *delete.DeleteOptions, args []string) error { - if len(args) == 0 { - return errors.New("Missing cluster name") - } - if len(args) > 1 { - return errors.New("Only supported delete the restore of one cluster") - } - if !o.Force && len(o.Names) == 0 { - return errors.New("Missing --name as restore name.") - } - if o.Force && len(o.Names) == 0 { - // do force action, if specified --force and not specified --name, all restores with the cluster will be deleted - // if no specify restore name and cluster name is specified. it will delete all restores with the cluster - o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) - o.ConfirmedNames = args - } - o.ConfirmedNames = o.Names - return nil -} diff --git a/internal/cli/cmd/cluster/dataprotection_test.go b/internal/cli/cmd/cluster/dataprotection_test.go index 34f2e0430..2a9415ba2 100644 --- a/internal/cli/cmd/cluster/dataprotection_test.go +++ b/internal/cli/cmd/cluster/dataprotection_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -20,10 +23,12 @@ import ( "bytes" "context" "fmt" + "strings" "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/runtime" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" @@ -35,6 +40,7 @@ import ( clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/delete" "github.com/apecloud/kubeblocks/internal/cli/list" @@ -42,13 +48,16 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/constant" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) var _ = Describe("DataProtection", func() { + const policyName = "policy" var streams genericclioptions.IOStreams var tf *cmdtesting.TestFactory + var out *bytes.Buffer BeforeEach(func() { - streams, _, _, _ = genericclioptions.NewTestIOStreams() + streams, _, out, _ = genericclioptions.NewTestIOStreams() tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) tf.Client = &clientfake.RESTClient{} }) @@ -58,45 +67,78 @@ var _ = Describe("DataProtection", func() { }) Context("backup", func() { + initClient := func(policies ...*dataprotectionv1alpha1.BackupPolicy) { + clusterDef := testing.FakeClusterDef() + cluster := testing.FakeCluster(testing.ClusterName, testing.Namespace) + clusterDefLabel := map[string]string{ + constant.ClusterDefLabelKey: clusterDef.Name, + } + cluster.SetLabels(clusterDefLabel) + pods := testing.FakePods(1, testing.Namespace, testing.ClusterName) + objects := []runtime.Object{ + cluster, clusterDef, &pods.Items[0], + } + for _, v := range policies { + objects = append(objects, v) + } + tf.FakeDynamicClient = testing.FakeDynamicClient(objects...) + } + + It("list-backup-policy", func() { + By("fake client") + defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) + policy2 := testing.FakeBackupPolicy("policy1", testing.ClusterName) + initClient(defaultBackupPolicy, policy2) + + By("test list-backup-policy cmd") + cmd := NewListBackupPolicyCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + cmd.Run(cmd, nil) + Expect(out.String()).Should(ContainSubstring(defaultBackupPolicy.Name)) + Expect(out.String()).Should(ContainSubstring("true")) + Expect(len(strings.Split(strings.Trim(out.String(), "\n"), "\n"))).Should(Equal(3)) + }) + It("validate create backup", func() { By("without cluster name") o := &CreateBackupOptions{ - BaseOptions: create.BaseOptions{ + CreateOptions: create.CreateOptions{ Dynamic: testing.FakeDynamicClient(), IOStreams: streams, + Factory: tf, }, } - o.IOStreams = streams Expect(o.Validate()).To(MatchError("missing cluster name")) - By("not found connection secret") + By("test without default backupPolicy") o.Name = testing.ClusterName - Expect(o.Validate()).Should(HaveOccurred()) + o.Namespace = testing.Namespace + initClient() + o.Dynamic = tf.FakeDynamicClient + Expect(o.Validate()).Should(MatchError(fmt.Errorf(`not found any backup policy for cluster "%s"`, testing.ClusterName))) + + By("test with two default backupPolicy") + defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) + initClient(defaultBackupPolicy, testing.FakeBackupPolicy("policy2", testing.ClusterName)) + o.Dynamic = tf.FakeDynamicClient + Expect(o.Validate()).Should(MatchError(fmt.Errorf(`cluster "%s" has multiple default backup policies`, o.Name))) + + By("test with one default backupPolicy") + initClient(defaultBackupPolicy) + o.Dynamic = tf.FakeDynamicClient + Expect(o.Validate()).Should(Succeed()) }) It("run backup command", func() { - clusterDef := testing.FakeClusterDef() - cluster := testing.FakeCluster(testing.ClusterName, testing.Namespace) - clusterDefLabel := map[string]string{ - constant.ClusterDefLabelKey: clusterDef.Name, - } - cluster.SetLabels(clusterDefLabel) - - template := testing.FakeBackupPolicyTemplate() - template.SetLabels(clusterDefLabel) - - secrets := testing.FakeSecrets(testing.Namespace, testing.ClusterName) - pods := testing.FakePods(1, testing.Namespace, testing.ClusterName) - tf.FakeDynamicClient = fake.NewSimpleDynamicClient( - scheme.Scheme, &secrets.Items[0], cluster, clusterDef, template, &pods.Items[0]) - tf.Client = &clientfake.RESTClient{} + defaultBackupPolicy := testing.FakeBackupPolicy(policyName, testing.ClusterName) + initClient(defaultBackupPolicy) + By("test with specified backupPolicy") cmd := NewCreateBackupCmd(tf, streams) Expect(cmd).ShouldNot(BeNil()) // must succeed otherwise exit 1 and make test fails - _ = cmd.Flags().Set("backup-type", "snapshot") + _ = cmd.Flags().Set("backup-policy", defaultBackupPolicy.Name) cmd.Run(cmd, []string{testing.ClusterName}) }) - }) It("delete-backup", func() { @@ -129,7 +171,7 @@ var _ = Describe("DataProtection", func() { Expect(cmd).ShouldNot(BeNil()) By("test list-backup cmd with no backup") tf.FakeDynamicClient = testing.FakeDynamicClient() - o := list.NewListOptions(tf, streams, types.BackupGVR()) + o := ListBackupOptions{ListOptions: list.NewListOptions(tf, streams, types.BackupGVR())} Expect(printBackupList(o)).Should(Succeed()) Expect(o.ErrOut.(*bytes.Buffer).String()).Should(ContainSubstring("No backups found")) @@ -144,36 +186,6 @@ var _ = Describe("DataProtection", func() { Expect(o.Out.(*bytes.Buffer).String()).Should(ContainSubstring("apecloud-mysql (deleted)")) }) - It("delete-restore", func() { - By("test delete-restore cmd") - cmd := NewDeleteRestoreCmd(tf, streams) - Expect(cmd).ShouldNot(BeNil()) - - args := []string{"test1"} - clusterLabel := util.BuildLabelSelectorByNames("", args) - - By("test delete-restore with cluster") - o := delete.NewDeleteOptions(tf, streams, types.BackupGVR()) - Expect(completeForDeleteRestore(o, args)).Should(HaveOccurred()) - - By("test delete-restore with cluster and force") - o.Force = true - Expect(completeForDeleteRestore(o, args)).Should(Succeed()) - Expect(o.LabelSelector == clusterLabel).Should(BeTrue()) - - By("test delete-restore with cluster and force and labels") - o.Force = true - customLabel := "test=test" - o.LabelSelector = customLabel - Expect(completeForDeleteRestore(o, args)).Should(Succeed()) - Expect(o.LabelSelector == customLabel+","+clusterLabel).Should(BeTrue()) - }) - - It("list-restore", func() { - cmd := NewListRestoreCmd(tf, streams) - Expect(cmd).ShouldNot(BeNil()) - }) - It("restore", func() { timestamp := time.Now().Format("20060102150405") backupName := "backup-test-" + timestamp @@ -186,26 +198,24 @@ var _ = Describe("DataProtection", func() { constant.ClusterDefLabelKey: clusterDef.Name, } cluster.SetLabels(clusterDefLabel) - - template := testing.FakeBackupPolicyTemplate() - template.SetLabels(clusterDefLabel) + backupPolicy := testing.FakeBackupPolicy("backPolicy", cluster.Name) pods := testing.FakePods(1, testing.Namespace, clusterName) tf.FakeDynamicClient = fake.NewSimpleDynamicClient( - scheme.Scheme, &secrets.Items[0], &pods.Items[0], cluster, template) + scheme.Scheme, &secrets.Items[0], &pods.Items[0], cluster, backupPolicy) tf.FakeDynamicClient = fake.NewSimpleDynamicClient( - scheme.Scheme, &secrets.Items[0], &pods.Items[0], clusterDef, cluster, template) + scheme.Scheme, &secrets.Items[0], &pods.Items[0], clusterDef, cluster, backupPolicy) tf.Client = &clientfake.RESTClient{} // create backup cmd := NewCreateBackupCmd(tf, streams) Expect(cmd).ShouldNot(BeNil()) - _ = cmd.Flags().Set("backup-type", "snapshot") - _ = cmd.Flags().Set("backup-name", backupName) + _ = cmd.Flags().Set("type", "snapshot") + _ = cmd.Flags().Set("name", backupName) cmd.Run(nil, []string{clusterName}) By("restore new cluster from source cluster which is not deleted") // mock backup is ok - mockBackupInfo(tf.FakeDynamicClient, backupName, clusterName) + mockBackupInfo(tf.FakeDynamicClient, backupName, clusterName, nil) cmdRestore := NewCreateRestoreCmd(tf, streams) Expect(cmdRestore != nil).To(BeTrue()) _ = cmdRestore.Flags().Set("backup", backupName) @@ -213,7 +223,7 @@ var _ = Describe("DataProtection", func() { By("restore new cluster from source cluster which is deleted") // mock cluster is not lived in kubernetes - mockBackupInfo(tf.FakeDynamicClient, backupName, "deleted-cluster") + mockBackupInfo(tf.FakeDynamicClient, backupName, "deleted-cluster", nil) cmdRestore.Run(nil, []string{newClusterName + "1"}) By("run restore cmd with cluster spec.affinity=nil") @@ -222,21 +232,65 @@ var _ = Describe("DataProtection", func() { k8sapitypes.MergePatchType, patchCluster, metav1.PatchOptions{}) cmdRestore.Run(nil, []string{newClusterName + "-with-nil-affinity"}) }) + + It("restore-to-time", func() { + timestamp := time.Now().Format("20060102150405") + backupName := "backup-test-" + timestamp + clusterName := "source-cluster-" + timestamp + secrets := testing.FakeSecrets(testing.Namespace, clusterName) + clusterDef := testing.FakeClusterDef() + cluster := testing.FakeCluster(clusterName, testing.Namespace) + clusterDefLabel := map[string]string{ + constant.ClusterDefLabelKey: clusterDef.Name, + } + cluster.SetLabels(clusterDefLabel) + backupPolicy := testing.FakeBackupPolicy("backPolicy", cluster.Name) + backupTypeMeta := testing.FakeBackup("backup-none").TypeMeta + backupLabels := map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.KBAppComponentLabelKey: "test", + } + now := metav1.Now() + baseBackup := testapps.NewBackupFactory(testing.Namespace, "backup-base"). + SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). + SetBackLog(now.Add(-time.Minute), now.Add(-time.Second)). + SetLabels(backupLabels).GetObject() + baseBackup.TypeMeta = backupTypeMeta + baseBackup.Status.Phase = dataprotectionv1alpha1.BackupCompleted + incrBackup := testapps.NewBackupFactory(testing.Namespace, backupName). + SetBackupType(dataprotectionv1alpha1.BackupTypeLogFile). + SetBackLog(now.Add(-time.Minute), now.Add(time.Minute)). + SetLabels(backupLabels).GetObject() + incrBackup.TypeMeta = backupTypeMeta + + pods := testing.FakePods(1, testing.Namespace, clusterName) + tf.FakeDynamicClient = fake.NewSimpleDynamicClient( + scheme.Scheme, &secrets.Items[0], &pods.Items[0], cluster, backupPolicy, baseBackup, incrBackup) + tf.Client = &clientfake.RESTClient{} + + By("restore new cluster from source cluster which is not deleted") + cmdRestore := NewCreateRestoreCmd(tf, streams) + Expect(cmdRestore != nil).To(BeTrue()) + _ = cmdRestore.Flags().Set("restore-to-time", util.TimeFormatWithDuration(&now, time.Second)) + _ = cmdRestore.Flags().Set("source-cluster", clusterName) + cmdRestore.Run(nil, []string{}) + }) }) -func mockBackupInfo(dynamic dynamic.Interface, backupName, clusterName string) { +func mockBackupInfo(dynamic dynamic.Interface, backupName, clusterName string, manifests map[string]any) { clusterString := fmt.Sprintf(`{"metadata":{"name":"deleted-cluster","namespace":"%s"},"spec":{"clusterDefinitionRef":"apecloud-mysql","clusterVersionRef":"ac-mysql-8.0.30","componentSpecs":[{"name":"mysql","componentDefRef":"mysql","replicas":1}]}}`, testing.Namespace) backupStatus := &unstructured.Unstructured{ - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "phase": "Completed", + Object: map[string]any{ + "status": map[string]any{ + "phase": "Completed", + "manifests": manifests, }, - "metadata": map[string]interface{}{ + "metadata": map[string]any{ "name": backupName, - "annotations": map[string]interface{}{ + "annotations": map[string]any{ constant.ClusterSnapshotAnnotationKey: clusterString, }, - "labels": map[string]interface{}{ + "labels": map[string]any{ constant.AppInstanceLabelKey: clusterName, constant.KBAppComponentLabelKey: "test", }, diff --git a/internal/cli/cmd/cluster/delete.go b/internal/cli/cmd/cluster/delete.go index 2ffb250ac..77347dd33 100644 --- a/internal/cli/cmd/cluster/delete.go +++ b/internal/cli/cmd/cluster/delete.go @@ -1,28 +1,36 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( + "context" "fmt" "github.com/spf13/cobra" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/errors" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" @@ -41,6 +49,7 @@ var deleteExample = templates.Examples(` func NewDeleteCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := delete.NewDeleteOptions(f, streams, types.ClusterGVR()) o.PreDeleteHook = clusterPreDeleteHook + o.PostDeleteHook = clusterPostDeleteHook cmd := &cobra.Command{ Use: "delete NAME", @@ -63,14 +72,13 @@ func deleteCluster(o *delete.DeleteOptions, args []string) error { return o.Run() } -func clusterPreDeleteHook(object runtime.Object) error { - if object.GetObjectKind().GroupVersionKind().Kind != appsv1alpha1.ClusterKind { - klog.V(1).Infof("object %s is not of kind %s, skip PreDeleteHook.", object.GetObjectKind().GroupVersionKind().Kind, appsv1alpha1.ClusterKind) +func clusterPreDeleteHook(o *delete.DeleteOptions, object runtime.Object) error { + if object == nil { return nil } - unstructed := object.(*unstructured.Unstructured) - cluster := &appsv1alpha1.Cluster{} - if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructed.Object, cluster); err != nil { + + cluster, err := getClusterFromObject(object) + if err != nil { return err } if cluster.Spec.TerminationPolicy == appsv1alpha1.DoNotTerminate { @@ -78,3 +86,77 @@ func clusterPreDeleteHook(object runtime.Object) error { } return nil } + +func clusterPostDeleteHook(o *delete.DeleteOptions, object runtime.Object) error { + if object == nil { + return nil + } + + c, err := getClusterFromObject(object) + if err != nil { + return err + } + + client, err := o.Factory.KubernetesClientSet() + if err != nil { + return err + } + + // HACK: for a postgresql cluster, we need to delete the sa, role and rolebinding + if err = deleteDependencies(client, c.Namespace, c.Name); err != nil { + return err + } + return nil +} + +func deleteDependencies(client kubernetes.Interface, ns string, name string) error { + klog.V(1).Infof("delete dependencies for cluster %s", name) + var ( + saName = saNamePrefix + name + roleName = roleNamePrefix + name + roleBindingName = roleBindingNamePrefix + name + allErr []error + ) + + // now, delete the dependencies, for postgresql, we delete sa, role and rolebinding + ctx := context.TODO() + gracePeriod := int64(0) + deleteOptions := metav1.DeleteOptions{GracePeriodSeconds: &gracePeriod} + checkErr := func(err error) bool { + if err != nil && !apierrors.IsNotFound(err) { + return true + } + return false + } + + // delete rolebinding + klog.V(1).Infof("delete rolebinding %s", roleBindingName) + if err := client.RbacV1().RoleBindings(ns).Delete(ctx, roleBindingName, deleteOptions); checkErr(err) { + allErr = append(allErr, err) + } + + // delete service account + klog.V(1).Infof("delete service account %s", saName) + if err := client.CoreV1().ServiceAccounts(ns).Delete(ctx, saName, deleteOptions); checkErr(err) { + allErr = append(allErr, err) + } + + // delete role + klog.V(1).Infof("delete role %s", roleName) + if err := client.RbacV1().Roles(ns).Delete(ctx, roleName, deleteOptions); checkErr(err) { + allErr = append(allErr, err) + } + return errors.NewAggregate(allErr) +} + +func getClusterFromObject(object runtime.Object) (*appsv1alpha1.Cluster, error) { + if object.GetObjectKind().GroupVersionKind().Kind != appsv1alpha1.ClusterKind { + return nil, fmt.Errorf("object %s is not of kind %s", object.GetObjectKind().GroupVersionKind().Kind, appsv1alpha1.ClusterKind) + } + u := object.(*unstructured.Unstructured) + cluster := &appsv1alpha1.Cluster{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, cluster); err != nil { + return nil, err + } + return cluster, nil +} diff --git a/internal/cli/cmd/cluster/delete_ops.go b/internal/cli/cmd/cluster/delete_ops.go index ae22df2a5..faca11b20 100644 --- a/internal/cli/cmd/cluster/delete_ops.go +++ b/internal/cli/cmd/cluster/delete_ops.go @@ -1,28 +1,39 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( + "context" "fmt" + jsonpatch "github.com/evanphx/json-patch" "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/delete" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" @@ -30,6 +41,7 @@ import ( func NewDeleteOpsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { o := delete.NewDeleteOptions(f, streams, types.OpsGVR()) + o.PreDeleteHook = preDeleteOps cmd := &cobra.Command{ Use: "delete-ops", Short: "Delete an OpsRequest.", @@ -44,9 +56,45 @@ func NewDeleteOpsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *co return cmd } -// completeForDeleteOps complete cmd for delete OpsRequest, if resource name +func preDeleteOps(o *delete.DeleteOptions, obj runtime.Object) error { + unstructured := obj.(*unstructured.Unstructured) + opsRequest := &appsv1alpha1.OpsRequest{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(unstructured.Object, opsRequest); err != nil { + return err + } + if opsRequest.Status.Phase != appsv1alpha1.OpsRunningPhase { + return nil + } + if !o.Force { + return fmt.Errorf(`OpsRequest "%s" is Running, you can specify "--force" to delete it`, opsRequest.Name) + } + // remove the finalizers + dynamic, err := o.Factory.DynamicClient() + if err != nil { + return err + } + oldOps := opsRequest.DeepCopy() + opsRequest.Finalizers = []string{} + oldData, err := json.Marshal(oldOps) + if err != nil { + return err + } + newData, err := json.Marshal(opsRequest) + if err != nil { + return err + } + patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) + if err != nil { + return err + } + _, err = dynamic.Resource(types.OpsGVR()).Namespace(opsRequest.Namespace).Patch(context.TODO(), + opsRequest.Name, apitypes.MergePatchType, patchBytes, metav1.PatchOptions{}) + return err +} + +// completeForDeleteOps completes cmd for delete OpsRequest, if resource name // is not specified, construct a label selector based on the cluster name to -// delete all OpeRequest belonging to the cluster. +// delete all OpeRequests belonging to the cluster. func completeForDeleteOps(o *delete.DeleteOptions, args []string) error { // If resource name is not empty, delete these resources by name, do not need // to construct the label selector. @@ -64,7 +112,7 @@ func completeForDeleteOps(o *delete.DeleteOptions, args []string) error { } o.ConfirmedNames = args - // If no specify OpsRequest name and cluster name is specified, delete all OpsRequest belonging to the cluster + // If OpsRequest name is unset and cluster name is set, delete all OpsRequests belonging to the cluster o.LabelSelector = util.BuildLabelSelectorByNames(o.LabelSelector, args) return nil } diff --git a/internal/cli/cmd/cluster/delete_ops_test.go b/internal/cli/cmd/cluster/delete_ops_test.go new file mode 100644 index 000000000..6d8f07947 --- /dev/null +++ b/internal/cli/cmd/cluster/delete_ops_test.go @@ -0,0 +1,144 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package cluster + +import ( + "bytes" + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/delete" + clitesting "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var _ = Describe("Expose", func() { + const ( + namespace = "test" + opsName = "test-ops" + ) + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + in *bytes.Buffer + ) + generateOpsObject := func(opsName string, phase appsv1alpha1.OpsPhase) *appsv1alpha1.OpsRequest { + return &appsv1alpha1.OpsRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: opsName, + Namespace: namespace, + }, + Spec: appsv1alpha1.OpsRequestSpec{ + ClusterRef: "test-cluster", + Type: "Restart", + }, + Status: appsv1alpha1.OpsRequestStatus{ + Phase: phase, + }, + } + } + BeforeEach(func() { + streams, in, _, _ = genericclioptions.NewTestIOStreams() + tf = clitesting.NewTestFactory(namespace) + }) + + AfterEach(func() { + tf.Cleanup() + }) + + initClient := func(opsRequest runtime.Object) { + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + httpResp := func(obj runtime.Object) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} + } + + tf.UnstructuredClient = &clientfake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + return httpResp(opsRequest), nil + }), + } + + tf.FakeDynamicClient = clitesting.FakeDynamicClient(opsRequest) + tf.Client = tf.UnstructuredClient + } + + It("test completeForDeleteOps function", func() { + clusterName := "wesql" + args := []string{clusterName} + clusterLabel := util.BuildLabelSelectorByNames("", args) + testLabel := "kubeblocks.io/test=test" + + By("test delete OpsRequest with cluster") + o := delete.NewDeleteOptions(tf, streams, types.OpsGVR()) + Expect(completeForDeleteOps(o, args)).Should(Succeed()) + Expect(o.LabelSelector == clusterLabel).Should(BeTrue()) + + By("test delete OpsRequest with cluster and custom label") + o.LabelSelector = testLabel + Expect(completeForDeleteOps(o, args)).Should(Succeed()) + Expect(o.LabelSelector == testLabel+","+clusterLabel).Should(BeTrue()) + + By("test delete OpsRequest with name") + o.Names = []string{"test1"} + Expect(completeForDeleteOps(o, nil)).Should(Succeed()) + Expect(len(o.ConfirmedNames)).Should(Equal(1)) + }) + + It("Testing the deletion of running OpsRequest", func() { + By("init opsRequests and k8s client") + runningOps := generateOpsObject(opsName, appsv1alpha1.OpsRunningPhase) + initClient(runningOps) + + By("expect error when deleting running opsRequest") + o := delete.NewDeleteOptions(tf, streams, types.OpsGVR()) + o.PreDeleteHook = preDeleteOps + o.Names = []string{runningOps.Name} + in.Write([]byte(runningOps.Name + "\n")) + err := o.Run() + Expect(err).ShouldNot(BeNil()) + Expect(err.Error()).Should(Equal(fmt.Sprintf(`OpsRequest "%s" is Running, you can specify "--force" to delete it`, runningOps.Name))) + + By("expect success when deleting running opsRequest with --force") + o.GracePeriod = 0 + o.Names = []string{runningOps.Name} + in.Write([]byte(runningOps.Name + "\n")) + o.Force = true + err = o.Run() + Expect(err).Should(BeNil()) + }) +}) diff --git a/internal/cli/cmd/cluster/describe.go b/internal/cli/cmd/cluster/describe.go index 80669a7ab..8f4e1e3bd 100644 --- a/internal/cli/cmd/cluster/describe.go +++ b/internal/cli/cmd/cluster/describe.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -20,9 +23,11 @@ import ( "fmt" "io" "strings" + "time" "github.com/spf13/cobra" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" @@ -31,6 +36,7 @@ import ( "k8s.io/kubectl/pkg/util/templates" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" @@ -126,11 +132,11 @@ func (o *describeOptions) describeCluster(name string) error { Name: name, Namespace: o.namespace, GetOptions: cluster.GetOptions{ - WithClusterDef: true, - WithService: true, - WithPod: true, - WithEvent: true, - WithPVC: true, + WithClusterDef: true, + WithService: true, + WithPod: true, + WithPVC: true, + WithDataProtection: true, }, } @@ -155,8 +161,11 @@ func (o *describeOptions) describeCluster(name string) error { // images showImages(comps, o.Out) + // data protection info + showDataProtection(o.BackupPolicies, o.Backups, o.Out) + // events - showEvents(o.Events, o.Cluster.Name, o.Cluster.Namespace, o.Out) + showEvents(o.Cluster.Name, o.Cluster.Namespace, o.Out) fmt.Fprintln(o.Out) return nil @@ -196,27 +205,9 @@ func showImages(comps []*cluster.ComponentInfo, out io.Writer) { tbl.Print() } -func showEvents(events *corev1.EventList, name string, namespace string, out io.Writer) { - objs := util.SortEventsByLastTimestamp(events, corev1.EventTypeWarning) - - // print last 5 events - title := fmt.Sprintf("\nEvents(last 5 warnings, see more:kbcli cluster list-events -n %s %s):", namespace, name) - tbl := newTbl(out, title, "TIME", "TYPE", "REASON", "OBJECT", "MESSAGE") - cnt := 0 - for _, o := range *objs { - e := o.(*corev1.Event) - // do not output KubeBlocks probe events - if e.InvolvedObject.FieldPath == constant.ProbeCheckRolePath { - continue - } - - tbl.AddRow(util.GetEventTimeStr(e), e.Type, e.Reason, util.GetEventObject(e), e.Message) - cnt++ - if cnt == 5 { - break - } - } - tbl.Print() +func showEvents(name string, namespace string, out io.Writer) { + // hint user how to get events + fmt.Fprintf(out, "\nShow cluster events: kbcli cluster list-events -n %s %s", namespace, name) } func showEndpoints(c *appsv1alpha1.Cluster, svcList *corev1.ServiceList, out io.Writer) { @@ -235,3 +226,67 @@ func showEndpoints(c *appsv1alpha1.Cluster, svcList *corev1.ServiceList, out io. } tbl.Print() } + +func showDataProtection(backupPolicies []dpv1alpha1.BackupPolicy, backups []dpv1alpha1.Backup, out io.Writer) { + if len(backupPolicies) == 0 { + return + } + tbl := newTbl(out, "\nData Protection:", "AUTO-BACKUP", "BACKUP-SCHEDULE", "TYPE", "BACKUP-TTL", "LAST-SCHEDULE", "RECOVERABLE-TIME") + for _, policy := range backupPolicies { + if policy.Annotations[constant.DefaultBackupPolicyAnnotationKey] != "true" { + continue + } + if policy.Status.Phase != dpv1alpha1.PolicyAvailable { + continue + } + ttlString := printer.NoneString + backupSchedule := printer.NoneString + backupType := printer.NoneString + scheduleEnable := "Disabled" + if policy.Spec.Schedule.Snapshot != nil { + if policy.Spec.Schedule.Snapshot.Enable { + scheduleEnable = "Enabled" + backupSchedule = policy.Spec.Schedule.Snapshot.CronExpression + backupType = string(dpv1alpha1.BackupTypeSnapshot) + } + } + if policy.Spec.Schedule.Datafile != nil { + if policy.Spec.Schedule.Datafile.Enable { + scheduleEnable = "Enabled" + backupSchedule = policy.Spec.Schedule.Datafile.CronExpression + backupType = string(dpv1alpha1.BackupTypeDataFile) + } + } + if policy.Spec.Retention != nil && policy.Spec.Retention.TTL != nil { + ttlString = *policy.Spec.Retention.TTL + } + lastScheduleTime := printer.NoneString + if policy.Status.LastScheduleTime != nil { + lastScheduleTime = util.TimeFormat(policy.Status.LastScheduleTime) + } + tbl.AddRow(scheduleEnable, backupSchedule, backupType, ttlString, lastScheduleTime, getBackupRecoverableTime(backups)) + } + tbl.Print() +} + +// getBackupRecoverableTime returns the recoverable time range string +func getBackupRecoverableTime(backups []dpv1alpha1.Backup) string { + recoverabelTime := dpv1alpha1.GetRecoverableTimeRange(backups) + var result string + for _, i := range recoverabelTime { + result = addTimeRange(result, i.StartTime, i.StopTime) + } + if result == "" { + return printer.NoneString + } + return result +} + +func addTimeRange(result string, start, end *metav1.Time) string { + if result != "" { + result += ", " + } + result += fmt.Sprintf("%s ~ %s", util.TimeFormatWithDuration(start, time.Second), + util.TimeFormatWithDuration(end, time.Second)) + return result +} diff --git a/internal/cli/cmd/cluster/describe_ops.go b/internal/cli/cmd/cluster/describe_ops.go index 4f03a3a64..9dca04c30 100644 --- a/internal/cli/cmd/cluster/describe_ops.go +++ b/internal/cli/cmd/cluster/describe_ops.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -231,7 +234,7 @@ func (o *describeOpsOptions) getRestartCommand(spec appsv1alpha1.OpsRequestSpec) componentNames[i] = v.ComponentName } return []string{ - fmt.Sprintf("kbcli cluster restart %s --component-names=%s", spec.ClusterRef, + fmt.Sprintf("kbcli cluster restart %s --components=%s", spec.ClusterRef, strings.Join(componentNames, ",")), } } @@ -267,13 +270,16 @@ func (o *describeOpsOptions) getVerticalScalingCommand(spec appsv1alpha1.OpsRequ spec.VerticalScalingList, convertObject, getCompName) commands := make([]string, len(componentNameSlice)) for i := range componentNameSlice { - resource := resourceSlice[i].(corev1.ResourceRequirements) - commands[i] = fmt.Sprintf("kbcli cluster vertical-scale %s --component-names=%s", + commands[i] = fmt.Sprintf("kbcli cluster vscale %s --components=%s", spec.ClusterRef, strings.Join(componentNameSlice[i], ",")) - commands[i] += o.addResourceFlag("requests.cpu", resource.Requests.Cpu()) - commands[i] += o.addResourceFlag("requests.memory", resource.Requests.Memory()) - commands[i] += o.addResourceFlag("limits.cpu", resource.Limits.Cpu()) - commands[i] += o.addResourceFlag("limits.memory", resource.Limits.Memory()) + class := spec.VerticalScalingList[i].Class + if class != "" { + commands[i] += fmt.Sprintf("--class=%s", class) + } else { + resource := resourceSlice[i].(corev1.ResourceRequirements) + commands[i] += o.addResourceFlag("cpu", resource.Limits.Cpu()) + commands[i] += o.addResourceFlag("memory", resource.Limits.Memory()) + } } return commands } @@ -293,7 +299,7 @@ func (o *describeOpsOptions) getHorizontalScalingCommand(spec appsv1alpha1.OpsRe spec.HorizontalScalingList, convertObject, getCompName) commands := make([]string, len(componentNameSlice)) for i := range componentNameSlice { - commands[i] = fmt.Sprintf("kbcli cluster horizontal-scale %s --component-names=%s --replicas=%d", + commands[i] = fmt.Sprintf("kbcli cluster hscale %s --components=%s --replicas=%d", spec.ClusterRef, strings.Join(componentNameSlice[i], ","), replicasSlice[i].(int32)) } return commands @@ -313,7 +319,7 @@ func (o *describeOpsOptions) getVolumeExpansionCommand(spec appsv1alpha1.OpsRequ v.VolumeClaimTemplates, convertObject, getVCTName) for i := range vctNameSlice { storage := storageSlice[i].(resource.Quantity) - commands = append(commands, fmt.Sprintf("kbcli cluster volume-expand %s --component-names=%s --volume-claim-template-names=%s --storage=%s", + commands = append(commands, fmt.Sprintf("kbcli cluster volume-expand %s --components=%s --volume-claim-template-names=%s --storage=%s", spec.ClusterRef, v.ComponentName, strings.Join(vctNameSlice[i], ","), storage.String())) } } @@ -341,7 +347,7 @@ func (o *describeOpsOptions) getReconfiguringCommand(spec appsv1alpha1.OpsReques commandArgs = append(commandArgs, "cluster") commandArgs = append(commandArgs, "configure") commandArgs = append(commandArgs, spec.ClusterRef) - commandArgs = append(commandArgs, fmt.Sprintf("--component-names=%s", componentName)) + commandArgs = append(commandArgs, fmt.Sprintf("--components=%s", componentName)) commandArgs = append(commandArgs, fmt.Sprintf("--config-spec=%s", configuration.Name)) config := configuration.Keys[0] @@ -406,7 +412,7 @@ func (o *describeOpsOptions) printLastConfiguration(configuration appsv1alpha1.L } } -// printLastConfigurationByOpsType the entry function for printing last configuration by ops type. +// printLastConfigurationByOpsType prints the last configuration by ops type. func (o *describeOpsOptions) printLastConfigurationByOpsType(configuration appsv1alpha1.LastConfiguration, headers []interface{}, handleOpsObject func(tbl *printer.TablePrinter, cName string, compConf appsv1alpha1.LastComponentConfiguration), diff --git a/internal/cli/cmd/cluster/describe_ops_test.go b/internal/cli/cmd/cluster/describe_ops_test.go index d5e927065..cbbb710bd 100644 --- a/internal/cli/cmd/cluster/describe_ops_test.go +++ b/internal/cli/cmd/cluster/describe_ops_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cmd/cluster/describe_test.go b/internal/cli/cmd/cluster/describe_test.go index 95cc3dacd..584d0b458 100644 --- a/internal/cli/cmd/cluster/describe_test.go +++ b/internal/cli/cmd/cluster/describe_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -20,10 +23,12 @@ import ( "bytes" "net/http" "strings" + "time" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -32,6 +37,7 @@ import ( clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" ) @@ -103,12 +109,42 @@ var _ = Describe("Expose", func() { It("showEvents", func() { out := &bytes.Buffer{} - showEvents(testing.FakeEvents(), "test-cluster", namespace, out) + showEvents("test-cluster", namespace, out) + Expect(out.String()).ShouldNot(BeEmpty()) + }) + + It("showDataProtections", func() { + out := &bytes.Buffer{} + fakeBackupPolicies := []dpv1alpha1.BackupPolicy{ + *testing.FakeBackupPolicy("backup-policy-test", "test-cluster"), + } + fakeBackups := []dpv1alpha1.Backup{ + *testing.FakeBackup("backup-test"), + *testing.FakeBackup("backup-test2"), + } + now := metav1.Now() + fakeBackups[0].Status = dpv1alpha1.BackupStatus{ + Phase: dpv1alpha1.BackupCompleted, + Manifests: &dpv1alpha1.ManifestsStatus{ + BackupLog: &dpv1alpha1.BackupLogStatus{ + StartTime: &now, + StopTime: &now, + }, + }, + } + after := metav1.Time{Time: now.Add(time.Hour)} + fakeBackups[1].Status = dpv1alpha1.BackupStatus{ + Phase: dpv1alpha1.BackupCompleted, + Manifests: &dpv1alpha1.ManifestsStatus{ + BackupLog: &dpv1alpha1.BackupLogStatus{ + StartTime: &now, + StopTime: &after, + }, + }, + } + showDataProtection(fakeBackupPolicies, fakeBackups, out) strs := strings.Split(out.String(), "\n") - // sorted - firstEvent := strs[3] - secondEvent := strs[4] - Expect(strings.Compare(firstEvent, secondEvent) < 0).Should(BeTrue()) + Expect(strs).ShouldNot(BeEmpty()) }) }) diff --git a/internal/cli/cmd/cluster/errors.go b/internal/cli/cmd/cluster/errors.go index 2e782da39..200ddf4f5 100644 --- a/internal/cli/cmd/cluster/errors.go +++ b/internal/cli/cmd/cluster/errors.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -22,25 +25,25 @@ import ( ) var ( - clusterNotExistErrMessage = "cluster[name=%s] is not exist. Please check that is spelled correctly." - componentNotExistErrMessage = "cluster[name=%s] does not has this component[name=%s]. Please check that --component-name is spelled correctly." + clusterNotExistErrMessage = "cluster[name=%s] does not exist. Please check that is spelled correctly." + componentNotExistErrMessage = "cluster[name=%s] does not have component[name=%s]. Please check that --component is spelled correctly." missingClusterArgErrMassage = "cluster name should be specified, using --help." missingUpdatedParametersErrMessage = "missing updated parameters, using --help." - multiComponentsErrorMessage = "when multi component exist, must specify which component to use. Please using --component-name" - multiConfigTemplateErrorMessage = "when multi config template exist, must specify which config template to use. Please using --config-spec" - multiConfigFileErrorMessage = "when multi config files exist, must specify which config file to update. Please using --config-file" + multiComponentsErrorMessage = "when multi components exist, specify a component, using --component" + multiConfigTemplateErrorMessage = "when multi config templates exist, specify a config template, using --config-spec" + multiConfigFileErrorMessage = "when multi config files exist, specify a config file, using --config-file" - notFoundValidConfigTemplateErrorMessage = "not find valid config template, component[name=%s] in the cluster[name=%s]" + notFoundValidConfigTemplateErrorMessage = "cannot find valid config templates for component[name=%s] in the cluster[name=%s]" - notFoundConfigSpecErrorMessage = "not find config spec[%s], component[name=%s] in the cluster[name=%s]" + notFoundConfigSpecErrorMessage = "cannot find config spec[%s] for component[name=%s] in the cluster[name=%s]" - notFoundConfigFileErrorMessage = "not find config file, file[name=%s] in the configspec[name=%s], all configfiles: %v" - notSupportFileUpdateErrorMessage = "not support file[%s] update, current support files: %v" + notFoundConfigFileErrorMessage = "cannot find config file[name=%s] in the configspec[name=%s], all configfiles: %v" + notSupportFileUpdateErrorMessage = "not supported file[%s] for updating, current supported files: %v" - notCueSchemaPrompt = "The config template not define cue schema and parameter explain info cannot be generated." - cue2openAPISchemaFailedPrompt = "The cue schema may not satisfy the conversion constraints of openAPISchema and parameter explain info cannot be generated." - restartConfirmPrompt = "The parameter change you modified needs to be restarted, which may cause the cluster to be unavailable for a period of time. Do you need to continue...\n, " + notCueSchemaPrompt = "The config template is not defined in cue schema and parameter explanation info cannot be generated." + cue2openAPISchemaFailedPrompt = "The cue schema may not satisfy the conversion constraints of openAPISchema and parameter explanation info cannot be generated." + restartConfirmPrompt = "The parameter change incurs a cluster restart, which brings the cluster down for a while. Enter to continue...\n, " confirmApplyReconfigurePrompt = "Are you sure you want to apply these changes?\n" ) diff --git a/internal/cli/cmd/cluster/label.go b/internal/cli/cmd/cluster/label.go new file mode 100644 index 000000000..432536ad5 --- /dev/null +++ b/internal/cli/cmd/cluster/label.go @@ -0,0 +1,343 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package cluster + +import ( + "encoding/json" + "fmt" + "strings" + + jsonpatch "github.com/evanphx/json-patch" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + ktypes "k8s.io/apimachinery/pkg/types" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/cluster" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + labelExample = templates.Examples(` + # list label for clusters with specified name + kbcli cluster label mycluster --list + + # add label 'env' and value 'dev' for clusters with specified name + kbcli cluster label mycluster env=dev + + # add label 'env' and value 'dev' for all clusters + kbcli cluster label env=dev --all + + # add label 'env' and value 'dev' for the clusters that match the selector + kbcli cluster label env=dev -l type=mysql + + # update cluster with the label 'env' with value 'test', overwriting any existing value + kbcli cluster label mycluster --overwrite env=test + + # delete label env for clusters with specified name + kbcli cluster label mycluster env-`) +) + +type LabelOptions struct { + Factory cmdutil.Factory + GVR schema.GroupVersionResource + + // Common user flags + overwrite bool + all bool + list bool + selector string + + // results of arg parsing + resources []string + newLabels map[string]string + removeLabels []string + + namespace string + enforceNamespace bool + dryRunStrategy cmdutil.DryRunStrategy + dryRunVerifier *resource.QueryParamVerifier + builder *resource.Builder + unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) + + genericclioptions.IOStreams +} + +func NewLabelOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, gvr schema.GroupVersionResource) *LabelOptions { + return &LabelOptions{ + Factory: f, + GVR: gvr, + IOStreams: streams, + } +} + +func NewLabelCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewLabelOptions(f, streams, types.ClusterGVR()) + cmd := &cobra.Command{ + Use: "label NAME", + Short: "Update the labels on cluster", + Example: labelExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.complete(cmd, args)) + util.CheckErr(o.validate()) + util.CheckErr(o.run()) + }, + } + + cmd.Flags().BoolVar(&o.overwrite, "overwrite", o.overwrite, "If true, allow labels to be overwritten, otherwise reject label updates that overwrite existing labels.") + cmd.Flags().BoolVar(&o.all, "all", o.all, "Select all cluster") + cmd.Flags().BoolVar(&o.list, "list", o.list, "If true, display the labels of the clusters") + cmdutil.AddDryRunFlag(cmd) + cmdutil.AddLabelSelectorFlagVar(cmd, &o.selector) + + return cmd +} + +func (o *LabelOptions) complete(cmd *cobra.Command, args []string) error { + var err error + + o.dryRunStrategy, err = cmdutil.GetDryRunStrategy(cmd) + if err != nil { + return err + } + + // parse resources and labels + resources, labelArgs, err := cmdutil.GetResourcesAndPairs(args, "label") + if err != nil { + return err + } + o.resources = resources + o.newLabels, o.removeLabels, err = parseLabels(labelArgs) + if err != nil { + return err + } + + o.namespace, o.enforceNamespace, err = o.Factory.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + o.builder = o.Factory.NewBuilder() + o.unstructuredClientForMapping = o.Factory.UnstructuredClientForMapping + dynamicClient, err := o.Factory.DynamicClient() + if err != nil { + return err + } + o.dryRunVerifier = resource.NewQueryParamVerifier(dynamicClient, o.Factory.OpenAPIGetter(), resource.QueryParamDryRun) + return nil +} + +func (o *LabelOptions) validate() error { + if o.all && len(o.selector) > 0 { + return fmt.Errorf("cannot set --all and --selector at the same time") + } + + if !o.all && len(o.selector) == 0 && len(o.resources) == 0 { + return fmt.Errorf("at least one cluster is required") + } + + if len(o.newLabels) < 1 && len(o.removeLabels) < 1 && !o.list { + return fmt.Errorf("at least one label update is required") + } + return nil +} + +func (o *LabelOptions) run() error { + r := o.builder. + Unstructured(). + NamespaceParam(o.namespace).DefaultNamespace(). + LabelSelector(o.selector). + ResourceTypeOrNameArgs(o.all, append([]string{util.GVRToString(o.GVR)}, o.resources...)...). + ContinueOnError(). + Latest(). + Flatten(). + Do() + + if err := r.Err(); err != nil { + return err + } + + infos, err := r.Infos() + if err != nil { + return err + } + + if len(infos) == 0 { + return fmt.Errorf("no clusters found") + } + + for _, info := range infos { + obj := info.Object + oldData, err := json.Marshal(obj) + if err != nil { + return err + } + + if o.dryRunStrategy == cmdutil.DryRunClient || o.list { + err = labelFunc(obj, o.overwrite, o.newLabels, o.removeLabels) + if err != nil { + return err + } + } else { + name, namespace := info.Name, info.Namespace + if err != nil { + return err + } + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + for _, label := range o.removeLabels { + if _, ok := accessor.GetLabels()[label]; !ok { + fmt.Fprintf(o.Out, "label %q not found.\n", label) + } + } + + if err := labelFunc(obj, o.overwrite, o.newLabels, o.removeLabels); err != nil { + return err + } + + newObj, err := json.Marshal(obj) + if err != nil { + return err + } + patchBytes, err := jsonpatch.CreateMergePatch(oldData, newObj) + createPatch := err == nil + mapping := info.ResourceMapping() + client, err := o.unstructuredClientForMapping(mapping) + if err != nil { + return err + } + helper := resource.NewHelper(client, mapping). + DryRun(o.dryRunStrategy == cmdutil.DryRunServer) + if createPatch { + _, err = helper.Patch(namespace, name, ktypes.MergePatchType, patchBytes, nil) + } else { + _, err = helper.Replace(namespace, name, false, obj) + } + if err != nil { + return err + } + } + } + + if o.list { + dynamic, err := o.Factory.DynamicClient() + if err != nil { + return err + } + + client, err := o.Factory.KubernetesClientSet() + if err != nil { + return err + } + + opt := &cluster.PrinterOptions{ + ShowLabels: true, + } + + p := cluster.NewPrinter(o.IOStreams.Out, cluster.PrintLabels, opt) + for _, info := range infos { + if err = addRow(dynamic, client, info.Namespace, info.Name, p); err != nil { + return err + } + } + p.Print() + } + + return nil +} + +func parseLabels(spec []string) (map[string]string, []string, error) { + labels := map[string]string{} + var remove []string + for _, labelSpec := range spec { + switch { + case strings.Contains(labelSpec, "="): + parts := strings.Split(labelSpec, "=") + if len(parts) != 2 { + return nil, nil, fmt.Errorf("invalid label spec: %s", labelSpec) + } + labels[parts[0]] = parts[1] + case strings.HasSuffix(labelSpec, "-"): + remove = append(remove, labelSpec[:len(labelSpec)-1]) + default: + return nil, nil, fmt.Errorf("unknown label spec: %s", labelSpec) + } + } + for _, removeLabel := range remove { + if _, found := labels[removeLabel]; found { + return nil, nil, fmt.Errorf("cannot modify and remove label within the same command") + } + } + return labels, remove, nil +} + +func validateNoOverwrites(obj runtime.Object, labels map[string]string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + + objLabels := accessor.GetLabels() + if objLabels == nil { + return nil + } + + for key := range labels { + if _, found := objLabels[key]; found { + return fmt.Errorf("'%s' already has a value (%s), and --overwrite is false", key, objLabels[key]) + } + } + return nil +} + +func labelFunc(obj runtime.Object, overwrite bool, labels map[string]string, remove []string) error { + accessor, err := meta.Accessor(obj) + if err != nil { + return err + } + if !overwrite { + if err := validateNoOverwrites(obj, labels); err != nil { + return err + } + } + + objLabels := accessor.GetLabels() + if objLabels == nil { + objLabels = make(map[string]string) + } + + for key, value := range labels { + objLabels[key] = value + } + for _, label := range remove { + delete(objLabels, label) + } + accessor.SetLabels(objLabels) + + return nil +} diff --git a/internal/cli/cmd/cluster/label_test.go b/internal/cli/cmd/cluster/label_test.go new file mode 100644 index 000000000..a52910b7d --- /dev/null +++ b/internal/cli/cmd/cluster/label_test.go @@ -0,0 +1,84 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package cluster + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/types" +) + +var _ = Describe("cluster label", func() { + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace("default") + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("label command", func() { + cmd := NewLabelCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + + Context("complete", func() { + var o *LabelOptions + var cmd *cobra.Command + var args []string + BeforeEach(func() { + cmd = NewLabelCmd(tf, streams) + o = NewLabelOptions(tf, streams, types.ClusterDefGVR()) + args = []string{"c1", "env=dev"} + }) + + It("args is empty", func() { + Expect(o.complete(cmd, nil)).Should(Succeed()) + Expect(o.validate()).Should(HaveOccurred()) + }) + + It("cannot set --all and --selector at the same time", func() { + o.all = true + o.selector = "status=unhealthy" + Expect(o.complete(cmd, args)).Should(Succeed()) + Expect(o.validate()).Should(HaveOccurred()) + }) + + It("at least one label update is required", func() { + Expect(o.complete(cmd, []string{"c1"})).Should(Succeed()) + Expect(o.validate()).Should(HaveOccurred()) + }) + + It("cannot modify and remove label within the same command", func() { + Expect(o.complete(cmd, []string{"c1", "env=dev", "env-"})).Should(HaveOccurred()) + }) + }) +}) diff --git a/internal/cli/cmd/cluster/list.go b/internal/cli/cmd/cluster/list.go index f005148c6..6e9807e8a 100644 --- a/internal/cli/cmd/cluster/list.go +++ b/internal/cli/cmd/cluster/list.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -38,7 +41,7 @@ var ( # list all clusters kbcli cluster list - # list a single cluster with specified NAME + # list a single cluster with specified name kbcli cluster list mycluster # list a single cluster in YAML output format @@ -106,7 +109,7 @@ func NewListInstancesCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) util.CheckErr(run(o, cluster.PrintInstances)) }, } - cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespace", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") return cmd } @@ -124,7 +127,7 @@ func NewListComponentsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams util.CheckErr(run(o, cluster.PrintComponents)) }, } - cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespace", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") return cmd } @@ -142,7 +145,7 @@ func NewListEventsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *c util.CheckErr(run(o, cluster.PrintEvents)) }, } - cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespace", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") return cmd } diff --git a/internal/cli/cmd/cluster/list_logs.go b/internal/cli/cmd/cluster/list_logs.go index 92c71f7e7..e3273f036 100644 --- a/internal/cli/cmd/cluster/list_logs.go +++ b/internal/cli/cmd/cluster/list_logs.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -143,7 +146,8 @@ func (o *ListLogsOptions) printListLogs(dataObj *cluster.ClusterObjects) error { tbl := printer.NewTablePrinter(o.Out) logFilesData := o.gatherLogFilesData(dataObj.Cluster, dataObj.ClusterDef, dataObj.Pods) if len(logFilesData) == 0 { - fmt.Fprintln(o.ErrOut, "No log files found. \nYou can enable the log feature when creating a cluster with option of \"--enable-all-logs=true\"") + fmt.Fprintf(o.ErrOut, "No log files found. You can enable the log feature with the kbcli command below.\n"+ + "kbcli cluster update %s --enable-all-logs=true --namespace %s\n", dataObj.Cluster.Name, dataObj.Cluster.Namespace) } else { tbl.SetHeader("INSTANCE", "LOG-TYPE", "FILE-PATH", "SIZE", "LAST-WRITTEN", "COMPONENT") for _, f := range logFilesData { @@ -163,7 +167,7 @@ type logFileInfo struct { component string } -// gatherLogFilesData gathers all log files data from every instance of the cluster. +// gatherLogFilesData gathers all log files data from each instance of the cluster. func (o *ListLogsOptions) gatherLogFilesData(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition, pods *corev1.PodList) []logFileInfo { logFileInfoList := make([]logFileInfo, 0, len(pods.Items)) for _, p := range pods.Items { diff --git a/internal/cli/cmd/cluster/list_logs_test.go b/internal/cli/cmd/cluster/list_logs_test.go index 6cdd10b15..888c01b69 100644 --- a/internal/cli/cmd/cluster/list_logs_test.go +++ b/internal/cli/cmd/cluster/list_logs_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -22,6 +25,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime/schema" diff --git a/internal/cli/cmd/cluster/list_ops.go b/internal/cli/cmd/cluster/list_ops.go index 045d5db59..886935eba 100644 --- a/internal/cli/cmd/cluster/list_ops.go +++ b/internal/cli/cmd/cluster/list_ops.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( "context" + "fmt" "sort" "strings" @@ -37,13 +41,17 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/util" ) -var listOpsExample = templates.Examples(` +var ( + listOpsExample = templates.Examples(` # list all opsRequests kbcli cluster list-ops # list all opsRequests of specified cluster kbcli cluster list-ops mycluster`) + defaultDisplayPhase = []string{"pending", "creating", "running", "canceling", "failed"} +) + type opsListOptions struct { *list.ListOptions status []string @@ -73,7 +81,8 @@ func NewListOpsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr } o.AddFlags(cmd) cmd.Flags().StringSliceVar(&o.opsType, "type", nil, "The OpsRequest type") - cmd.Flags().StringSliceVar(&o.status, "status", []string{"running", "pending", "failed"}, "Options include all, pending, running, succeeded, failed. by default, outputs the pending/running/failed OpsRequest.") + cmd.Flags().StringSliceVar(&o.status, "status", defaultDisplayPhase, fmt.Sprintf("Options include all, %s. by default, outputs the %s OpsRequest.", + strings.Join(defaultDisplayPhase, ", "), strings.Join(defaultDisplayPhase, "/"))) cmd.Flags().StringVar(&o.opsRequestName, "name", "", "The OpsRequest name to get the details.") return cmd } @@ -100,7 +109,7 @@ func (o *opsListOptions) printOpsList() error { // sort the unstructured objects with the creationTimestamp in positive order sort.Sort(unstructuredList(opsList.Items)) - // check if specific the "all" keyword for status. + // check if specified with "all" keyword for status. isAllStatus := o.isAllStatus() tblPrinter := printer.NewTablePrinter(o.Out) tblPrinter.SetHeader("NAME", "TYPE", "CLUSTER", "COMPONENT", "STATUS", "PROGRESS", "CREATED-TIME") @@ -117,7 +126,7 @@ func (o *opsListOptions) printOpsList() error { } continue } - // if the OpsRequest phase is not in the expected phases, continue + // if the OpsRequest phase is not expected, continue if !isAllStatus && !o.containsIgnoreCase(o.status, phase) { continue } @@ -133,7 +142,6 @@ func (o *opsListOptions) printOpsList() error { } message := "No opsRequests found" if len(o.opsRequestName) == 0 && !o.isAllStatus() { - // if do not view the ops in all status and do not specify the opsName, add this prompt command. message += ", you can try as follows:\n\tkbcli cluster list-ops --status all" } printer.PrintLine(message) diff --git a/internal/cli/cmd/cluster/list_ops_test.go b/internal/cli/cmd/cluster/list_ops_test.go index 97918f6bd..25867b333 100644 --- a/internal/cli/cmd/cluster/list_ops_test.go +++ b/internal/cli/cmd/cluster/list_ops_test.go @@ -1,18 +1,22 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ + package cluster import ( diff --git a/internal/cli/cmd/cluster/list_test.go b/internal/cli/cmd/cluster/list_test.go index fc8679273..c46310b31 100644 --- a/internal/cli/cmd/cluster/list_test.go +++ b/internal/cli/cmd/cluster/list_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -34,6 +37,7 @@ import ( cmdtesting "k8s.io/kubectl/pkg/cmd/testing" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" ) @@ -49,6 +53,7 @@ var _ = Describe("list", func() { namespace = "test" clusterName = "test" clusterName1 = "test1" + clusterName2 = "test2" verticalScalingReason = "VerticalScaling" ) @@ -58,12 +63,19 @@ var _ = Describe("list", func() { _ = appsv1alpha1.AddToScheme(scheme.Scheme) codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) - cluster := testing.FakeCluster(clusterName, namespace) - clusterWithCondition := testing.FakeCluster(clusterName1, namespace, metav1.Condition{ + cluster := testing.FakeCluster(clusterName, namespace, metav1.Condition{ + Type: appsv1alpha1.ConditionTypeApplyResources, + Status: metav1.ConditionFalse, + Reason: "HorizontalScaleFailed", + }) + clusterWithVerticalScaling := testing.FakeCluster(clusterName1, namespace, metav1.Condition{ Type: appsv1alpha1.ConditionTypeLatestOpsRequestProcessed, Status: metav1.ConditionFalse, Reason: verticalScalingReason, }) + clusterWithVerticalScaling.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase + clusterWithAbnormalPhase := testing.FakeCluster(clusterName2, namespace) + clusterWithAbnormalPhase.Status.Phase = appsv1alpha1.AbnormalClusterPhase pods := testing.FakePods(3, namespace, clusterName) httpResp := func(obj runtime.Object) *http.Response { return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} @@ -77,7 +89,8 @@ var _ = Describe("list", func() { return map[string]*http.Response{ "/namespaces/" + namespace + "/clusters": httpResp(&appsv1alpha1.ClusterList{Items: []appsv1alpha1.Cluster{*cluster}}), "/namespaces/" + namespace + "/clusters/" + clusterName: httpResp(cluster), - "/namespaces/" + namespace + "/clusters/" + clusterName1: httpResp(clusterWithCondition), + "/namespaces/" + namespace + "/clusters/" + clusterName1: httpResp(clusterWithVerticalScaling), + "/namespaces/" + namespace + "/clusters/" + clusterName2: httpResp(clusterWithAbnormalPhase), "/namespaces/" + namespace + "/secrets": httpResp(testing.FakeSecrets(namespace, clusterName)), "/api/v1/nodes/" + testing.NodeName: httpResp(testing.FakeNode()), urlPrefix + "/services": httpResp(&corev1.ServiceList{}), @@ -89,7 +102,7 @@ var _ = Describe("list", func() { } tf.Client = tf.UnstructuredClient - tf.FakeDynamicClient = testing.FakeDynamicClient(cluster, clusterWithCondition, testing.FakeClusterDef(), testing.FakeClusterVersion()) + tf.FakeDynamicClient = testing.FakeDynamicClient(cluster, clusterWithVerticalScaling, clusterWithAbnormalPhase, testing.FakeClusterDef(), testing.FakeClusterVersion()) }) AfterEach(func() { @@ -100,11 +113,11 @@ var _ = Describe("list", func() { cmd := NewListCmd(tf, streams) Expect(cmd).ShouldNot(BeNil()) - cmd.Run(cmd, []string{clusterName}) + cmd.Run(cmd, []string{clusterName, clusterName1, clusterName2}) Expect(out.String()).Should(ContainSubstring(testing.ClusterDefName)) - - cmd.Run(cmd, []string{clusterName1}) Expect(out.String()).Should(ContainSubstring(verticalScalingReason)) + Expect(out.String()).Should(ContainSubstring(cluster.ConditionsError)) + Expect(out.String()).Should(ContainSubstring(string(appsv1alpha1.AbnormalClusterPhase))) }) It("list instances", func() { diff --git a/internal/cli/cmd/cluster/logs.go b/internal/cli/cmd/cluster/logs.go index b897bcfaf..8c452f044 100644 --- a/internal/cli/cmd/cluster/logs.go +++ b/internal/cli/cmd/cluster/logs.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -33,7 +36,6 @@ import ( "k8s.io/kubectl/pkg/polymorphichelpers" "k8s.io/kubectl/pkg/util/templates" - computil "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/exec" "github.com/apecloud/kubeblocks/internal/cli/types" @@ -49,7 +51,7 @@ var ( # Display only the most recent 20 lines from cluster mycluster with default primary instance (stdout) kbcli cluster logs mycluster --tail=20 - # Display stdout info of specific instance my-instance-0 (cluster name comes from annotation app.kubernetes.io/instance) + # Display stdout info of specific instance my-instance-0 (cluster name comes from annotation app.kubernetes.io/instance) kbcli cluster logs --instance my-instance-0 # Return snapshot logs from cluster mycluster with specific instance my-instance-0 (stdout) @@ -110,7 +112,7 @@ func (o *LogsOptions) addFlags(cmd *cobra.Command) { cmd.Flags().StringVarP(&o.PodName, "instance", "i", "", "Instance name.") cmd.Flags().StringVarP(&o.logOptions.Container, "container", "c", "", "Container name.") cmd.Flags().BoolVarP(&o.logOptions.Follow, "follow", "f", false, "Specify if the logs should be streamed.") - cmd.Flags().Int64Var(&o.logOptions.Tail, "tail", -1, "Lines of recent log file to display. Defaults to -1 with showing all log lines.") + cmd.Flags().Int64Var(&o.logOptions.Tail, "tail", -1, "Lines of recent log file to display. Defaults to -1 for showing all log lines.") cmd.Flags().Int64Var(&o.logOptions.LimitBytes, "limit-bytes", 0, "Maximum bytes of logs to return.") cmd.Flags().BoolVar(&o.logOptions.Prefix, "prefix", false, "Prefix each log line with the log source (pod name and container name). Only take effect for stdout&stderr.") cmd.Flags().BoolVar(&o.logOptions.IgnoreLogErrors, "ignore-errors", false, "If watching / following pod logs, allow for any errors that occur to be non-fatal. Only take effect for stdout&stderr.") @@ -119,8 +121,8 @@ func (o *LogsOptions) addFlags(cmd *cobra.Command) { cmd.Flags().DurationVar(&o.logOptions.SinceSeconds, "since", o.logOptions.SinceSeconds, "Only return logs newer than a relative duration like 5s, 2m, or 3h. Defaults to all logs. Only one of since-time / since may be used. Only take effect for stdout&stderr.") cmd.Flags().BoolVarP(&o.logOptions.Previous, "previous", "p", o.logOptions.Previous, "If true, print the logs for the previous instance of the container in a pod if it exists. Only take effect for stdout&stderr.") - cmd.Flags().StringVar(&o.fileType, "file-type", "", "Log-file type. Can see the output info of list-logs cmd. No set file-path and file-type will output stdout/stderr of target container.") - cmd.Flags().StringVar(&o.filePath, "file-path", "", "Log-file path. Specify target file path and have a premium priority. No set file-path and file-type will output stdout/stderr of target container.") + cmd.Flags().StringVar(&o.fileType, "file-type", "", "Log-file type. List them with list-logs cmd. When file-path and file-type are unset, output stdout/stderr of target container.") + cmd.Flags().StringVar(&o.filePath, "file-path", "", "Log-file path. File path has a priority over file-type. When file-path and file-type are unset, output stdout/stderr of target container.") cmd.MarkFlagsMutuallyExclusive("file-path", "file-type") cmd.MarkFlagsMutuallyExclusive("since", "since-time") @@ -142,10 +144,10 @@ func (o *LogsOptions) complete(args []string) error { if len(args) > 0 { o.clusterName = args[0] } - // no set podName and find the default pod of cluster + // podName not set, find the default pod of cluster if len(o.PodName) == 0 { infos := cluster.GetSimpleInstanceInfos(o.Dynamic, o.clusterName, o.Namespace) - if len(infos) == 0 || infos[0].Name == computil.ComponentStatusDefaultPodName { + if len(infos) == 0 || infos[0].Name == constant.ComponentStatusDefaultPodName { return fmt.Errorf("failed to find the default instance, please check cluster status") } // first element is the default instance to connect @@ -169,7 +171,7 @@ func (o *LogsOptions) complete(args []string) error { command = assembleTail(o.logOptions.Follow, o.logOptions.Tail, o.logOptions.LimitBytes) + " " + o.filePath case o.isStdoutForContainer(): { - // no set file-path and file-type, and will output container's stdout & stderr, like kubectl logs + // file-path and file-type are unset, output container's stdout & stderr, like kubectl logs o.logOptions.RESTClientGetter = o.Factory o.logOptions.LogsForObject = polymorphichelpers.LogsForObjectFn o.logOptions.Object = pod diff --git a/internal/cli/cmd/cluster/logs_test.go b/internal/cli/cmd/cluster/logs_test.go index 7835c7723..70126f7fe 100644 --- a/internal/cli/cmd/cluster/logs_test.go +++ b/internal/cli/cmd/cluster/logs_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cmd/cluster/operations.go b/internal/cli/cmd/cluster/operations.go old mode 100644 new mode 100755 index 86305f56d..6f4d9ef95 --- a/internal/cli/cmd/cluster/operations.go +++ b/internal/cli/cmd/cluster/operations.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -21,29 +24,36 @@ import ( "fmt" "strings" + jsonpatch "github.com/evanphx/json-patch" "github.com/spf13/cobra" + "golang.org/x/exp/slices" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + apitypes "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/json" "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/class" "github.com/apecloud/kubeblocks/internal/cli/cluster" "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/delete" "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/constant" ) type OperationsOptions struct { - create.BaseOptions + create.CreateOptions `json:"-"` HasComponentNamesFlag bool `json:"-"` - // RequireConfirm if it is true, the second verification will be performed before creating ops. - RequireConfirm bool `json:"-"` + // autoApprove when set true, skip the double check. + autoApprove bool `json:"-"` ComponentNames []string `json:"componentNames,omitempty"` OpsRequestName string `json:"opsRequestName"` TTLSecondsAfterSucceed int `json:"ttlSecondsAfterSucceed"` @@ -81,27 +91,50 @@ type OperationsOptions struct { Services []appsv1alpha1.ClusterComponentService `json:"services,omitempty"` } -func newBaseOperationsOptions(streams genericclioptions.IOStreams, opsType appsv1alpha1.OpsType, hasComponentNamesFlag bool) *OperationsOptions { - return &OperationsOptions{ +func newBaseOperationsOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, + opsType appsv1alpha1.OpsType, hasComponentNamesFlag bool) *OperationsOptions { + customOutPut := func(opt *create.CreateOptions) { + output := fmt.Sprintf("OpsRequest %s created successfully, you can view the progress:", opt.Name) + printer.PrintLine(output) + nextLine := fmt.Sprintf("\tkbcli cluster describe-ops %s -n %s", opt.Name, opt.Namespace) + printer.PrintLine(nextLine) + } + + o := &OperationsOptions{ // nil cannot be set to a map struct in CueLang, so init the map of KeyValues. KeyValues: map[string]string{}, - BaseOptions: create.BaseOptions{IOStreams: streams}, OpsType: opsType, HasComponentNamesFlag: hasComponentNamesFlag, - RequireConfirm: true, + autoApprove: false, + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: "cluster_operations_template.cue", + GVR: types.OpsGVR(), + CustomOutPut: customOutPut, + }, } + + o.OpsTypeLower = strings.ToLower(string(o.OpsType)) + o.CreateOptions.Options = o + return o } -// buildCommonFlags build common flags for operations command -func (o *OperationsOptions) buildCommonFlags(cmd *cobra.Command) { +// addCommonFlags adds common flags for operations command +func (o *OperationsOptions) addCommonFlags(cmd *cobra.Command) { + // add print flags + printer.AddOutputFlagForCreate(cmd, &o.Format) + cmd.Flags().StringVar(&o.OpsRequestName, "name", "", "OpsRequest name. if not specified, it will be randomly generated ") cmd.Flags().IntVar(&o.TTLSecondsAfterSucceed, "ttlSecondsAfterSucceed", 0, "Time to live after the OpsRequest succeed") + cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`) + cmd.Flags().Lookup("dry-run").NoOptDefVal = "unchanged" if o.HasComponentNamesFlag { - cmd.Flags().StringSliceVar(&o.ComponentNames, "components", nil, " Component names to this operations") + cmd.Flags().StringSliceVar(&o.ComponentNames, "components", nil, "Component names to this operations") } } -// CompleteRestartOps when restart a cluster and components is null, it means restarting all components of the cluster. +// CompleteRestartOps restarts all components of the cluster // we should set all component names to ComponentNames flag. func (o *OperationsOptions) CompleteRestartOps() error { if o.Name == "" { @@ -122,7 +155,7 @@ func (o *OperationsOptions) CompleteRestartOps() error { return nil } -// CompleteComponentsFlag when components flag is null and the cluster only has one component, should auto complete it. +// CompleteComponentsFlag when components flag is null and the cluster only has one component, auto complete it. func (o *OperationsOptions) CompleteComponentsFlag() error { if o.Name == "" { return makeMissingClusterNameErr() @@ -154,6 +187,78 @@ func (o *OperationsOptions) validateVolumeExpansion() error { if len(o.Storage) == 0 { return fmt.Errorf("missing storage") } + for _, cName := range o.ComponentNames { + for _, vctName := range o.VCTNames { + labels := fmt.Sprintf("%s=%s,%s=%s,%s=%s", + constant.AppInstanceLabelKey, o.Name, + constant.KBAppComponentLabelKey, cName, + constant.VolumeClaimTemplateNameLabelKey, vctName, + ) + pvcs, err := o.Client.CoreV1().PersistentVolumeClaims(o.Namespace).List(context.Background(), + metav1.ListOptions{LabelSelector: labels, Limit: 1}) + if err != nil { + return err + } + if len(pvcs.Items) == 0 { + continue + } + pvc := pvcs.Items[0] + specStorage := pvc.Spec.Resources.Requests.Storage() + statusStorage := pvc.Status.Capacity.Storage() + targetStorage := resource.MustParse(o.Storage) + // determine whether the opsRequest is a recovery action for volume expansion failure + if specStorage.Cmp(targetStorage) > 0 && + statusStorage.Cmp(targetStorage) <= 0 { + o.autoApprove = false + fmt.Fprintln(o.Out, printer.BoldYellow("Warning: this opsRequest is a recovery action for volume expansion failure and will re-create the PersistentVolumeClaims when RECOVER_VOLUME_EXPANSION_FAILURE=false")) + break + } + } + } + return nil +} + +func (o *OperationsOptions) validateVScale(cluster *appsv1alpha1.Cluster) error { + if o.Class != "" && (o.CPU != "" || o.Memory != "") { + return fmt.Errorf("class and cpu/memory cannot be both specified") + } + if o.Class == "" && o.CPU == "" && o.Memory == "" { + return fmt.Errorf("class or cpu/memory must be specified") + } + componentClasses, err := class.ListClassesByClusterDefinition(o.Dynamic, cluster.Spec.ClusterDefRef) + if err != nil { + return err + } + + fillClassParams := func(comp *appsv1alpha1.ClusterComponentSpec) { + if o.Class != "" { + comp.ClassDefRef = &appsv1alpha1.ClassDefRef{Class: o.Class} + comp.Resources = corev1.ResourceRequirements{} + } else { + comp.ClassDefRef = &appsv1alpha1.ClassDefRef{} + requests := make(corev1.ResourceList) + if o.CPU != "" { + requests[corev1.ResourceCPU] = resource.MustParse(o.CPU) + } + if o.Memory != "" { + requests[corev1.ResourceMemory] = resource.MustParse(o.Memory) + } + requests.DeepCopyInto(&comp.Resources.Requests) + requests.DeepCopyInto(&comp.Resources.Limits) + } + } + + for _, name := range o.ComponentNames { + for _, comp := range cluster.Spec.ComponentSpecs { + if comp.Name != name { + continue + } + fillClassParams(&comp) + if _, err = class.ValidateComponentClass(&comp, componentClasses); err != nil { + return err + } + } + } return nil } @@ -164,10 +269,14 @@ func (o *OperationsOptions) Validate() error { } // check if cluster exist - _, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) + obj, err := o.Dynamic.Resource(types.ClusterGVR()).Namespace(o.Namespace).Get(context.TODO(), o.Name, metav1.GetOptions{}) if err != nil { return err } + var cluster appsv1alpha1.Cluster + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &cluster); err != nil { + return err + } // common validate for componentOps if o.HasComponentNamesFlag && len(o.ComponentNames) == 0 { @@ -176,43 +285,28 @@ func (o *OperationsOptions) Validate() error { switch o.OpsType { case appsv1alpha1.VolumeExpansionType: - if err := o.validateVolumeExpansion(); err != nil { + if err = o.validateVolumeExpansion(); err != nil { return err } case appsv1alpha1.UpgradeType: - if err := o.validateUpgrade(); err != nil { + if err = o.validateUpgrade(); err != nil { + return err + } + case appsv1alpha1.VerticalScalingType: + if err = o.validateVScale(&cluster); err != nil { + return err + } + case appsv1alpha1.ExposeType: + if err = o.validateExpose(); err != nil { return err } } - if o.RequireConfirm { + if !o.autoApprove && o.DryRun == "none" { return delete.Confirm([]string{o.Name}, o.In) } return nil } -// buildOperationsInputs builds operations inputs -func buildOperationsInputs(f cmdutil.Factory, o *OperationsOptions) create.Inputs { - o.OpsTypeLower = strings.ToLower(string(o.OpsType)) - customOutPut := func(opt *create.BaseOptions) { - output := fmt.Sprintf("OpsRequest %s created successfully, you can view the progress:", opt.Name) - printer.PrintLine(output) - nextLine := fmt.Sprintf("\tkbcli cluster describe-ops %s -n %s", opt.Name, opt.Namespace) - printer.PrintLine(nextLine) - } - return create.Inputs{ - CueTemplateName: "cluster_operations_template.cue", - ResourceName: types.ResourceOpsRequests, - BaseOptionsObj: &o.BaseOptions, - Options: o, - Factory: f, - Validate: o.Validate, - CustomOutPut: customOutPut, - Group: types.AppsAPIGroup, - Version: types.AppsAPIVersion, - ResourceNameGVRForCompletion: types.ClusterGVR(), - } -} - func (o *OperationsOptions) validateExpose() error { switch util.ExposeType(o.ExposeType) { case "", util.ExposeToVPC, util.ExposeToInternet: @@ -229,7 +323,11 @@ func (o *OperationsOptions) validateExpose() error { } func (o *OperationsOptions) fillExpose() error { - provider, err := util.GetK8SProvider(o.Client) + version, err := util.GetK8sVersion(o.Client.Discovery()) + if err != nil { + return err + } + provider, err := util.GetK8sProvider(version, o.Client) if err != nil { return err } @@ -237,6 +335,10 @@ func (o *OperationsOptions) fillExpose() error { return fmt.Errorf("unknown k8s provider") } + if err = o.CompleteComponentsFlag(); err != nil { + return err + } + // default expose to internet exposeType := util.ExposeType(o.ExposeType) if exposeType == "" { @@ -258,14 +360,6 @@ func (o *OperationsOptions) fillExpose() error { return err } - if len(o.ComponentNames) == 0 { - if len(cluster.Spec.ComponentSpecs) == 1 { - o.ComponentNames = append(o.ComponentNames, cluster.Spec.ComponentSpecs[0].Name) - } else { - return fmt.Errorf("please specify --components") - } - } - compMap := make(map[string]appsv1alpha1.ClusterComponentSpec) for _, compSpec := range cluster.Spec.ComponentSpecs { compMap[compSpec.Name] = compSpec @@ -301,108 +395,151 @@ func (o *OperationsOptions) fillExpose() error { var restartExample = templates.Examples(` # restart all components - kbcli cluster restart + kbcli cluster restart mycluster - # restart specifies the component, separate with commas when more than one - kbcli cluster restart --components= + # specified component to restart, separate with commas for multiple components + kbcli cluster restart mycluster --components=mysql `) // NewRestartCmd creates a restart command func NewRestartCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.RestartType, true) - inputs := buildOperationsInputs(f, o) - inputs.Use = "restart" - inputs.Short = "Restart the specified components in the cluster." - inputs.Example = restartExample - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - } - inputs.Complete = o.CompleteRestartOps - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.RestartType, true) + cmd := &cobra.Command{ + Use: "restart NAME", + Short: "Restart the specified components in the cluster.", + Example: restartExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.CompleteRestartOps()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before restarting the cluster") + return cmd } var upgradeExample = templates.Examples(` - # upgrade the cluster to the specified version - kbcli cluster upgrade --cluster-version= + # upgrade the cluster to the target version + kbcli cluster upgrade mycluster --cluster-version=ac-mysql-8.0.30 `) -// NewUpgradeCmd creates a upgrade command +// NewUpgradeCmd creates an upgrade command func NewUpgradeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.UpgradeType, false) - inputs := buildOperationsInputs(f, o) - inputs.Use = "upgrade" - inputs.Short = "Upgrade the cluster version." - inputs.Example = upgradeExample - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Reference cluster version (required)") - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.UpgradeType, false) + cmd := &cobra.Command{ + Use: "upgrade NAME", + Short: "Upgrade the cluster version.", + Example: upgradeExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().StringVar(&o.ClusterVersionRef, "cluster-version", "", "Reference cluster version (required)") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before upgrading the cluster") + return cmd } var verticalScalingExample = templates.Examples(` - # scale the computing resources of specified components, separate with commas when more than one - kbcli cluster vscale --components= --cpu=500m --memory=500Mi + # scale the computing resources of specified components, separate with commas for multiple components + kbcli cluster vscale mycluster --components=mysql --cpu=500m --memory=500Mi + + # scale the computing resources of specified components by class, run command 'kbcli class list --cluster-definition cluster-definition-name' to get available classes + kbcli cluster vscale mycluster --components=mysql --class=general-2c4g `) // NewVerticalScalingCmd creates a vertical scaling command func NewVerticalScalingCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.VerticalScalingType, true) - inputs := buildOperationsInputs(f, o) - inputs.Use = "vscale" - inputs.Short = "Vertically scale the specified components in the cluster." - inputs.Example = verticalScalingExample - inputs.Complete = o.CompleteComponentsFlag - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().StringVar(&o.CPU, "cpu", "", "Requested and limited size of component cpu") - cmd.Flags().StringVar(&o.Memory, "memory", "", "Requested and limited size of component memory") - cmd.Flags().StringVar(&o.Class, "class", "", "Component class") - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.VerticalScalingType, true) + cmd := &cobra.Command{ + Use: "vscale NAME", + Short: "Vertically scale the specified components in the cluster.", + Example: verticalScalingExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.CompleteComponentsFlag()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().StringVar(&o.CPU, "cpu", "", "Request and limit size of component cpu") + cmd.Flags().StringVar(&o.Memory, "memory", "", "Request and limit size of component memory") + cmd.Flags().StringVar(&o.Class, "class", "", "Component class") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before vertically scaling the cluster") + return cmd } var horizontalScalingExample = templates.Examples(` - # expand storage resources of specified components, separate with commas when more than one - kbcli cluster hscale --components= --replicas=3 + # expand storage resources of specified components, separate with commas for multiple components + kbcli cluster hscale mycluster --components=mysql --replicas=3 `) // NewHorizontalScalingCmd creates a horizontal scaling command func NewHorizontalScalingCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.HorizontalScalingType, true) - inputs := buildOperationsInputs(f, o) - inputs.Use = "hscale" - inputs.Short = "Horizontally scale the specified components in the cluster." - inputs.Example = horizontalScalingExample - inputs.Complete = o.CompleteComponentsFlag - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().IntVar(&o.Replicas, "replicas", o.Replicas, "Replicas with the specified components") - _ = cmd.MarkFlagRequired("replicas") - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.HorizontalScalingType, true) + cmd := &cobra.Command{ + Use: "hscale NAME", + Short: "Horizontally scale the specified components in the cluster.", + Example: horizontalScalingExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.CompleteComponentsFlag()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + o.addCommonFlags(cmd) + cmd.Flags().IntVar(&o.Replicas, "replicas", o.Replicas, "Replicas with the specified components") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before horizontally scaling the cluster") + _ = cmd.MarkFlagRequired("replicas") + return cmd } var volumeExpansionExample = templates.Examples(` - # restart specifies the component, separate with commas when more than one - kbcli cluster volume-expand --components= \ - --volume-claim-templates=data --storage=10Gi + # restart specifies the component, separate with commas for multiple components + kbcli cluster volume-expand mycluster --components=mysql --volume-claim-templates=data --storage=10Gi `) -// NewVolumeExpansionCmd creates a vertical scaling command +// NewVolumeExpansionCmd creates a volume expanding command func NewVolumeExpansionCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.VolumeExpansionType, true) - inputs := buildOperationsInputs(f, o) - inputs.Use = "volume-expand" - inputs.Short = "Expand volume with the specified components and volumeClaimTemplates in the cluster." - inputs.Example = volumeExpansionExample - inputs.Complete = o.CompleteComponentsFlag - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().StringSliceVarP(&o.VCTNames, "volume-claim-templates", "t", nil, "VolumeClaimTemplate names in components (required)") - cmd.Flags().StringVar(&o.Storage, "storage", "", "Volume storage size (required)") - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.VolumeExpansionType, true) + cmd := &cobra.Command{ + Use: "volume-expand NAME", + Short: "Expand volume with the specified components and volumeClaimTemplates in the cluster.", + Example: volumeExpansionExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.CompleteComponentsFlag()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().StringSliceVarP(&o.VCTNames, "volume-claim-templates", "t", nil, "VolumeClaimTemplate names in components (required)") + cmd.Flags().StringVar(&o.Storage, "storage", "", "Volume storage size (required)") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before expanding the cluster volume") + return cmd } var ( @@ -410,7 +547,7 @@ var ( # Expose a cluster to vpc kbcli cluster expose mycluster --type vpc --enable=true - # Expose a cluster to internet + # Expose a cluster to public internet kbcli cluster expose mycluster --type internet --enable=true # Stop exposing a cluster @@ -418,63 +555,153 @@ var ( `) ) -// NewExposeCmd creates a Expose command +// NewExposeCmd creates an expose command func NewExposeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.ExposeType, true) - inputs := buildOperationsInputs(f, o) - inputs.Use = "expose" - inputs.Short = "Expose a cluster." - inputs.Example = exposeExamples - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - cmd.Flags().StringVar(&o.ExposeType, "type", "", "Expose type, currently supported types are 'vpc', 'internet'") - util.CheckErr(cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{string(util.ExposeToVPC), string(util.ExposeToInternet)}, cobra.ShellCompDirectiveNoFileComp - })) - cmd.Flags().StringVar(&o.ExposeEnabled, "enable", "", "Enable or disable the expose, values can be true or false") - util.CheckErr(cmd.RegisterFlagCompletionFunc("enable", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp - })) - _ = cmd.MarkFlagRequired("enable") - } - inputs.Validate = o.validateExpose - inputs.Complete = o.fillExpose - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.ExposeType, true) + cmd := &cobra.Command{ + Use: "expose NAME --enable=[true|false] --type=[vpc|internet]", + Short: "Expose a cluster with a new endpoint, the new endpoint can be found by executing 'kbcli cluster describe NAME'.", + Example: exposeExamples, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.fillExpose()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().StringVar(&o.ExposeType, "type", "", "Expose type, currently supported types are 'vpc', 'internet'") + cmd.Flags().StringVar(&o.ExposeEnabled, "enable", "", "Enable or disable the expose, values can be true or false") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before exposing the cluster") + + util.CheckErr(cmd.RegisterFlagCompletionFunc("type", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{string(util.ExposeToVPC), string(util.ExposeToInternet)}, cobra.ShellCompDirectiveNoFileComp + })) + util.CheckErr(cmd.RegisterFlagCompletionFunc("enable", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"true", "false"}, cobra.ShellCompDirectiveNoFileComp + })) + + _ = cmd.MarkFlagRequired("enable") + return cmd } var stopExample = templates.Examples(` # stop the cluster and release all the pods of the cluster - kbcli cluster stop + kbcli cluster stop mycluster `) // NewStopCmd creates a stop command func NewStopCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.StopType, false) - inputs := buildOperationsInputs(f, o) - inputs.Use = "stop" - inputs.Short = "Stop the cluster and release all the pods of the cluster." - inputs.Example = stopExample - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.StopType, false) + cmd := &cobra.Command{ + Use: "stop NAME", + Short: "Stop the cluster and release all the pods of the cluster.", + Example: stopExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before stopping the cluster") + return cmd } var startExample = templates.Examples(` # start the cluster when cluster is stopped - kbcli cluster start + kbcli cluster start mycluster `) // NewStartCmd creates a start command func NewStartCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := newBaseOperationsOptions(streams, appsv1alpha1.StartType, false) - o.RequireConfirm = false - inputs := buildOperationsInputs(f, o) - inputs.Use = "start" - inputs.Short = "Start the cluster if cluster is stopped." - inputs.Example = startExample - inputs.BuildFlags = func(cmd *cobra.Command) { - o.buildCommonFlags(cmd) - } - return create.BuildCommand(inputs) + o := newBaseOperationsOptions(f, streams, appsv1alpha1.StartType, false) + o.autoApprove = true + cmd := &cobra.Command{ + Use: "start NAME", + Short: "Start the cluster if cluster is stopped.", + Example: startExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.ClusterGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + o.addCommonFlags(cmd) + return cmd +} + +var cancelExample = templates.Examples(` + # cancel the opsRequest which is not completed. + kbcli cluster cancel-ops +`) + +func cancelOps(o *OperationsOptions) error { + opsRequest := &appsv1alpha1.OpsRequest{} + if err := cluster.GetK8SClientObject(o.Dynamic, opsRequest, o.GVR, o.Namespace, o.Name); err != nil { + return err + } + notSupportedPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsFailedPhase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.OpsCancelledPhase} + if slices.Contains(notSupportedPhases, opsRequest.Status.Phase) { + return fmt.Errorf("can not cancel the opsRequest when phase is %s", opsRequest.Status.Phase) + } + if opsRequest.Status.Phase == appsv1alpha1.OpsCancellingPhase { + return fmt.Errorf(`opsRequest "%s" is cancelling`, opsRequest.Name) + } + supportedType := []appsv1alpha1.OpsType{appsv1alpha1.HorizontalScalingType, appsv1alpha1.VerticalScalingType} + if !slices.Contains(supportedType, opsRequest.Spec.Type) { + return fmt.Errorf("opsRequest type: %s not support cancel action", opsRequest.Spec.Type) + } + if !o.autoApprove { + if err := delete.Confirm([]string{o.Name}, o.In); err != nil { + return err + } + } + oldOps := opsRequest.DeepCopy() + opsRequest.Spec.Cancel = true + oldData, err := json.Marshal(oldOps) + if err != nil { + return err + } + newData, err := json.Marshal(opsRequest) + if err != nil { + return err + } + patchBytes, err := jsonpatch.CreateMergePatch(oldData, newData) + if err != nil { + return err + } + if _, err = o.Dynamic.Resource(types.OpsGVR()).Namespace(opsRequest.Namespace).Patch(context.TODO(), + opsRequest.Name, apitypes.MergePatchType, patchBytes, metav1.PatchOptions{}); err != nil { + return err + } + fmt.Fprintf(o.Out, "start to cancel opsRequest \"%s\", you can view the progress:\n\tkbcli cluster list-ops --name %s\n", o.Name, o.Name) + return nil +} + +func NewCancelCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := newBaseOperationsOptions(f, streams, "", false) + cmd := &cobra.Command{ + Use: "cancel-ops NAME", + Short: "cancel the pending/creating/running OpsRequest which type is vscale or hscale.", + Example: cancelExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.OpsGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.BehaviorOnFatal(printer.FatalWithRedColor) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(cancelOps(o)) + }, + } + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before cancel the opsRequest") + return cmd } diff --git a/internal/cli/cmd/cluster/operations_test.go b/internal/cli/cmd/cluster/operations_test.go index eba9f5c00..e24e2fa8b 100644 --- a/internal/cli/cmd/cluster/operations_test.go +++ b/internal/cli/cmd/cluster/operations_test.go @@ -1,35 +1,43 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( "bytes" + "fmt" + "strings" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/cli/delete" "github.com/apecloud/kubeblocks/internal/cli/testing" - "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/constant" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) var _ = Describe("operations", func() { @@ -52,8 +60,12 @@ var _ = Describe("operations", func() { clusterWithOneComp.Spec.ComponentSpecs = []appsv1alpha1.ClusterComponentSpec{ clusterWithOneComp.Spec.ComponentSpecs[0], } + clusterWithOneComp.Spec.ComponentSpecs[0].ClassDefRef = &appsv1alpha1.ClassDefRef{Class: testapps.Class1c1gName} + classDef := testapps.NewComponentClassDefinitionFactory("custom", clusterWithOneComp.Spec.ClusterDefRef, testing.ComponentDefName). + AddClasses(testapps.DefaultResourceConstraintName, []string{testapps.Class1c1gName}). + GetObject() tf.FakeDynamicClient = testing.FakeDynamicClient(testing.FakeClusterDef(), - testing.FakeClusterVersion(), clusterWithTwoComps, clusterWithOneComp) + testing.FakeClusterVersion(), clusterWithTwoComps, clusterWithOneComp, classDef) tf.Client = &clientfake.RESTClient{} }) @@ -61,16 +73,38 @@ var _ = Describe("operations", func() { tf.Cleanup() }) - initCommonOperationOps := func(opsType appsv1alpha1.OpsType, clusterName string, hasComponentNamesFlag bool) *OperationsOptions { - o := newBaseOperationsOptions(streams, opsType, hasComponentNamesFlag) + initCommonOperationOps := func(opsType appsv1alpha1.OpsType, clusterName string, hasComponentNamesFlag bool, objs ...runtime.Object) *OperationsOptions { + o := newBaseOperationsOptions(tf, streams, opsType, hasComponentNamesFlag) o.Dynamic = tf.FakeDynamicClient + o.Client = testing.FakeClientSet(objs...) o.Name = clusterName o.Namespace = testing.Namespace return o } + getOpsName := func(opsType appsv1alpha1.OpsType, phase appsv1alpha1.OpsPhase) string { + return strings.ToLower(string(opsType)) + "-" + strings.ToLower(string(phase)) + } + + generationOps := func(opsType appsv1alpha1.OpsType, phase appsv1alpha1.OpsPhase) *appsv1alpha1.OpsRequest { + return &appsv1alpha1.OpsRequest{ + ObjectMeta: metav1.ObjectMeta{ + Name: getOpsName(opsType, phase), + Namespace: testing.Namespace, + }, + Spec: appsv1alpha1.OpsRequestSpec{ + ClusterRef: "test-cluster", + Type: opsType, + }, + Status: appsv1alpha1.OpsRequestStatus{ + Phase: phase, + }, + } + + } + It("Upgrade Ops", func() { - o := newBaseOperationsOptions(streams, appsv1alpha1.UpgradeType, false) + o := newBaseOperationsOptions(tf, streams, appsv1alpha1.UpgradeType, false) o.Dynamic = tf.FakeDynamicClient By("validate o.name is null") @@ -89,22 +123,90 @@ var _ = Describe("operations", func() { }) It("VolumeExpand Ops", func() { - o := initCommonOperationOps(appsv1alpha1.VolumeExpansionType, clusterName, true) + compName := "replicasets" + vctName := "data" + persistentVolumeClaim := &corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s-%s-%d", vctName, clusterName, compName, 0), + Namespace: testing.Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.VolumeClaimTemplateNameLabelKey: vctName, + constant.KBAppComponentLabelKey: compName, + }, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + "storage": resource.MustParse("3Gi"), + }, + }, + }, + Status: corev1.PersistentVolumeClaimStatus{ + Capacity: map[corev1.ResourceName]resource.Quantity{ + "storage": resource.MustParse("1Gi"), + }, + }, + } + o := initCommonOperationOps(appsv1alpha1.VolumeExpansionType, clusterName, true, persistentVolumeClaim) By("validate volumeExpansion when components is null") Expect(o.Validate()).To(MatchError(`missing components, please specify the "--components" flag for multi-components cluster`)) By("validate volumeExpansion when vct-names is null") - o.ComponentNames = []string{"replicasets"} + o.ComponentNames = []string{compName} Expect(o.Validate()).To(MatchError("missing volume-claim-templates")) By("validate volumeExpansion when storage is null") - o.VCTNames = []string{"data"} + o.VCTNames = []string{vctName} Expect(o.Validate()).To(MatchError("missing storage")) + + By("validate recovery from volume expansion failure") o.Storage = "2Gi" + Expect(o.Validate()).Should(Succeed()) + Expect(o.Out.(*bytes.Buffer).String()).To(ContainSubstring("Warning: this opsRequest is a recovery action for volume expansion failure and will re-create the PersistentVolumeClaims when RECOVER_VOLUME_EXPANSION_FAILURE=false")) + + By("validate passed") + o.Storage = "4Gi" in.Write([]byte(o.Name + "\n")) Expect(o.Validate()).Should(Succeed()) }) + It("Vscale Ops", func() { + o := initCommonOperationOps(appsv1alpha1.VerticalScalingType, clusterName1, true) + By("test CompleteComponentsFlag function") + o.ComponentNames = nil + By("expect to auto complete components when cluster has only one component") + Expect(o.CompleteComponentsFlag()).Should(Succeed()) + Expect(o.ComponentNames[0]).Should(Equal(testing.ComponentName)) + + By("validate invalid class") + o.Class = "class-not-exists" + in.Write([]byte(o.Name + "\n")) + Expect(o.Validate()).Should(HaveOccurred()) + + By("expect to validate success with class") + o.Class = testapps.Class1c1gName + in.Write([]byte(o.Name + "\n")) + Expect(o.Validate()).ShouldNot(HaveOccurred()) + + By("validate invalid resource") + o.Class = "" + o.CPU = "100" + o.Memory = "100Gi" + in.Write([]byte(o.Name + "\n")) + Expect(o.Validate()).Should(HaveOccurred()) + + By("expect to validate success with resource") + o.Class = "" + o.CPU = "1" + o.Memory = "1Gi" + in.Write([]byte(o.Name + "\n")) + Expect(o.Validate()).ShouldNot(HaveOccurred()) + }) + It("Hscale Ops", func() { o := initCommonOperationOps(appsv1alpha1.HorizontalScalingType, clusterName1, true) By("test CompleteComponentsFlag function") @@ -128,8 +230,8 @@ var _ = Describe("operations", func() { It("Restart ops", func() { o := initCommonOperationOps(appsv1alpha1.RestartType, clusterName, true) By("expect for not found error") - inputs := buildOperationsInputs(tf, o) - Expect(o.Complete(inputs, []string{clusterName + "2"})) + o.Args = []string{clusterName + "2"} + Expect(o.Complete()) Expect(o.CompleteRestartOps().Error()).Should(ContainSubstring("not found")) By("expect for complete success") @@ -145,26 +247,59 @@ var _ = Describe("operations", func() { Expect(testing.ContainExpectStrings(capturedOutput, "kbcli cluster describe-ops")).Should(BeTrue()) }) - It("list and delete operations", func() { - clusterName := "wesql" - args := []string{clusterName} - clusterLabel := util.BuildLabelSelectorByNames("", args) - testLabel := "kubeblocks.io/test=test" - - By("test delete OpsRequest with cluster") - o := delete.NewDeleteOptions(tf, streams, types.OpsGVR()) - Expect(completeForDeleteOps(o, args)).Should(Succeed()) - Expect(o.LabelSelector == clusterLabel).Should(BeTrue()) - - By("test delete OpsRequest with cluster and custom label") - o.LabelSelector = testLabel - Expect(completeForDeleteOps(o, args)).Should(Succeed()) - Expect(o.LabelSelector == testLabel+","+clusterLabel).Should(BeTrue()) - - By("test delete OpsRequest with name") - o.Names = []string{"test1"} - Expect(completeForDeleteOps(o, nil)).Should(Succeed()) - Expect(len(o.ConfirmedNames)).Should(Equal(1)) - }) + It("cancel ops", func() { + By("init some opsRequests which are needed for canceling opsRequest") + completedPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsCancelledPhase, appsv1alpha1.OpsSucceedPhase, appsv1alpha1.OpsFailedPhase} + supportedOpsType := []appsv1alpha1.OpsType{appsv1alpha1.VerticalScalingType, appsv1alpha1.HorizontalScalingType} + notSupportedOpsType := []appsv1alpha1.OpsType{appsv1alpha1.RestartType, appsv1alpha1.UpgradeType} + processingPhases := []appsv1alpha1.OpsPhase{appsv1alpha1.OpsPendingPhase, appsv1alpha1.OpsCreatingPhase, appsv1alpha1.OpsRunningPhase} + opsList := make([]runtime.Object, 0) + for _, opsType := range supportedOpsType { + for _, phase := range completedPhases { + opsList = append(opsList, generationOps(opsType, phase)) + } + for _, phase := range processingPhases { + opsList = append(opsList, generationOps(opsType, phase)) + } + // mock cancelling opsRequest + opsList = append(opsList, generationOps(opsType, appsv1alpha1.OpsCancellingPhase)) + } + + for _, opsType := range notSupportedOpsType { + opsList = append(opsList, generationOps(opsType, appsv1alpha1.OpsRunningPhase)) + } + tf.FakeDynamicClient = testing.FakeDynamicClient(opsList...) + By("expect an error for not supported phase") + o := newBaseOperationsOptions(tf, streams, "", false) + o.Dynamic = tf.FakeDynamicClient + o.Namespace = testing.Namespace + o.autoApprove = true + for _, phase := range completedPhases { + for _, opsType := range supportedOpsType { + o.Name = getOpsName(opsType, phase) + Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf("can not cancel the opsRequest when phase is %s", phase))) + } + } + + By("expect an error for not supported opsType") + for _, opsType := range notSupportedOpsType { + o.Name = getOpsName(opsType, appsv1alpha1.OpsRunningPhase) + Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf("opsRequest type: %s not support cancel action", opsType))) + } + + By("expect an error for cancelling opsRequest") + for _, opsType := range supportedOpsType { + o.Name = getOpsName(opsType, appsv1alpha1.OpsCancellingPhase) + Expect(cancelOps(o).Error()).Should(Equal(fmt.Sprintf(`opsRequest "%s" is cancelling`, o.Name))) + } + + By("expect succeed for canceling the opsRequest which is processing") + for _, phase := range processingPhases { + for _, opsType := range supportedOpsType { + o.Name = getOpsName(opsType, phase) + Expect(cancelOps(o)).Should(Succeed()) + } + } + }) }) diff --git a/internal/cli/cmd/cluster/suite_test.go b/internal/cli/cmd/cluster/suite_test.go index 53c9e84b3..bc75d56a7 100644 --- a/internal/cli/cmd/cluster/suite_test.go +++ b/internal/cli/cmd/cluster/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster diff --git a/internal/cli/cmd/cluster/update.go b/internal/cli/cmd/cluster/update.go index 7cd8da50d..e4940a453 100644 --- a/internal/cli/cmd/cluster/update.go +++ b/internal/cli/cmd/cluster/update.go @@ -1,29 +1,39 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster import ( + "bytes" + "context" "encoding/csv" + "encoding/json" "fmt" "strconv" "strings" + "text/template" + "github.com/google/uuid" + "github.com/pkg/errors" "github.com/spf13/cobra" "github.com/spf13/pflag" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" @@ -36,6 +46,9 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/patch" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/controller/plan" + "github.com/apecloud/kubeblocks/internal/gotemplate" ) var clusterUpdateExample = templates.Examples(` @@ -53,6 +66,12 @@ var clusterUpdateExample = templates.Examples(` # update cluster tolerations kbcli cluster update mycluster --tolerations='"key=engineType,value=mongo,operator=Equal,effect=NoSchedule","key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + + # edit cluster + kbcli cluster update mycluster --edit + + # enable cluster monitor and edit + # kbcli cluster update mycluster --monitor=true --edit `) type updateOptions struct { @@ -152,7 +171,10 @@ func (o *updateOptions) buildPatch(flags []*pflag.Flag) error { } buildTolObj := func(obj map[string]interface{}, v pflag.Value, field string) error { - tolerations := buildTolerations(o.TolerationsRaw) + tolerations, err := util.BuildTolerations(o.TolerationsRaw) + if err != nil { + return err + } return unstructured.SetNestedField(obj, tolerations, field) } @@ -232,38 +254,190 @@ func (o *updateOptions) buildComponents(field string, val string) error { switch field { case "monitor": - return o.setMonitor(val) + return o.updateMonitor(val) case "enable-all-logs": - return o.setEnabledLog(val) + return o.updateEnabledLog(val) default: return nil } } -func (o *updateOptions) setEnabledLog(val string) error { +func (o *updateOptions) updateEnabledLog(val string) error { boolVal, err := strconv.ParseBool(val) if err != nil { return err } - // disable all monitor + // update --enabled-all-logs=false for all components if !boolVal { - for _, c := range o.cluster.Spec.ComponentSpecs { - c.EnabledLogs = nil + for index := range o.cluster.Spec.ComponentSpecs { + o.cluster.Spec.ComponentSpecs[index].EnabledLogs = nil } return nil } - // enable all monitor + // update --enabled-all-logs=true for all components cd, err := cluster.GetClusterDefByName(o.dynamic, o.cluster.Spec.ClusterDefRef) if err != nil { return err } + // set --enabled-all-logs at cluster components setEnableAllLogs(o.cluster, cd) + if err = o.reconfigureLogVariables(o.cluster, cd); err != nil { + return errors.Wrap(err, "failed to reconfigure log variables of target cluster") + } + return nil +} + +const logsBlockName = "logsBlock" +const logsTemplateName = "template-logs-block" +const topTPLLogsObject = "component" +const defaultSectionName = "default" + +// reconfigureLogVariables reconfigures the log variables of cluster +func (o *updateOptions) reconfigureLogVariables(c *appsv1alpha1.Cluster, cd *appsv1alpha1.ClusterDefinition) error { + var ( + err error + keyName string + configSpec *appsv1alpha1.ComponentConfigSpec + configTemplate *corev1.ConfigMap + formatter *appsv1alpha1.FormatterConfig + logTPL *template.Template + logValue *gotemplate.TplValues + buf bytes.Buffer + logVariables map[string]string + unstructuredObj *unstructured.Unstructured + ) + for _, compSpec := range c.Spec.ComponentSpecs { + if configSpec, err = findFirstConfigSpec(c.Spec.ComponentSpecs, cd.Spec.ComponentDefs, compSpec.Name); err != nil { + return err + } + if configTemplate, formatter, err = findConfigTemplateInfo(o.dynamic, configSpec); err != nil { + return err + } + if keyName, logTPL, err = findLogsBlockTPL(configTemplate.Data); err != nil { + return err + } + if logValue, err = buildLogsTPLValues(&compSpec); err != nil { + return err + } + if err = logTPL.Execute(&buf, logValue); err != nil { + return err + } + formatter.FormatterOptions = appsv1alpha1.FormatterOptions{IniConfig: &appsv1alpha1.IniConfig{SectionName: defaultSectionName}} + if logVariables, err = cfgcore.TransformConfigFileToKeyValueMap(keyName, formatter, buf.Bytes()); err != nil { + return err + } + // build OpsRequest and apply this OpsRequest + opsRequest := buildLogsReconfiguringOps(c.Name, c.Namespace, compSpec.Name, configSpec.Name, keyName, logVariables) + if unstructuredObj, err = util.ConvertObjToUnstructured(opsRequest); err != nil { + return err + } + if err = util.CreateResourceIfAbsent(o.dynamic, types.OpsGVR(), c.Namespace, unstructuredObj); err != nil { + return err + } + } return nil } -func (o *updateOptions) setMonitor(val string) error { +func findFirstConfigSpec( + compSpecs []appsv1alpha1.ClusterComponentSpec, + cdCompSpecs []appsv1alpha1.ClusterComponentDefinition, + compName string) (*appsv1alpha1.ComponentConfigSpec, error) { + configSpecs, err := util.GetConfigTemplateListWithResource(compSpecs, cdCompSpecs, nil, compName, true) + if err != nil { + return nil, err + } + if len(configSpecs) == 0 { + return nil, errors.Errorf("no config templates for component %s", compName) + } + return &configSpecs[0], nil +} + +func findConfigTemplateInfo(dynamic dynamic.Interface, configSpec *appsv1alpha1.ComponentConfigSpec) (*corev1.ConfigMap, *appsv1alpha1.FormatterConfig, error) { + if configSpec == nil { + return nil, nil, errors.New("configTemplateSpec is nil") + } + configTemplate, err := cluster.GetConfigMapByName(dynamic, configSpec.Namespace, configSpec.TemplateRef) + if err != nil { + return nil, nil, err + } + configConstraint, err := cluster.GetConfigConstraintByName(dynamic, configSpec.ConfigConstraintRef) + if err != nil { + return nil, nil, err + } + return configTemplate, configConstraint.Spec.FormatterConfig, nil +} + +func newConfigTemplateEngine() *template.Template { + customizedFuncMap := plan.BuiltInCustomFunctions(nil, nil, nil) + engine := gotemplate.NewTplEngine(nil, customizedFuncMap, logsTemplateName, nil, context.TODO()) + return engine.GetTplEngine() +} + +func findLogsBlockTPL(confData map[string]string) (string, *template.Template, error) { + engine := newConfigTemplateEngine() + for key, value := range confData { + if !strings.Contains(value, logsBlockName) { + continue + } + tpl, err := engine.Parse(value) + if err != nil { + return key, nil, err + } + logTPL := tpl.Lookup(logsBlockName) + // find target logs template + if logTPL != nil { + return key, logTPL, nil + } + } + return "", nil, errors.New("no logs config template found") +} + +func buildLogsTPLValues(compSpec *appsv1alpha1.ClusterComponentSpec) (*gotemplate.TplValues, error) { + compMap := map[string]interface{}{} + bytesData, err := json.Marshal(compSpec) + if err != nil { + return nil, err + } + err = json.Unmarshal(bytesData, &compMap) + if err != nil { + return nil, err + } + value := gotemplate.TplValues{ + topTPLLogsObject: compMap, + } + return &value, nil +} + +func buildLogsReconfiguringOps(clusterName, namespace, compName, configName, keyName string, variables map[string]string) *appsv1alpha1.OpsRequest { + opsName := fmt.Sprintf("%s-%s", "logs-reconfigure", uuid.NewString()) + opsRequest := util.NewOpsRequestForReconfiguring(opsName, namespace, clusterName) + parameterPairs := make([]appsv1alpha1.ParameterPair, 0, len(variables)) + for key, value := range variables { + v := value + parameterPairs = append(parameterPairs, appsv1alpha1.ParameterPair{ + Key: key, + Value: &v, + }) + } + var keys []appsv1alpha1.ParameterConfig + keys = append(keys, appsv1alpha1.ParameterConfig{ + Key: keyName, + Parameters: parameterPairs, + }) + var configurations []appsv1alpha1.Configuration + configurations = append(configurations, appsv1alpha1.Configuration{ + Keys: keys, + Name: configName, + }) + reconfigure := opsRequest.Spec.Reconfigure + reconfigure.ComponentName = compName + reconfigure.Configurations = append(reconfigure.Configurations, configurations...) + return opsRequest +} + +func (o *updateOptions) updateMonitor(val string) error { boolVal, err := strconv.ParseBool(val) if err != nil { return err diff --git a/internal/cli/cmd/cluster/update_test.go b/internal/cli/cmd/cluster/update_test.go index ad89939aa..ba9da4d2a 100644 --- a/internal/cli/cmd/cluster/update_test.go +++ b/internal/cli/cmd/cluster/update_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package cluster @@ -21,8 +24,11 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/patch" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" @@ -101,4 +107,177 @@ var _ = Describe("cluster update", func() { Expect(o.Patch).Should(ContainSubstring("k1")) }) }) + Context("logs variables reconfiguring tests", func() { + var ( + c *appsv1alpha1.Cluster + cd *appsv1alpha1.ClusterDefinition + myConfig string + ) + BeforeEach(func() { + c = testing.FakeCluster("c1", "default") + cd = testing.FakeClusterDef() + myConfig = ` +{{ block "logsBlock" . }} +log_statements_unsafe_for_binlog=OFF +log_error_verbosity=2 +log_output=FILE +{{- if hasKey $.component "enabledLogs" }} +{{- if mustHas "error" $.component.enabledLogs }} +log_error=/data/mysql/log/mysqld-error.log +{{- end }} +{{- if mustHas "slow" $.component.enabledLogs }} +slow_query_log=ON +long_query_time=5 +slow_query_log_file=/data/mysql/log/mysqld-slowquery.log +{{- end }} +{{- if mustHas "general" $.component.enabledLogs }} +general_log=ON +general_log_file=/data/mysql/log/mysqld.log +{{- end }} +{{- end }} +{{ end }} +` + }) + + It("findFirstConfigSpec tests", func() { + tests := []struct { + compSpecs []appsv1alpha1.ClusterComponentSpec + cdCompSpecs []appsv1alpha1.ClusterComponentDefinition + compName string + expectedErr bool + }{ + { + compSpecs: nil, + cdCompSpecs: nil, + compName: "name", + expectedErr: true, + }, + { + compSpecs: c.Spec.ComponentSpecs, + cdCompSpecs: cd.Spec.ComponentDefs, + compName: testing.ComponentName, + expectedErr: false, + }, + { + compSpecs: c.Spec.ComponentSpecs, + cdCompSpecs: cd.Spec.ComponentDefs, + compName: "error-name", + expectedErr: true, + }, + } + for _, test := range tests { + configSpec, err := findFirstConfigSpec(test.compSpecs, test.cdCompSpecs, test.compName) + if test.expectedErr { + Expect(err).Should(HaveOccurred()) + } else { + Expect(configSpec).ShouldNot(BeNil()) + Expect(err).ShouldNot(HaveOccurred()) + } + } + }) + + It("findConfigTemplateInfo tests", func() { + tests := []struct { + dynamic dynamic.Interface + configSpec *appsv1alpha1.ComponentConfigSpec + expectedErr bool + }{{ + dynamic: nil, + configSpec: nil, + expectedErr: true, + }, { + dynamic: testing.FakeDynamicClient(testing.FakeConfigMap("config-template")), + configSpec: &appsv1alpha1.ComponentConfigSpec{ + ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ + TemplateRef: "config-template", + Namespace: testing.Namespace, + }, + }, + expectedErr: true, + }, { + dynamic: testing.FakeDynamicClient(testing.FakeConfigMap("config-template")), + configSpec: &appsv1alpha1.ComponentConfigSpec{ + ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ + TemplateRef: "config-template", + Namespace: testing.Namespace, + }, + }, + expectedErr: true, + }, { + dynamic: testing.FakeDynamicClient(testing.FakeConfigMap("config-template"), testing.FakeConfigConstraint("config-constraint")), + configSpec: &appsv1alpha1.ComponentConfigSpec{ + ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ + TemplateRef: "config-template", + + Namespace: testing.Namespace, + }, + ConfigConstraintRef: "config-constraint", + }, + expectedErr: false, + }} + for _, test := range tests { + cm, format, err := findConfigTemplateInfo(test.dynamic, test.configSpec) + if test.expectedErr { + Expect(err).Should(HaveOccurred()) + } else { + Expect(cm).ShouldNot(BeNil()) + Expect(format).ShouldNot(BeNil()) + Expect(err).ShouldNot(HaveOccurred()) + } + } + }) + + It("findLogsBlockTPL tests", func() { + tests := []struct { + confData map[string]string + keyName string + expectedErr bool + }{{ + confData: nil, + keyName: "", + expectedErr: true, + }, { + confData: map[string]string{ + "test.cnf": "test", + "my.cnf": "{{ logsBlock", + }, + keyName: "my.cnf", + expectedErr: true, + }, { + confData: map[string]string{ + "my.cnf": myConfig, + }, + keyName: "my.cnf", + expectedErr: false, + }, + } + for _, test := range tests { + key, tpl, err := findLogsBlockTPL(test.confData) + if test.expectedErr { + Expect(err).Should(HaveOccurred()) + } else { + Expect(key).Should(Equal(test.keyName)) + Expect(tpl).ShouldNot(BeNil()) + Expect(err).ShouldNot(HaveOccurred()) + } + } + }) + + It("buildLogsTPLValues tests", func() { + configSpec := testing.FakeCluster("test", "test").Spec.ComponentSpecs[0] + tplValue, err := buildLogsTPLValues(&configSpec) + Expect(err).ShouldNot(HaveOccurred()) + Expect(tplValue).ShouldNot(BeNil()) + }) + + It("buildLogsReconfiguringOps tests", func() { + opsRequest := buildLogsReconfiguringOps("clusterName", "namespace", "compName", "configName", "keyName", map[string]string{"key1": "value1", "key2": "value2"}) + Expect(opsRequest).ShouldNot(BeNil()) + Expect(opsRequest.Spec.Reconfigure.ComponentName).Should(Equal("compName")) + Expect(opsRequest.Spec.Reconfigure.Configurations).Should(HaveLen(1)) + Expect(opsRequest.Spec.Reconfigure.Configurations[0].Keys).Should(HaveLen(1)) + Expect(opsRequest.Spec.Reconfigure.Configurations[0].Keys[0].Parameters).Should(HaveLen(2)) + }) + + }) }) diff --git a/internal/cli/cmd/clusterdefinition/clusterdefinition.go b/internal/cli/cmd/clusterdefinition/clusterdefinition.go index c8c853c33..78449a8a9 100644 --- a/internal/cli/cmd/clusterdefinition/clusterdefinition.go +++ b/internal/cli/cmd/clusterdefinition/clusterdefinition.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterdefinition @@ -28,7 +31,7 @@ import ( ) var listExample = templates.Examples(` - # list all ClusterDefinition + # list all ClusterDefinitions kbcli clusterdefinition list`) func NewClusterDefinitionCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -39,6 +42,7 @@ func NewClusterDefinitionCmd(f cmdutil.Factory, streams genericclioptions.IOStre } cmd.AddCommand(NewListCmd(f, streams)) + cmd.AddCommand(NewListComponentsCmd(f, streams)) return cmd } @@ -56,6 +60,6 @@ func NewListCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C util.CheckErr(err) }, } - o.AddFlags(cmd) + o.AddFlags(cmd, true) return cmd } diff --git a/internal/cli/cmd/clusterdefinition/clusterdefinition_test.go b/internal/cli/cmd/clusterdefinition/clusterdefinition_test.go index 7c11eed58..6673b181f 100644 --- a/internal/cli/cmd/clusterdefinition/clusterdefinition_test.go +++ b/internal/cli/cmd/clusterdefinition/clusterdefinition_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterdefinition @@ -24,13 +27,12 @@ import ( ) var _ = Describe("clusterdefinition", func() { - const namespace = "test" var streams genericclioptions.IOStreams var tf *cmdtesting.TestFactory BeforeEach(func() { streams, _, _, _ = genericclioptions.NewTestIOStreams() - tf = cmdtesting.NewTestFactory().WithNamespace(namespace) + tf = cmdtesting.NewTestFactory() }) AfterEach(func() { diff --git a/internal/cli/cmd/clusterdefinition/list_compoents.go b/internal/cli/cmd/clusterdefinition/list_compoents.go new file mode 100644 index 000000000..eb66dc9be --- /dev/null +++ b/internal/cli/cmd/clusterdefinition/list_compoents.go @@ -0,0 +1,100 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package clusterdefinition + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/list" + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + listComponentsExample = templates.Examples(` + # List all components belonging to the cluster definition. + kbcli clusterdefinition list-components apecloud-mysql`) +) + +func NewListComponentsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := list.NewListOptions(f, streams, types.ClusterDefGVR()) + o.AllNamespaces = true + cmd := &cobra.Command{ + Use: "list-components", + Short: "List cluster definition components.", + Example: listComponentsExample, + Aliases: []string{"ls-comps"}, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(validate(args)) + o.Names = args + util.CheckErr(run(o)) + }, + } + return cmd +} + +func validate(args []string) error { + if len(args) == 0 { + return fmt.Errorf("missing clusterdefinition name") + } + return nil +} + +func run(o *list.ListOptions) error { + o.Print = false + + r, err := o.Run() + if err != nil { + return err + } + infos, err := r.Infos() + if err != nil { + return err + } + p := printer.NewTablePrinter(o.Out) + p.SetHeader("NAME", "WORKLOAD-TYPE", "CHARACTER-TYPE", "CLUSTER-DEFINITION", "IS-MAIN") + p.SortBy(4, 1) + for _, info := range infos { + var cd v1alpha1.ClusterDefinition + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(info.Object.(*unstructured.Unstructured).Object, &cd); err != nil { + return err + } + for i, comp := range cd.Spec.ComponentDefs { + if i == 0 { + p.AddRow(printer.BoldGreen(comp.Name), comp.WorkloadType, comp.CharacterType, cd.Name, "true") + } else { + p.AddRow(comp.Name, comp.WorkloadType, comp.CharacterType, cd.Name, "false") + } + + } + } + p.Print() + return nil +} diff --git a/internal/cli/cmd/clusterdefinition/list_component_test.go b/internal/cli/cmd/clusterdefinition/list_component_test.go new file mode 100644 index 000000000..4eb6a8efe --- /dev/null +++ b/internal/cli/cmd/clusterdefinition/list_component_test.go @@ -0,0 +1,114 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package clusterdefinition + +import ( + "bytes" + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + "k8s.io/kubectl/pkg/scheme" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/list" + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/types" +) + +var _ = Describe("clusterdefinition list components", func() { + var ( + cmd *cobra.Command + streams genericclioptions.IOStreams + out *bytes.Buffer + tf *cmdtesting.TestFactory + ) + + const ( + namespace = testing.Namespace + clusterdefinitionName = testing.ClusterDefName + ) + + mockClient := func(data runtime.Object) *cmdtesting.TestFactory { + tf := testing.NewTestFactory(namespace) + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &clientfake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, data)}, + } + tf.Client = tf.UnstructuredClient + tf.FakeDynamicClient = testing.FakeDynamicClient(data) + return tf + } + + BeforeEach(func() { + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + clusterDef := testing.FakeClusterDef() + tf = mockClient(clusterDef) + streams, _, out, _ = genericclioptions.NewTestIOStreams() + cmd = NewListComponentsCmd(tf, streams) + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("create list-components cmd", func() { + cmd := NewListComponentsCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + + It("list-components requires a clusterdefinition Name", func() { + Expect(validate([]string{})).Should(HaveOccurred()) + }) + + It("cd list-components when the cd do not exist", func() { + o := list.NewListOptions(tf, streams, types.ClusterDefGVR()) + o.AllNamespaces = true + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &clientfake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, testing.FakeResourceNotFound(types.ClusterDefGVR(), clusterdefinitionName+"-no-exist"))}, + } + Expect(run(o)).Should(HaveOccurred()) + + }) + + It("list-components", func() { + cmd.Run(cmd, []string{clusterdefinitionName}) + expected := `NAME WORKLOAD-TYPE CHARACTER-TYPE CLUSTER-DEFINITION IS-MAIN +fake-component-type mysql fake-cluster-definition true +fake-component-type-1 mysql fake-cluster-definition false +` + Expect(expected).Should(Equal(out.String())) + fmt.Println(out.String()) + }) +}) diff --git a/internal/cli/cmd/clusterdefinition/suite_test.go b/internal/cli/cmd/clusterdefinition/suite_test.go index 2136ee232..204a1b093 100644 --- a/internal/cli/cmd/clusterdefinition/suite_test.go +++ b/internal/cli/cmd/clusterdefinition/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterdefinition diff --git a/internal/cli/cmd/clusterversion/clusterversion.go b/internal/cli/cmd/clusterversion/clusterversion.go index f77ff4509..2b8e7036e 100644 --- a/internal/cli/cmd/clusterversion/clusterversion.go +++ b/internal/cli/cmd/clusterversion/clusterversion.go @@ -1,36 +1,50 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterversion import ( "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/cli-runtime/pkg/genericclioptions" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" + "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/list" + "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/util/flags" + "github.com/apecloud/kubeblocks/internal/constant" ) var listExample = templates.Examples(` - # list all ClusterVersion + # list all ClusterVersions kbcli clusterversion list`) +type ListClusterVersionOptions struct { + *list.ListOptions + clusterDefinitionRef string +} + func NewClusterVersionCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "clusterversion", @@ -39,11 +53,15 @@ func NewClusterVersionCmd(f cmdutil.Factory, streams genericclioptions.IOStreams } cmd.AddCommand(NewListCmd(f, streams)) + cmd.AddCommand(newSetDefaultCMD(f, streams)) + cmd.AddCommand(newUnSetDefaultCMD(f, streams)) return cmd } func NewListCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { - o := list.NewListOptions(f, streams, types.ClusterVersionGVR()) + o := &ListClusterVersionOptions{ + ListOptions: list.NewListOptions(f, streams, types.ClusterVersionGVR()), + } cmd := &cobra.Command{ Use: "list", Short: "List ClusterVersions.", @@ -51,11 +69,57 @@ func NewListCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C Aliases: []string{"ls"}, ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), Run: func(cmd *cobra.Command, args []string) { + if len(o.clusterDefinitionRef) != 0 { + o.LabelSelector = util.BuildClusterDefinitionRefLable(o.LabelSelector, []string{o.clusterDefinitionRef}) + } o.Names = args - _, err := o.Run() - util.CheckErr(err) + util.CheckErr(run(o)) }, } - o.AddFlags(cmd) + o.AddFlags(cmd, true) + flags.AddClusterDefinitionFlag(f, cmd, &o.clusterDefinitionRef) return cmd } + +func run(o *ListClusterVersionOptions) error { + if !o.Format.IsHumanReadable() { + _, err := o.Run() + return err + } + o.Print = false + r, err := o.Run() + if err != nil { + return err + } + infos, err := r.Infos() + if err != nil { + return err + } + p := printer.NewTablePrinter(o.Out) + p.SetHeader("NAME", "CLUSTER-DEFINITION", "STATUS", "IS-DEFAULT", "CREATED-TIME") + p.SortBy(2) + for _, info := range infos { + var cv v1alpha1.ClusterVersion + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(info.Object.(*unstructured.Unstructured).Object, &cv); err != nil { + return err + } + isDefaultValue := isDefault(&cv) + if isDefaultValue == "true" { + p.AddRow(printer.BoldGreen(cv.Name), cv.Labels[constant.ClusterDefLabelKey], cv.Status.Phase, isDefaultValue, util.TimeFormat(&cv.CreationTimestamp)) + continue + } + p.AddRow(cv.Name, cv.Labels[constant.ClusterDefLabelKey], cv.Status.Phase, isDefaultValue, util.TimeFormat(&cv.CreationTimestamp)) + } + p.Print() + return nil +} + +func isDefault(cv *v1alpha1.ClusterVersion) string { + if cv.Annotations == nil { + return "false" + } + if _, ok := cv.Annotations[constant.DefaultClusterVersionAnnotationKey]; !ok { + return "false" + } + return cv.Annotations[constant.DefaultClusterVersionAnnotationKey] +} diff --git a/internal/cli/cmd/clusterversion/clusterversion_test.go b/internal/cli/cmd/clusterversion/clusterversion_test.go index 2225a0b49..11880920b 100644 --- a/internal/cli/cmd/clusterversion/clusterversion_test.go +++ b/internal/cli/cmd/clusterversion/clusterversion_test.go @@ -1,36 +1,72 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterversion import ( + "bytes" + "fmt" + "net/http" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" ) var _ = Describe("clusterversion", func() { - const namespace = "test" var streams genericclioptions.IOStreams var tf *cmdtesting.TestFactory + out := new(bytes.Buffer) + var CreateTime string + mockClient := func(data runtime.Object) *cmdtesting.TestFactory { + tf := testing.NewTestFactory(testing.Namespace) + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &clientfake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, data)}, + } + tf.Client = tf.UnstructuredClient + tf.FakeDynamicClient = testing.FakeDynamicClient(data) + return tf + } BeforeEach(func() { - streams, _, _, _ = genericclioptions.NewTestIOStreams() - tf = cmdtesting.NewTestFactory().WithNamespace(namespace) + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + _ = metav1.AddMetaToScheme(scheme.Scheme) + streams, _, out, _ = genericclioptions.NewTestIOStreams() + fakeCV := testing.FakeClusterVersion() + CreateTime = util.TimeFormat(&fakeCV.CreationTimestamp) + tf = mockClient(fakeCV) }) AfterEach(func() { @@ -47,4 +83,11 @@ var _ = Describe("clusterversion", func() { cmd := NewListCmd(tf, streams) Expect(cmd).ShouldNot(BeNil()) }) + + It("list --cluster-definition", func() { + cmd := NewListCmd(tf, streams) + cmd.Run(cmd, []string{"--cluster-definition=" + testing.ClusterDefName}) + expected := fmt.Sprintf("NAME CLUSTER-DEFINITION STATUS IS-DEFAULT CREATED-TIME \nfake-cluster-version fake-cluster-definition false %s \n", CreateTime) + Expect(expected).Should(Equal(out.String())) + }) }) diff --git a/internal/cli/cmd/clusterversion/set_default.go b/internal/cli/cmd/clusterversion/set_default.go new file mode 100644 index 000000000..35c016eb7 --- /dev/null +++ b/internal/cli/cmd/clusterversion/set_default.go @@ -0,0 +1,196 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package clusterversion + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + apitypes "k8s.io/apimachinery/pkg/types" + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/constant" +) + +var ( + setDefaultExample = templates.Examples(` + # set ac-mysql-8.0.30 as the default clusterversion + kbcli clusterversion set-default ac-mysql-8.0.30`, + ) + + unsetDefaultExample = templates.Examples(` + # unset ac-mysql-8.0.30 to default clusterversion if it's default + kbcli clusterversion unset-default ac-mysql-8.0.30`) + + clusterVersionGVR = types.ClusterVersionGVR() +) + +const ( + annotationTrueValue = "true" + annotationFalseValue = "false" +) + +type SetOrUnsetDefaultOption struct { + Factory cmdutil.Factory + IOStreams genericclioptions.IOStreams + // `set-default` sets the setDefault to true, `unset-default` sets to false + setDefault bool +} + +func newSetOrUnsetDefaultOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, toSet bool) *SetOrUnsetDefaultOption { + return &SetOrUnsetDefaultOption{ + Factory: f, + IOStreams: streams, + setDefault: toSet, + } +} + +func newSetDefaultCMD(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := newSetOrUnsetDefaultOptions(f, streams, true) + cmd := &cobra.Command{ + Use: "set-default NAME", + Short: "Set the clusterversion to the default clusterversion for its clusterdefinition.", + Example: setDefaultExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, clusterVersionGVR), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.validate(args)) + util.CheckErr(o.run(args)) + }, + } + return cmd +} + +func newUnSetDefaultCMD(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := newSetOrUnsetDefaultOptions(f, streams, false) + cmd := &cobra.Command{ + Use: "unset-default NAME", + Short: "Unset the clusterversion if it's default.", + Example: unsetDefaultExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, clusterVersionGVR), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.validate(args)) + util.CheckErr(o.run(args)) + }, + } + return cmd +} + +func (o *SetOrUnsetDefaultOption) run(args []string) error { + client, err := o.Factory.DynamicClient() + if err != nil { + return err + } + var allErrs []error + // unset-default logic + if !o.setDefault { + for _, cv := range args { + if err := patchDefaultClusterVersionAnnotations(client, cv, annotationFalseValue); err != nil { + allErrs = append(allErrs, err) + } + } + return utilerrors.NewAggregate(allErrs) + } + // set-default logic + cv2Cd, cd2DefaultCv, err := getMapsBetweenCvAndCd(client) + if err != nil { + return err + } + // alreadySet is to mark if two input args have the same clusterdefintion + alreadySet := make(map[string]string) + for _, cv := range args { + cd, ok := cv2Cd[cv] + if !ok { + allErrs = append(allErrs, fmt.Errorf("cluterversion \"%s\" not found", cv)) + continue + } + if _, ok := cd2DefaultCv[cd]; ok && cv != cd2DefaultCv[cd] { + allErrs = append(allErrs, fmt.Errorf("clusterdefinition \"%s\" already has a default cluster version \"%s\"", cv2Cd[cv], cd2DefaultCv[cd])) + continue + } + if _, ok := alreadySet[cd]; ok { + allErrs = append(allErrs, fmt.Errorf("\"%s\" has the same clusterdefinition with \"%s\"", cv, alreadySet[cd])) + continue + } + if err := patchDefaultClusterVersionAnnotations(client, cv, annotationTrueValue); err != nil { + allErrs = append(allErrs, err) + continue + } + alreadySet[cd] = cv + } + return utilerrors.NewAggregate(allErrs) +} + +func (o *SetOrUnsetDefaultOption) validate(args []string) error { + if len(args) == 0 { + return fmt.Errorf("clusterversion name should be specified, run \"kbcli clusterversion list\" to list the clusterversions") + } + return nil +} + +// patchDefaultClusterVersionAnnotations patches the Annotations for the clusterversion in K8S +func patchDefaultClusterVersionAnnotations(client dynamic.Interface, cvName string, value string) error { + patchData := map[string]interface{}{ + "metadata": map[string]interface{}{ + "annotations": map[string]interface{}{ + constant.DefaultClusterVersionAnnotationKey: value, + }, + }, + } + patchBytes, _ := json.Marshal(patchData) + _, err := client.Resource(clusterVersionGVR).Patch(context.Background(), cvName, apitypes.MergePatchType, patchBytes, metav1.PatchOptions{}) + return err +} + +func getMapsBetweenCvAndCd(client dynamic.Interface) (map[string]string, map[string]string, error) { + lists, err := client.Resource(clusterVersionGVR).List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, nil, err + } + cvToCd := make(map[string]string) + cdToDefaultCv := make(map[string]string) + for _, item := range lists.Items { + name := item.GetName() + annotations := item.GetAnnotations() + labels := item.GetLabels() + if labels == nil { + continue + } + if _, ok := labels[constant.ClusterDefLabelKey]; !ok { + continue + } + cvToCd[name] = labels[constant.ClusterDefLabelKey] + if annotations == nil { + continue + } + if annotations[constant.DefaultClusterVersionAnnotationKey] == annotationTrueValue { + cdToDefaultCv[labels[constant.ClusterDefLabelKey]] = name + } + } + return cvToCd, cdToDefaultCv, nil +} diff --git a/internal/cli/cmd/clusterversion/set_default_test.go b/internal/cli/cmd/clusterversion/set_default_test.go new file mode 100644 index 000000000..ebaec8c7e --- /dev/null +++ b/internal/cli/cmd/clusterversion/set_default_test.go @@ -0,0 +1,194 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package clusterversion + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes/scheme" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/constant" +) + +var _ = Describe("set-default", func() { + var streams genericclioptions.IOStreams + var tf *cmdtesting.TestFactory + + const ( + cluterversion = testing.ClusterVersionName + clusterversionInSameCD = testing.ClusterVersionName + "-sameCD" + ClusterversionOtherCD = testing.ClusterVersionName + "-other" + errorClusterversion = "08jfa2" + ) + + beginWithMultipleClusterversion := func() { + tf.FakeDynamicClient = testing.FakeDynamicClient([]runtime.Object{ + &appsv1alpha1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: cluterversion, + Labels: map[string]string{ + constant.ClusterDefLabelKey: testing.ClusterDefName, + }, + }, + }, + &appsv1alpha1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: clusterversionInSameCD, + Labels: map[string]string{ + constant.ClusterDefLabelKey: testing.ClusterDefName, + }, + }, + }, + &appsv1alpha1.ClusterVersion{ + ObjectMeta: metav1.ObjectMeta{ + Name: ClusterversionOtherCD, + Labels: map[string]string{ + constant.ClusterDefLabelKey: testing.ClusterDefName + "-other", + }, + }, + }, + }...) + } + + getFakeClusterVersion := func(dynamic dynamic.Interface, clusterVersionName string) (*appsv1alpha1.ClusterVersion, error) { + var cv appsv1alpha1.ClusterVersion + u, err := dynamic.Resource(clusterVersionGVR).Get(context.Background(), clusterVersionName, metav1.GetOptions{}) + if err != nil { + return nil, err + } + err = runtime.DefaultUnstructuredConverter.FromUnstructured(u.Object, &cv) + if err != nil { + return nil, err + } + return &cv, nil + } + + var validateSetOrUnsetResult func(needToChecks []string, value []string) + validateSetOrUnsetResult = func(needToChecks []string, value []string) { + if len(needToChecks) == 0 { + return + } + cv, err := getFakeClusterVersion(tf.FakeDynamicClient, needToChecks[0]) + Expect(err).Should(Succeed()) + Expect(isDefault(cv)).Should(Equal(value[0])) + validateSetOrUnsetResult(needToChecks[1:], value[1:]) + } + + BeforeEach(func() { + _ = appsv1alpha1.AddToScheme(scheme.Scheme) + _ = metav1.AddMetaToScheme(scheme.Scheme) + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = testing.NewTestFactory(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + beginWithMultipleClusterversion() + }) + + It("test isDefault Func", func() { + cv := testing.FakeClusterVersion() + Expect(isDefault(cv)).Should(Equal(annotationFalseValue)) + cv.SetAnnotations(map[string]string{ + constant.DefaultClusterVersionAnnotationKey: annotationFalseValue, + }) + Expect(isDefault(cv)).Should(Equal(annotationFalseValue)) + cv.Annotations[constant.DefaultClusterVersionAnnotationKey] = annotationTrueValue + Expect(isDefault(cv)).Should(Equal(annotationTrueValue)) + }) + + It("set-default cmd", func() { + cmd := newSetDefaultCMD(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + + It("unset-default cmd", func() { + cmd := newUnSetDefaultCMD(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + + It("set-default empty args", func() { + o := newSetOrUnsetDefaultOptions(tf, streams, true) + Expect(o.validate([]string{})).Should(HaveOccurred()) + }) + + It("set-default error args", func() { + o := newSetOrUnsetDefaultOptions(tf, streams, true) + Expect(o.run([]string{errorClusterversion})).Should(HaveOccurred()) + }) + + It("unset-default empty args", func() { + o := newSetOrUnsetDefaultOptions(tf, streams, false) + Expect(o.validate([]string{})).Should(HaveOccurred()) + }) + + It("unset-default error args", func() { + o := newSetOrUnsetDefaultOptions(tf, streams, false) + Expect(o.run([]string{errorClusterversion})).Should(HaveOccurred()) + }) + + It("set-default and unset-default", func() { + // before set-default + validateSetOrUnsetResult([]string{cluterversion}, []string{annotationFalseValue}) + // set-default + cmd := newSetDefaultCMD(tf, streams) + cmd.Run(cmd, []string{cluterversion}) + validateSetOrUnsetResult([]string{cluterversion}, []string{annotationTrueValue}) + // unset-default + cmd = newUnSetDefaultCMD(tf, streams) + cmd.Run(cmd, []string{cluterversion}) + validateSetOrUnsetResult([]string{cluterversion}, []string{annotationFalseValue}) + }) + + It("the clusterDef already has a default cv when set-default", func() { + cmd := newSetDefaultCMD(tf, streams) + cmd.Run(cmd, []string{cluterversion}) + validateSetOrUnsetResult([]string{cluterversion, clusterversionInSameCD}, []string{annotationTrueValue, annotationFalseValue}) + o := newSetOrUnsetDefaultOptions(tf, streams, true) + err := o.run([]string{clusterversionInSameCD}) + Expect(err).Should(HaveOccurred()) + }) + + It("set-default args belonging to the same cd", func() { + o := newSetOrUnsetDefaultOptions(tf, streams, true) + err := o.run([]string{cluterversion, cluterversion}) + Expect(err).Should(HaveOccurred()) + }) + + It("set-default and unset-default multiple args", func() { + cmd := newSetDefaultCMD(tf, streams) + validateSetOrUnsetResult([]string{cluterversion, ClusterversionOtherCD}, []string{annotationFalseValue, annotationFalseValue}) + // set-default + cmd.Run(cmd, []string{cluterversion, ClusterversionOtherCD}) + validateSetOrUnsetResult([]string{cluterversion, ClusterversionOtherCD}, []string{annotationTrueValue, annotationTrueValue}) + // unset-defautl + cmd = newUnSetDefaultCMD(tf, streams) + cmd.Run(cmd, []string{cluterversion, ClusterversionOtherCD}) + validateSetOrUnsetResult([]string{cluterversion, ClusterversionOtherCD}, []string{annotationFalseValue, annotationFalseValue}) + }) +}) diff --git a/internal/cli/cmd/clusterversion/suite_test.go b/internal/cli/cmd/clusterversion/suite_test.go index 3a569110f..32a4136e3 100644 --- a/internal/cli/cmd/clusterversion/suite_test.go +++ b/internal/cli/cmd/clusterversion/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package clusterversion diff --git a/internal/cli/cmd/dashboard/dashboard.go b/internal/cli/cmd/dashboard/dashboard.go index feb0ae1c4..773a8b908 100644 --- a/internal/cli/cmd/dashboard/dashboard.go +++ b/internal/cli/cmd/dashboard/dashboard.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dashboard @@ -212,7 +215,7 @@ func newOpenCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.C cmd.Flags().StringVar(&o.localPort, "port", "", "dashboard local port") cmd.Flags().Duration(podRunningTimeoutFlag, defaultPodExecTimeout, - "The length of time (like 5s, 2m, or 3h, higher than zero) to wait until at least one pod is running") + "The time (like 5s, 2m, or 3h, higher than zero) to wait for at least one pod is running") return cmd } diff --git a/internal/cli/cmd/dashboard/dashboard_test.go b/internal/cli/cmd/dashboard/dashboard_test.go index 050430a0b..f0dcf960c 100644 --- a/internal/cli/cmd/dashboard/dashboard_test.go +++ b/internal/cli/cmd/dashboard/dashboard_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dashboard diff --git a/internal/cli/cmd/dashboard/suite_test.go b/internal/cli/cmd/dashboard/suite_test.go index fa1d7380c..a42645afd 100644 --- a/internal/cli/cmd/dashboard/suite_test.go +++ b/internal/cli/cmd/dashboard/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package dashboard diff --git a/internal/cli/cmd/fault/fault.go b/internal/cli/cmd/fault/fault.go new file mode 100644 index 000000000..63b44813b --- /dev/null +++ b/internal/cli/cmd/fault/fault.go @@ -0,0 +1,162 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "fmt" + "regexp" + "strconv" + "strings" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type Selector struct { + PodNameSelectors map[string][]string `json:"pods"` + + NamespaceSelectors []string `json:"namespaces"` + + LabelSelectors map[string]string `json:"labelSelectors"` + + PodPhaseSelectors []string `json:"podPhaseSelectors"` + + NodeLabelSelectors map[string]string `json:"nodeSelectors"` + + AnnotationSelectors map[string]string `json:"annotationSelectors"` + + NodeNameSelectors []string `json:"nodes"` +} + +type FaultBaseOptions struct { + Action string `json:"action"` + + Mode string `json:"mode"` + + Value string `json:"value"` + + Duration string `json:"duration"` + + Selector `json:"selector"` + + create.CreateOptions `json:"-"` +} + +func NewFaultCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "fault", + Short: "Inject faults to pod.", + } + cmd.AddCommand( + NewPodChaosCmd(f, streams), + NewNetworkChaosCmd(f, streams), + NewTimeChaosCmd(f, streams), + NewIOChaosCmd(f, streams), + NewStressChaosCmd(f, streams), + NewNodeChaosCmd(f, streams), + ) + return cmd +} + +func registerFlagCompletionFunc(cmd *cobra.Command, f cmdutil.Factory) { + var formatsWithDesc = map[string]string{ + "JSON": "Output result in JSON format", + "YAML": "Output result in YAML format", + } + util.CheckErr(cmd.RegisterFlagCompletionFunc("output", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + for format, desc := range formatsWithDesc { + if strings.HasPrefix(format, toComplete) { + names = append(names, fmt.Sprintf("%s\t%s", format, desc)) + } + } + return names, cobra.ShellCompDirectiveNoFileComp + })) +} + +func (o *FaultBaseOptions) AddCommonFlag(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.Mode, "mode", "all", `You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with.`) + cmd.Flags().StringVar(&o.Value, "value", "", `If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject.`) + cmd.Flags().StringVar(&o.Duration, "duration", "10s", "Supported formats of the duration are: ms / s / m / h.") + cmd.Flags().StringToStringVar(&o.LabelSelectors, "label", map[string]string{}, `label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0.`) + cmd.Flags().StringArrayVar(&o.NamespaceSelectors, "ns-fault", []string{"default"}, `Specifies the namespace into which you want to inject faults.`) + cmd.Flags().StringArrayVar(&o.PodPhaseSelectors, "phase", []string{}, `Specify the pod that injects the fault by the state of the pod.`) + cmd.Flags().StringToStringVar(&o.NodeLabelSelectors, "node-label", map[string]string{}, `label for node, such as '"kubernetes.io/arch=arm64,kubernetes.io/hostname=minikube-m03,kubernetes.io/os=linux.`) + cmd.Flags().StringArrayVar(&o.NodeNameSelectors, "node", []string{}, `Inject faults into pods in the specified node.`) + cmd.Flags().StringToStringVar(&o.AnnotationSelectors, "annotation", map[string]string{}, `Select the pod to inject the fault according to Annotation.`) + cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`) + cmd.Flags().Lookup("dry-run").NoOptDefVal = Unchanged + + printer.AddOutputFlagForCreate(cmd, &o.Format) +} + +func (o *FaultBaseOptions) BaseValidate() error { + if ok, err := IsRegularMatch(o.Duration); !ok { + return err + } + + if o.Value == "" && (o.Mode == "fixed" || o.Mode == "fixed-percent" || o.Mode == "random-max-percent") { + return fmt.Errorf("you must use --value to specify an integer") + } + + if ok, err := IsInteger(o.Value); !ok { + return err + } + + return nil +} + +func (o *FaultBaseOptions) BaseComplete() error { + if len(o.Args) > 0 { + o.PodNameSelectors = make(map[string][]string, len(o.NamespaceSelectors)) + for _, ns := range o.NamespaceSelectors { + o.PodNameSelectors[ns] = o.Args + } + } + return nil +} + +func IsRegularMatch(str string) (bool, error) { + pattern := regexp.MustCompile(`^\d+(ms|s|m|h)$`) + if str != "" && !pattern.MatchString(str) { + return false, fmt.Errorf("invalid duration:%s; input format must be in the form of number + time unit, like 10s, 10m", str) + } else { + return true, nil + } +} + +func IsInteger(str string) (bool, error) { + if _, err := strconv.Atoi(str); str != "" && err != nil { + return false, fmt.Errorf("invalid value:%s; must be an integer", str) + } else { + return true, nil + } +} + +func GetGVR(group, version, resourceName string) schema.GroupVersionResource { + return schema.GroupVersionResource{Group: group, Version: version, Resource: resourceName} +} diff --git a/internal/cli/cmd/fault/fault_constant.go b/internal/cli/cmd/fault/fault_constant.go new file mode 100644 index 000000000..77fb04158 --- /dev/null +++ b/internal/cli/cmd/fault/fault_constant.go @@ -0,0 +1,143 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" + +// Unchanged DryRun flag +const ( + Unchanged = "unchanged" +) + +// GVR +const ( + Group = "chaos-mesh.org" + Version = "v1alpha1" + + ResourcePodChaos = "podchaos" + ResourceNetworkChaos = "networkchaos" + ResourceDNSChaos = "dnschaos" + ResourceHTTPChaos = "httpchaos" + ResourceIOChaos = "iochaos" + ResourceStressChaos = "stresschaos" + ResourceTimeChaos = "timechaos" + ResourceAWSChaos = "awschaos" + ResourceGCPChaos = "gcpchaos" + + KindAWSChaos = "AWSChaos" + KindGCPChaos = "GCPChaos" +) + +// Cue Template Name +const ( + CueTemplatePodChaos = "pod_chaos_template.cue" + CueTemplateNetworkChaos = "network_chaos_template.cue" + CueTemplateDNSChaos = "dns_chaos_template.cue" + CueTemplateHTTPChaos = "http_chaos_template.cue" + CueTemplateIOChaos = "io_chaos_template.cue" + CueTemplateStressChaos = "stress_chaos_template.cue" + CueTemplateTimeChaos = "time_chaos_template.cue" + CueTemplateNodeChaos = "node_chaos_template.cue" +) + +// Pod Chaos Command +const ( + Kill = "kill" + KillShort = "kill pod" + Failure = "failure" + FailureShort = "failure pod" + KillContainer = "kill-container" + KillContainerShort = "kill containers" +) + +// NetWork Chaos Command +const ( + Partition = "partition" + PartitionShort = "Make a pod network partitioned from other objects." + Loss = "loss" + LossShort = "Cause pods to communicate with other objects to drop packets." + Delay = "delay" + DelayShort = "Make pods communicate with other objects lazily." + Duplicate = "duplicate" + DuplicateShort = "Make pods communicate with other objects to pick up duplicate packets." + Corrupt = "corrupt" + CorruptShort = "Distorts the messages a pod communicates with other objects." + Bandwidth = "bandwidth" + BandwidthShort = "Limit the bandwidth that pods use to communicate with other objects." +) + +// DNS Chaos Command +const ( + Random = "random" + RandomShort = "Make DNS return any IP when resolving external domain names." + Error = "error" + ErrorShort = "Make DNS return an error when resolving external domain names." +) + +// HTTP Chaos Command +const ( + Abort = "abort" + AbortShort = "Abort the HTTP request and response." + HTTPDelay = "delay" + HTTPDelayShort = "Delay the HTTP request and response." + Replace = "replace" + ReplaceShort = "Replace the HTTP request and response." + Patch = "patch" + PatchShort = "Patch the HTTP request and response." +) + +// IO Chaos Command +const ( + Latency = "latency" + LatencyShort = "Delayed IO operations." + Errno = "errno" + ErrnoShort = "Causes IO operations to return specific errors." + Attribute = "attribute" + AttributeShort = "Override the attributes of the file." + Mistake = "mistake" + MistakeShort = "Alters the contents of the file, distorting the contents of the file." +) + +// Stress Chaos Command +const ( + Stress = "stress" + StressShort = "Add memory pressure or CPU load to the system." +) + +// Time Chaos Command +const ( + Time = "time" + TimeShort = "Clock skew failure." +) + +// Node Chaos Command +const ( + Stop = "stop" + StopShort = "Stop instance" + Restart = "restart" + RestartShort = "Restart instance" + DetachVolume = "detach-volume" + DetachVolumeShort = "Detach volume" + + AWSSecretName = "cloud-key-secret-aws" + GCPSecretName = "cloud-key-secret-gcp" +) + +var supportedCloudProviders = []string{cp.AWS, cp.GCP} diff --git a/internal/cli/cmd/fault/fault_dns.go b/internal/cli/cmd/fault/fault_dns.go new file mode 100644 index 000000000..81ae14010 --- /dev/null +++ b/internal/cli/cmd/fault/fault_dns.go @@ -0,0 +1,142 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var faultDNSExample = templates.Examples(` + // Inject DNS faults into all pods under the default namespace, so that any IP is returned when accessing the bing.com domain name. + kbcli fault dns random --patterns=bing.com --duration=1m + + // Inject DNS faults into all pods under the default namespace, so that error is returned when accessing the bing.com domain name. + kbcli fault dns error --patterns=bing.com --duration=1m +`) + +type DNSChaosOptions struct { + Patterns []string `json:"patterns"` + + FaultBaseOptions +} + +func NewDNSChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *DNSChaosOptions { + o := &DNSChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateDNSChaos, + GVR: GetGVR(Group, Version, ResourceDNSChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewDNSChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "dns", + Short: "Inject faults into DNS server.", + } + cmd.AddCommand( + NewRandomCmd(f, streams), + NewErrorCmd(f, streams), + ) + return cmd +} + +func NewRandomCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewDNSChaosOptions(f, streams, string(v1alpha1.RandomAction)) + cmd := o.NewCobraCommand(Random, RandomShort) + + o.AddCommonFlag(cmd) + util.CheckErr(cmd.MarkFlagRequired("patterns")) + + return cmd +} + +func NewErrorCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewDNSChaosOptions(f, streams, string(v1alpha1.ErrorAction)) + cmd := o.NewCobraCommand(Error, ErrorShort) + + o.AddCommonFlag(cmd) + util.CheckErr(cmd.MarkFlagRequired("patterns")) + + return cmd +} + +func (o *DNSChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultDNSExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *DNSChaosOptions) AddCommonFlag(cmd *cobra.Command) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().StringArrayVar(&o.Patterns, "patterns", nil, `Select the domain name template that matching the failure behavior & supporting placeholders ? and wildcards *.`) + + // register flag completion func + registerFlagCompletionFunc(cmd, o.Factory) +} + +func (o *DNSChaosOptions) Validate() error { + return o.BaseValidate() +} + +func (o *DNSChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *DNSChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.DNSChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_dns_test.go b/internal/cli/cmd/fault/fault_dns_test.go new file mode 100644 index 000000000..d3b0029d9 --- /dev/null +++ b/internal/cli/cmd/fault/fault_dns_test.go @@ -0,0 +1,93 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Network DNS", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + Context("test fault network dns", func() { + + It("fault network dns random", func() { + inputs := [][]string{ + {"--dry-run=client", "--patterns=kubeblocks.io"}, + {"--mode=one", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--ns-fault=kb-system", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--node=minikube-m02", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--patterns=kubeblocks.io", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--patterns=kubeblocks.io", "--dry-run=client"}, + } + o := NewDNSChaosOptions(tf, streams, string(v1alpha1.RandomAction)) + cmd := o.NewCobraCommand(Random, RandomShort) + o.AddCommonFlag(cmd) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network dns error", func() { + inputs := [][]string{ + {"--patterns=kubeblocks.io", "--dry-run=client"}, + } + o := NewDNSChaosOptions(tf, streams, string(v1alpha1.ErrorAction)) + cmd := o.NewCobraCommand(Error, ErrorShort) + o.AddCommonFlag(cmd) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_http.go b/internal/cli/cmd/fault/fault_http.go new file mode 100644 index 000000000..13f6b999a --- /dev/null +++ b/internal/cli/cmd/fault/fault_http.go @@ -0,0 +1,243 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "fmt" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/json" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" +) + +var faultHTTPExample = templates.Examples(` + # By default, the method of GET from port 80 is blocked. + kbcli fault network http abort --duration=1m + + # Block the method of GET from port 4399. + kbcli fault network http abort --port=4399 --duration=1m + + # Block the method of POST from port 4399. + kbcli fault network http abort --port=4399 --method=POST --duration=1m + + # Delays post requests from port 4399. + kbcli fault network http delay --port=4399 --method=POST --delay=15s + + # Replace the GET method sent from port 80 with the PUT method. + kbcli fault network http replace --replace-method=PUT --duration=1m + + # Replace the GET method sent from port 80 with the PUT method, and replace the request body. + kbcli fault network http replace --body="you are good luck" --replace-method=PUT --duration=2m + + # Replace the response content "you" from port 80. + kbcli fault network http replace --target=Response --body=you --duration=30s + + # Append content to the body of the post request sent from port 4399, in JSON format. + kbcli fault network http patch --method=POST --port=4399 --body="you are good luck" --type=JSON --duration=30s +`) + +type HTTPReplace struct { + ReplaceBody []byte `json:"body,omitempty"` + InputReplaceBody string `json:"-"` + ReplacePath string `json:"path,omitempty"` + ReplaceMethod string `json:"method,omitempty"` +} + +type HTTPPatch struct { + HTTPPatchBody `json:"body,omitempty"` +} + +type HTTPPatchBody struct { + PatchBodyValue string `json:"value,omitempty"` + PatchBodyType string `json:"type,omitempty"` +} + +type HTTPChaosOptions struct { + Target string `json:"target"` + Port int32 `json:"port"` + Path string `json:"path"` + Method string `json:"method"` + Code int32 `json:"code,omitempty"` + + // abort command + Abort bool `json:"abort,omitempty"` + // delay command + Delay string `json:"delay,omitempty"` + // replace command + HTTPReplace `json:"replace,omitempty"` + // patch command + HTTPPatch `json:"patch,omitempty"` + + FaultBaseOptions +} + +func NewHTTPChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *HTTPChaosOptions { + o := &HTTPChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateHTTPChaos, + GVR: GetGVR(Group, Version, ResourceHTTPChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewHTTPChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "http", + Short: "Intercept HTTP requests and responses.", + } + cmd.AddCommand( + NewAbortCmd(f, streams), + NewHTTPDelayCmd(f, streams), + NewReplaceCmd(f, streams), + NewPatchCmd(f, streams), + ) + return cmd +} + +func NewAbortCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewHTTPChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(Abort, AbortShort) + + o.AddCommonFlag(cmd) + cmd.Flags().BoolVar(&o.Abort, "abort", true, `Indicates whether to inject the fault that interrupts the connection.`) + + return cmd +} + +func NewHTTPDelayCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewHTTPChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(HTTPDelay, HTTPDelayShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Delay, "delay", "10s", `The time for delay.`) + + return cmd +} + +func NewReplaceCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewHTTPChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(Replace, ReplaceShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.InputReplaceBody, "body", "", `The content of the request body or response body to replace the failure.`) + cmd.Flags().StringVar(&o.ReplacePath, "replace-path", "", `The URI path used to replace content.`) + cmd.Flags().StringVar(&o.ReplaceMethod, "replace-method", "", `The replaced content of the HTTP request method.`) + + return cmd +} + +func NewPatchCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewHTTPChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(Patch, PatchShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.PatchBodyValue, "body", "", `The fault of the request body or response body with patch faults.`) + cmd.Flags().StringVar(&o.PatchBodyType, "type", "", `The type of patch faults of the request body or response body. Currently, it only supports JSON.`) + + return cmd +} + +func (o *HTTPChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultHTTPExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *HTTPChaosOptions) AddCommonFlag(cmd *cobra.Command) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().StringVar(&o.Target, "target", "Request", `Specifies whether the target of fault injection is Request or Response. The target-related fields should be configured at the same time.`) + cmd.Flags().Int32Var(&o.Port, "port", 80, `The TCP port that the target service listens on.`) + cmd.Flags().StringVar(&o.Path, "path", "*", `The URI path of the target request. Supports Matching wildcards.`) + cmd.Flags().StringVar(&o.Method, "method", "GET", `The HTTP method of the target request method. For example: GET, POST, PUT, DELETE, HEAD, OPTIONS, PATCH.`) + cmd.Flags().Int32Var(&o.Code, "code", 0, `The status code responded by target.`) + + // register flag completion func + registerFlagCompletionFunc(cmd, o.Factory) +} + +func (o *HTTPChaosOptions) Validate() error { + if o.PatchBodyType != "" && o.PatchBodyType != "JSON" { + return fmt.Errorf("--type only supports JSON") + } + if o.PatchBodyValue != "" && o.PatchBodyType == "" { + return fmt.Errorf("--type is required when --body is specified") + } + if o.PatchBodyType != "" && o.PatchBodyValue == "" { + return fmt.Errorf("--body is required when --type is specified") + } + + var msg interface{} + if o.PatchBodyValue != "" && json.Unmarshal([]byte(o.PatchBodyValue), &msg) != nil { + return fmt.Errorf("--body is not a valid JSON") + } + + if o.Target == "Request" && o.Code != 0 { + return fmt.Errorf("--code is only supported when --target=Response") + } + + if ok, err := IsRegularMatch(o.Delay); !ok { + return err + } + return o.BaseValidate() +} + +func (o *HTTPChaosOptions) Complete() error { + o.ReplaceBody = []byte(o.InputReplaceBody) + return o.BaseComplete() +} + +func (o *HTTPChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.HTTPChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_http_test.go b/internal/cli/cmd/fault/fault_http_test.go new file mode 100644 index 000000000..3a7b44b7d --- /dev/null +++ b/internal/cli/cmd/fault/fault_http_test.go @@ -0,0 +1,139 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Network HTPP", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + Context("test fault network http", func() { + It("fault network http abort", func() { + inputs := [][]string{ + {"--dry-run=client"}, + {"--mode=one", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--dry-run=client"}, + {"--ns-fault=kb-system", "--dry-run=client"}, + {"--node=minikube-m02", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--dry-run=client"}, + {"--abort=true", "--dry-run=client"}, + } + o := NewHTTPChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Abort, AbortShort) + o.AddCommonFlag(cmd) + cmd.Flags().BoolVar(&o.Abort, "abort", true, `Indicates whether to inject the fault that interrupts the connection.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network http delay", func() { + inputs := [][]string{ + {""}, + {"--delay=50s", "--dry-run=client"}, + } + o := NewHTTPChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Delay, DelayShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Delay, "delay", "10s", `The time for delay.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network http replace", func() { + inputs := [][]string{ + {"--dry-run=client"}, + {"--replace-method=PUT", "--body=\"you are good luck\"", "--replace-path=/local/", "--duration=1m", "--dry-run=client"}, + {"--target=Response", "--replace-method=PUT", "--body=you", "--replace-path=/local/", "--duration=1m", "--dry-run=client"}, + } + o := NewHTTPChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Replace, ReplaceShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.InputReplaceBody, "body", "", `The content of the request body or response body to replace the failure.`) + cmd.Flags().StringVar(&o.ReplacePath, "replace-path", "", `The URI path used to replace content.`) + cmd.Flags().StringVar(&o.ReplaceMethod, "replace-method", "", `The replaced content of the HTTP request method.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network http patch", func() { + inputs := [][]string{ + {"--dry-run=client"}, + {"--body=\"you are good luck\"", "--type=JSON", "--duration=1m", "--dry-run=client"}, + {"--target=Response", "--body=\"you are good luck\"", "--type=JSON", "--duration=1m", "--dry-run=client"}, + } + o := NewHTTPChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Patch, PatchShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.PatchBodyValue, "body", "", `The fault of the request body or response body with patch faults.`) + cmd.Flags().StringVar(&o.PatchBodyType, "type", "", `The type of patch faults of the request body or response body. Currently, it only supports JSON.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_io.go b/internal/cli/cmd/fault/fault_io.go new file mode 100644 index 000000000..5431e1780 --- /dev/null +++ b/internal/cli/cmd/fault/fault_io.go @@ -0,0 +1,231 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var faultIOExample = templates.Examples(` + # Affects the first container in default namespace's all pods. Delay all IO operations under the /data path by 10s. + kbcli fault io latency --delay=10s --volume-path=/data + + # Affects the first container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data + + # Affects the mysql container in mycluster-mysql-0 pod. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data -c=mysql + + # There is a 50% probability of affecting the read IO operation of the test.txt file under the /data path. + kbcli fault io latency mycluster-mysql-0 --delay=10s --volume-path=/data --path=test.txt --percent=50 --method=READ -c=mysql + + # Same as above.Make all IO operations under the /data path return the specified error number 22 (Invalid argument). + kbcli fault io errno --volume-path=/data --errno=22 + + # Same as above.Modify the IO operation permission attribute of the files under the /data path to 72.(110 in octal). + kbcli fault io attribute --volume-path=/data --perm=72 + + # Modify all files so that random positions of 1's with a maximum length of 10 bytes will be replaced with 0's. + kbcli fault io mistake --volume-path=/data --filling=zero --max-occurrences=10 --max-length=1 +`) + +type IOAttribute struct { + Ino uint64 `json:"ino,omitempty"` + Size uint64 `json:"size,omitempty"` + Blocks uint64 `json:"blocks,omitempty"` + Perm uint16 `json:"perm,omitempty"` + Nlink uint32 `json:"nlink,omitempty"` + UID uint32 `json:"uid,omitempty"` + GID uint32 `json:"gid,omitempty"` +} + +type IOMistake struct { + Filling string `json:"filling,omitempty"` + MaxOccurrences int `json:"maxOccurrences,omitempty"` + MaxLength int `json:"maxLength,omitempty"` +} + +type IOChaosOptions struct { + // Parameters required by the `latency` command. + Delay string `json:"delay"` + + // Parameters required by the `fault` command. + Errno int `json:"errno"` + + // Parameters required by the `attribute` command. + IOAttribute `json:"attr,omitempty"` + + // Parameters required by the `mistake` command. + IOMistake `json:"mistake,omitempty"` + + VolumePath string `json:"volumePath"` + Path string `json:"path"` + Percent int `json:"percent"` + Methods []string `json:"methods,omitempty"` + ContainerNames []string `json:"containerNames,omitempty"` + + FaultBaseOptions +} + +func NewIOChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "io", + Short: "IO chaos.", + } + cmd.AddCommand( + NewIOLatencyCmd(f, streams), + NewIOFaultCmd(f, streams), + NewIOAttributeOverrideCmd(f, streams), + NewIOMistakeCmd(f, streams), + ) + return cmd +} + +func NewIOChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *IOChaosOptions { + o := &IOChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateIOChaos, + GVR: GetGVR(Group, Version, ResourceIOChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewIOLatencyCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewIOChaosOptions(f, streams, string(v1alpha1.IoLatency)) + cmd := o.NewCobraCommand(Latency, LatencyShort) + + o.AddCommonFlag(cmd, f) + cmd.Flags().StringVar(&o.Delay, "delay", "", `Specific delay time.`) + + util.CheckErr(cmd.MarkFlagRequired("delay")) + return cmd +} + +func NewIOFaultCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewIOChaosOptions(f, streams, string(v1alpha1.IoFaults)) + cmd := o.NewCobraCommand(Errno, ErrnoShort) + + o.AddCommonFlag(cmd, f) + cmd.Flags().IntVar(&o.Errno, "errno", 0, `The returned error number.`) + + util.CheckErr(cmd.MarkFlagRequired("errno")) + return cmd +} + +func NewIOAttributeOverrideCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewIOChaosOptions(f, streams, string(v1alpha1.IoAttrOverride)) + cmd := o.NewCobraCommand(Attribute, AttributeShort) + + o.AddCommonFlag(cmd, f) + cmd.Flags().Uint64Var(&o.Ino, "ino", 0, `ino number.`) + cmd.Flags().Uint64Var(&o.Size, "size", 0, `File size.`) + cmd.Flags().Uint64Var(&o.Blocks, "blocks", 0, `The number of blocks the file occupies.`) + cmd.Flags().Uint16Var(&o.Perm, "perm", 0, `Decimal representation of file permissions.`) + cmd.Flags().Uint32Var(&o.Nlink, "nlink", 0, `The number of hard links.`) + cmd.Flags().Uint32Var(&o.UID, "uid", 0, `Owner's user ID.`) + cmd.Flags().Uint32Var(&o.GID, "gid", 0, `The owner's group ID.`) + + return cmd +} + +func NewIOMistakeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewIOChaosOptions(f, streams, string(v1alpha1.IoMistake)) + cmd := o.NewCobraCommand(Mistake, MistakeShort) + + o.AddCommonFlag(cmd, f) + cmd.Flags().StringVar(&o.Filling, "filling", "", `The filling content of the error data can only be zero (filling with 0) or random (filling with random bytes).`) + cmd.Flags().IntVar(&o.MaxOccurrences, "max-occurrences", 1, `The maximum number of times an error can occur per operation.`) + cmd.Flags().IntVar(&o.MaxLength, "max-length", 1, `The maximum length (in bytes) of each error.`) + + util.CheckErr(cmd.MarkFlagRequired("filling")) + util.CheckErr(cmd.MarkFlagRequired("max-occurrences")) + util.CheckErr(cmd.MarkFlagRequired("max-length")) + + return cmd +} + +func (o *IOChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultIOExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *IOChaosOptions) AddCommonFlag(cmd *cobra.Command, f cmdutil.Factory) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().StringVar(&o.VolumePath, "volume-path", "", `The mount point of the volume in the target container must be the root directory of the mount.`) + cmd.Flags().StringVar(&o.Path, "path", "", `The effective scope of the injection error can be a wildcard or a single file.`) + cmd.Flags().IntVar(&o.Percent, "percent", 100, `Probability of failure per operation, in %.`) + cmd.Flags().StringArrayVar(&o.Methods, "method", nil, "The file system calls that need to inject faults. For example: WRITE READ") + cmd.Flags().StringArrayVarP(&o.ContainerNames, "container", "c", nil, "The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected.") + + util.CheckErr(cmd.MarkFlagRequired("volume-path")) + + // register flag completion func + registerFlagCompletionFunc(cmd, f) +} + +func (o *IOChaosOptions) Validate() error { + return o.BaseValidate() +} + +func (o *IOChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *IOChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.IOChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_io_test.go b/internal/cli/cmd/fault/fault_io_test.go new file mode 100644 index 000000000..6d4328694 --- /dev/null +++ b/internal/cli/cmd/fault/fault_io_test.go @@ -0,0 +1,146 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault IO", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + Context("test fault io", func() { + + It("fault io latency", func() { + inputs := [][]string{ + {"--mode=one", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--ns-fault=kb-system", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--node=minikube-m02", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--delay=10s", "--volume-path=/data", "--dry-run=client"}, + {"--delay=10s", "--volume-path=/data", "--path=test.txt", "--percent=50", "--method=READ", "-c=mysql", "--dry-run=client"}, + } + o := NewIOChaosOptions(tf, streams, string(v1alpha1.IoLatency)) + cmd := o.NewCobraCommand(Latency, LatencyShort) + o.AddCommonFlag(cmd, tf) + cmd.Flags().StringVar(&o.Delay, "delay", "", `Specific delay time.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault io errno", func() { + inputs := [][]string{ + {"--errno=22", "--volume-path=/data", "--dry-run=client"}, + {"--errno=22", "--volume-path=/data", "--path=test.txt", "--percent=50", "--method=READ", "--dry-run=client"}, + } + o := NewIOChaosOptions(tf, streams, string(v1alpha1.IoFaults)) + cmd := o.NewCobraCommand(Errno, ErrnoShort) + o.AddCommonFlag(cmd, tf) + cmd.Flags().IntVar(&o.Errno, "errno", 0, `The returned error number.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault io attribute", func() { + inputs := [][]string{ + {"--perm=72", "--size=72", "--blocks=72", "--nlink=72", "--ino=72", + "--uid=72", "--gid=72", "--volume-path=/data", "--dry-run=client"}, + } + o := NewIOChaosOptions(tf, streams, string(v1alpha1.IoAttrOverride)) + cmd := o.NewCobraCommand(Attribute, AttributeShort) + o.AddCommonFlag(cmd, tf) + cmd.Flags().Uint64Var(&o.Ino, "ino", 0, `ino number.`) + cmd.Flags().Uint64Var(&o.Size, "size", 0, `File size.`) + cmd.Flags().Uint64Var(&o.Blocks, "blocks", 0, `The number of blocks the file occupies.`) + cmd.Flags().Uint16Var(&o.Perm, "perm", 0, `Decimal representation of file permissions.`) + cmd.Flags().Uint32Var(&o.Nlink, "nlink", 0, `The number of hard links.`) + cmd.Flags().Uint32Var(&o.UID, "uid", 0, `Owner's user ID.`) + cmd.Flags().Uint32Var(&o.GID, "gid", 0, `The owner's group ID.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault io mistake", func() { + inputs := [][]string{ + {"--volume-path=/data", "--filling=zero", "--max-occurrences=10", "--max-length=1", "--dry-run=client"}, + {"--volume-path=/data", "--filling=random", "--max-occurrences=10", "--max-length=1", "--dry-run=client"}, + {"--volume-path=/data", "--filling=zero", "--max-occurrences=10", "--max-length=1", "--path=test.txt", "--percent=50", "--method=READ", "--dry-run=client"}, + } + o := NewIOChaosOptions(tf, streams, string(v1alpha1.IoMistake)) + cmd := o.NewCobraCommand(Mistake, MistakeShort) + o.AddCommonFlag(cmd, tf) + cmd.Flags().StringVar(&o.Filling, "filling", "", `The filling content of the error data can only be zero (filling with 0) or random (filling with random bytes).`) + cmd.Flags().IntVar(&o.MaxOccurrences, "max-occurrences", 1, `The maximum number of times an error can occur per operation.`) + cmd.Flags().IntVar(&o.MaxLength, "max-length", 1, `The maximum length (in bytes) of each error.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + }) +}) diff --git a/internal/cli/cmd/fault/fault_network.go b/internal/cli/cmd/fault/fault_network.go new file mode 100644 index 000000000..194902bd2 --- /dev/null +++ b/internal/cli/cmd/fault/fault_network.go @@ -0,0 +1,355 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "fmt" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var faultNetWorkExample = templates.Examples(` + # Isolate all pods network under the default namespace from the outside world, including the k8s internal network. + kbcli fault network partition + + # The specified pod is isolated from the k8s external network "kubeblocks.io". + kbcli fault network partition mycluster-mysql-1 --external-targets=kubeblocks.io + + # Isolate the network between two pods. + kbcli fault network partition mycluster-mysql-1 --target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2 + + // Like the partition command, the target can be specified through --target-label or --external-targets. The pod only has obstacles in communicating with this target. If the target is not specified, all communication will be blocked. + # Block all pod communication under the default namespace, resulting in a 50% packet loss rate. + kbcli fault network loss --loss=50 + + # Block the specified pod communication, so that the packet loss rate is 50%. + kbcli fault network loss mysql-cluster-mysql-2 --loss=50 + + kbcli fault network corrupt --corrupt=50 + + # Blocks specified pod communication with a 50% packet corruption rate. + kbcli fault network corrupt mysql-cluster-mysql-2 --corrupt=50 + + kbcli fault network duplicate --duplicate=50 + + # Block specified pod communication so that the packet repetition rate is 50%. + kbcli fault network duplicate mysql-cluster-mysql-2 --duplicate=50 + + kbcli fault network delay --latency=10s + + # Block the communication of the specified pod, causing its network delay for 10s. + kbcli fault network delay mysql-cluster-mysql-2 --latency=10s + + # Limit the communication bandwidth between mysql-cluster-mysql-2 and the outside. + kbcli fault network bandwidth mysql-cluster-mysql-2 --rate=1kbps --duration=1m +`) + +type Target struct { + TargetMode string `json:"mode,omitempty"` + TargetValue string `json:"value,omitempty"` + TargetSelector `json:"selector,omitempty"` +} + +type TargetSelector struct { + // Specifies the labels that target Pods come with. + TargetLabelSelectors map[string]string `json:"labelSelectors,omitempty"` + // Specifies the namespaces to which target Pods belong. + TargetNamespaceSelectors []string `json:"namespaces,omitempty"` +} + +// NetworkLoss Loss command +type NetworkLoss struct { + // The percentage of packet loss + Loss string `json:"loss,omitempty"` + // The correlation of loss or corruption or duplication or delay + Correlation string `json:"correlation,omitempty"` +} + +// NetworkDelay Delay command +type NetworkDelay struct { + // The latency of delay + Latency string `json:"latency,omitempty"` + // The jitter of delay + Jitter string `json:"jitter,omitempty"` + // The correlation of loss or corruption or duplication or delay + Correlation string `json:"correlation,omitempty"` +} + +// NetworkDuplicate Duplicate command +type NetworkDuplicate struct { + // The percentage of packet duplication + Duplicate string `json:"duplicate,omitempty"` + // The correlation of loss or corruption or duplication or delay + Correlation string `json:"correlation,omitempty"` +} + +// NetworkCorrupt Corrupt command +type NetworkCorrupt struct { + // The percentage of packet corruption + Corrupt string `json:"corrupt,omitempty"` + // The correlation of loss or corruption or duplication or delay + Correlation string `json:"correlation,omitempty"` +} + +// NetworkBandwidth Bandwidth command +type NetworkBandwidth struct { + // the rate at which the bandwidth is limited. + Rate string `json:"rate,omitempty"` + // the number of bytes waiting in the queue. + Limit uint32 `json:"limit,omitempty"` + // the maximum number of bytes that can be sent instantaneously. + Buffer uint32 `json:"buffer,omitempty"` + // the bucket's maximum consumption rate. Reference: https://man7.org/linux/man-pages/man8/tc-tbf.8.html. + Peakrate uint64 `json:"peakrate,omitempty"` + // the size of the peakrate bucket. Reference: https://man7.org/linux/man-pages/man8/tc-tbf.8.html. + Minburst uint32 `json:"minburst,omitempty"` +} + +type NetworkChaosOptions struct { + // Specify the network direction + Direction string `json:"direction"` + + // A network target outside of Kubernetes, which can be an IPv4 address or a domain name, + // such as "kubeblocks.io". Only works with direction: to. + ExternalTargets []string `json:"externalTargets,omitempty"` + + // A collection of target pods. Pods can be selected by namespace and label. + Target `json:"target,omitempty"` + + NetworkLoss `json:"loss,omitempty"` + + NetworkDelay `json:"delay,omitempty"` + + NetworkDuplicate `json:"duplicate,omitempty"` + + NetworkCorrupt `json:"corrupt,omitempty"` + + NetworkBandwidth `json:"bandwidth,omitempty"` + + FaultBaseOptions +} + +func NewNetworkChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *NetworkChaosOptions { + o := &NetworkChaosOptions{ + FaultBaseOptions: FaultBaseOptions{CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateNetworkChaos, + GVR: GetGVR(Group, Version, ResourceNetworkChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewNetworkChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "network", + Short: "Network chaos.", + } + cmd.AddCommand( + NewPartitionCmd(f, streams), + NewLossCmd(f, streams), + NewDelayCmd(f, streams), + NewDuplicateCmd(f, streams), + NewCorruptCmd(f, streams), + NewBandwidthCmd(f, streams), + NewDNSChaosCmd(f, streams), + NewHTTPChaosCmd(f, streams), + ) + return cmd +} + +func NewPartitionCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.PartitionAction)) + cmd := o.NewCobraCommand(Partition, PartitionShort) + + o.AddCommonFlag(cmd) + + return cmd +} + +func NewLossCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.LossAction)) + cmd := o.NewCobraCommand(Loss, LossShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Loss, "loss", "", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkLoss.Correlation, "correlation", "c", "", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + util.CheckErr(cmd.MarkFlagRequired("loss")) + + return cmd +} + +func NewDelayCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.DelayAction)) + cmd := o.NewCobraCommand(Delay, DelayShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Latency, "latency", "", `the length of time to delay.`) + cmd.Flags().StringVar(&o.Jitter, "jitter", "", `the variation range of the delay time.`) + cmd.Flags().StringVarP(&o.NetworkDelay.Correlation, "correlation", "c", "", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + + util.CheckErr(cmd.MarkFlagRequired("latency")) + + return cmd +} + +func NewDuplicateCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.DuplicateAction)) + cmd := o.NewCobraCommand(Duplicate, DuplicateShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Duplicate, "duplicate", "", `the probability of a packet being repeated. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkDuplicate.Correlation, "correlation", "c", "", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + util.CheckErr(cmd.MarkFlagRequired("duplicate")) + + return cmd +} + +func NewCorruptCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.CorruptAction)) + cmd := o.NewCobraCommand(Corrupt, CorruptShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Corrupt, "corrupt", "", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkCorrupt.Correlation, "correlation", "c", "", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + util.CheckErr(cmd.MarkFlagRequired("corrupt")) + + return cmd +} + +func NewBandwidthCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNetworkChaosOptions(f, streams, string(v1alpha1.BandwidthAction)) + cmd := o.NewCobraCommand(Bandwidth, BandwidthShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Rate, "rate", "", `the rate at which the bandwidth is limited. For example : 10 bps/kbps/mbps/gbps.`) + cmd.Flags().Uint32Var(&o.Limit, "limit", 1, `the number of bytes waiting in the queue.`) + cmd.Flags().Uint32Var(&o.Buffer, "buffer", 1, `the maximum number of bytes that can be sent instantaneously.`) + cmd.Flags().Uint64Var(&o.Peakrate, "peakrate", 0, `the maximum consumption rate of the bucket.`) + cmd.Flags().Uint32Var(&o.Minburst, "minburst", 0, `the size of the peakrate bucket.`) + + util.CheckErr(cmd.MarkFlagRequired("rate")) + + return cmd +} + +func (o *NetworkChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultNetWorkExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *NetworkChaosOptions) AddCommonFlag(cmd *cobra.Command) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().StringVar(&o.Direction, "direction", "to", `You can select "to"" or "from"" or "both"".`) + cmd.Flags().StringArrayVarP(&o.ExternalTargets, "external-target", "e", nil, "a network target outside of Kubernetes, which can be an IPv4 address or a domain name,\n\t such as \"www.baidu.com\". Only works with direction: to.") + cmd.Flags().StringVar(&o.TargetMode, "target-mode", "", `You can select "one", "all", "fixed", "fixed-percent", "random-max-percent", Specify the experimental mode, that is, which Pods to experiment with.`) + cmd.Flags().StringVar(&o.TargetValue, "target-value", "", `If you choose mode=fixed or fixed-percent or random-max-percent, you can enter a value to specify the number or percentage of pods you want to inject.`) + cmd.Flags().StringToStringVar(&o.TargetLabelSelectors, "target-label", nil, `label for pod, such as '"app.kubernetes.io/component=mysql, statefulset.kubernetes.io/pod-name=mycluster-mysql-0"'`) + cmd.Flags().StringArrayVar(&o.TargetNamespaceSelectors, "target-ns-fault", nil, `Specifies the namespace into which you want to inject faults.`) + + // register flag completion func + registerFlagCompletionFunc(cmd, o.Factory) +} + +func (o *NetworkChaosOptions) Validate() error { + if o.TargetValue == "" && (o.TargetMode == "fixed" || o.TargetMode == "fixed-percent" || o.TargetMode == "random-max-percent") { + return fmt.Errorf("--value is required to specify pod nums or percentage") + } + + if (o.TargetNamespaceSelectors != nil || o.TargetLabelSelectors != nil) && o.TargetMode == "" { + return fmt.Errorf("--target-mode is required to specify a target mode") + } + + if o.ExternalTargets != nil && o.Direction != "to" { + return fmt.Errorf("--direction=to is required when specifying external targets") + } + + if ok, err := IsInteger(o.TargetValue); !ok { + return err + } + + if ok, err := IsInteger(o.Loss); !ok { + return err + } + + if ok, err := IsInteger(o.Corrupt); !ok { + return err + } + + if ok, err := IsInteger(o.Duplicate); !ok { + return err + } + + if ok, err := IsRegularMatch(o.Latency); !ok { + return err + } + + if ok, err := IsRegularMatch(o.Jitter); !ok { + return err + } + + return o.BaseValidate() +} + +func (o *NetworkChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *NetworkChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.NetworkChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_network_test.go b/internal/cli/cmd/fault/fault_network_test.go new file mode 100644 index 000000000..9b20c708d --- /dev/null +++ b/internal/cli/cmd/fault/fault_network_test.go @@ -0,0 +1,184 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Network", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + Context("test fault network", func() { + It("fault network partition", func() { + inputs := [][]string{ + {"--dry-run=client"}, + {"--mode=one", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--dry-run=client"}, + {"--ns-fault=kb-system", "--dry-run=client"}, + {"--node=minikube-m02", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--dry-run=client"}, + {"--external-target=kubeblocks.io", "--dry-run=client"}, + {"--target-mode=one", "--target-label=statefulset.kubernetes.io/pod-name=mycluster-mysql-2", "--target-ns-fault=default", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.PartitionAction)) + cmd := o.NewCobraCommand(Partition, PartitionShort) + o.AddCommonFlag(cmd) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network loss", func() { + inputs := [][]string{ + {"--loss=50", "--dry-run=client"}, + {"--loss=50", "--correlation=100", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.LossAction)) + cmd := o.NewCobraCommand(Loss, LossShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Loss, "loss", "", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkLoss.Correlation, "correlation", "c", "0", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network delay", func() { + inputs := [][]string{ + {"--latency=50s", "--dry-run=client"}, + {"--latency=50s", "--jitter=10s", "--dry-run=client"}, + {"--latency=50s", "--correlation=100", "--dry-run=client"}, + {"--latency=50s", "--jitter=10s", "--correlation=100", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.DelayAction)) + cmd := o.NewCobraCommand(Delay, DelayShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Latency, "latency", "", `the length of time to delay.`) + cmd.Flags().StringVar(&o.Jitter, "jitter", "0ms", `the variation range of the delay time.`) + cmd.Flags().StringVarP(&o.NetworkDelay.Correlation, "correlation", "c", "0", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network duplicate", func() { + inputs := [][]string{ + {"--duplicate=50", "--dry-run=client"}, + {"--duplicate=50", "--correlation=100", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.DuplicateAction)) + cmd := o.NewCobraCommand(Duplicate, DuplicateShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Duplicate, "duplicate", "", `the probability of a packet being repeated. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkDuplicate.Correlation, "correlation", "c", "0", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network corrupt", func() { + inputs := [][]string{ + {"--corrupt=50", "--dry-run=client"}, + {"--corrupt=50", "--correlation=100", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.CorruptAction)) + cmd := o.NewCobraCommand(Corrupt, CorruptShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Corrupt, "corrupt", "", `Indicates the probability of a packet error occurring. Value range: [0, 100].`) + cmd.Flags().StringVarP(&o.NetworkCorrupt.Correlation, "correlation", "c", "0", `Indicates the correlation between the probability of a packet error occurring and whether it occurred the previous time. Value range: [0, 100].`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault network bandwidth", func() { + inputs := [][]string{ + {"--rate=10kbps", "--dry-run=client"}, + {"--rate=10kbps", "--limit=1000", "--buffer=100", "--peakrate=10", "--minburst=5", "--dry-run=client"}, + } + o := NewNetworkChaosOptions(tf, streams, string(v1alpha1.BandwidthAction)) + cmd := o.NewCobraCommand(Bandwidth, BandwidthShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringVar(&o.Rate, "rate", "", `the rate at which the bandwidth is limited. For example : 10 bps/kbps/mbps/gbps.`) + cmd.Flags().Uint32Var(&o.Limit, "limit", 1, `the number of bytes waiting in the queue.`) + cmd.Flags().Uint32Var(&o.Buffer, "buffer", 1, `the maximum number of bytes that can be sent instantaneously.`) + cmd.Flags().Uint64Var(&o.Peakrate, "peakrate", 0, `the maximum consumption rate of the bucket.`) + cmd.Flags().Uint32Var(&o.Minburst, "minburst", 0, `the size of the peakrate bucket.`) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_node.go b/internal/cli/cmd/fault/fault_node.go new file mode 100644 index 000000000..3029d1455 --- /dev/null +++ b/internal/cli/cmd/fault/fault_node.go @@ -0,0 +1,471 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "bufio" + "context" + "encoding/base64" + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + k8serrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/util/prompt" +) + +var faultNodeExample = templates.Examples(` + # Stop a specified EC2 instance. + kbcli fault node stop node1 -c=aws --region=cn-northwest-1 --duration=3m + + # Stop two specified EC2 instances. + kbcli fault node stop node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Restart two specified EC2 instances. + kbcli fault node restart node1 node2 -c=aws --region=cn-northwest-1 --duration=3m + + # Detach two specified volume from two specified EC2 instances. + kbcli fault node detach-volume node1 node2 -c=aws --region=cn-northwest-1 --duration=1m --volume-id=v1,v2 --device-name=/d1,/d2 + + # Stop two specified GCK instances. + kbcli fault node stop node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Restart two specified GCK instances. + kbcli fault node restart node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering + + # Detach two specified volume from two specified GCK instances. + kbcli fault node detach-volume node1 node2 -c=gcp --region=us-central1-c --project=apecloud-platform-engineering --device-name=/d1,/d2 +`) + +type NodeChaoOptions struct { + Kind string `json:"kind"` + + Action string `json:"action"` + + CloudProvider string `json:"-"` + + SecretName string `json:"secretName"` + + Region string `json:"region"` + + Instance string `json:"instance"` + + VolumeID string `json:"volumeID"` + VolumeIDs []string `json:"-"` + + DeviceName string `json:"deviceName,omitempty"` + DeviceNames []string `json:"-"` + + Project string `json:"project"` + + Duration string `json:"duration"` + + AutoApprove bool `json:"-"` + + create.CreateOptions `json:"-"` +} + +func NewNodeOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *NodeChaoOptions { + o := &NodeChaoOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateNodeChaos, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewNodeChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "node", + Short: "Node chaos.", + } + + cmd.AddCommand( + NewStopCmd(f, streams), + NewRestartCmd(f, streams), + NewDetachVolumeCmd(f, streams), + ) + return cmd +} + +func NewStopCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNodeOptions(f, streams) + cmd := o.NewCobraCommand(Stop, StopShort) + + o.AddCommonFlag(cmd) + return cmd +} + +func NewRestartCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNodeOptions(f, streams) + cmd := o.NewCobraCommand(Restart, RestartShort) + + o.AddCommonFlag(cmd) + return cmd +} + +func NewDetachVolumeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewNodeOptions(f, streams) + cmd := o.NewCobraCommand(DetachVolume, DetachVolumeShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringSliceVar(&o.VolumeIDs, "volume-id", nil, "The volume ids of the ec2. Only available when cloud-provider=aws.") + cmd.Flags().StringSliceVar(&o.DeviceNames, "device-name", nil, "The device name of the volume.") + + util.CheckErr(cmd.MarkFlagRequired("device-name")) + return cmd +} + +func (o *NodeChaoOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultNodeExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Execute(use, args, false)) + }, + } +} + +func (o *NodeChaoOptions) Execute(action string, args []string, testEnv bool) error { + o.Args = args + if err := o.CreateOptions.Complete(); err != nil { + return err + } + if err := o.Complete(action); err != nil { + return err + } + if err := o.Validate(); err != nil { + return err + } + + for idx, arg := range o.Args { + o.Instance = arg + if o.DeviceNames != nil { + o.DeviceName = o.DeviceNames[idx] + } + if o.VolumeIDs != nil { + o.VolumeID = o.VolumeIDs[idx] + } + if err := o.CreateSecret(testEnv); err != nil { + return err + } + if err := o.Run(); err != nil { + return err + } + } + return nil +} + +func (o *NodeChaoOptions) AddCommonFlag(cmd *cobra.Command) { + cmd.Flags().StringVarP(&o.CloudProvider, "cloud-provider", "c", "", fmt.Sprintf("Cloud provider type, one of %v", supportedCloudProviders)) + cmd.Flags().StringVar(&o.Region, "region", "", "The region of the node.") + cmd.Flags().StringVar(&o.Project, "project", "", "The name of the GCP project. Only available when cloud-provider=gcp.") + cmd.Flags().StringVar(&o.SecretName, "secret", "", "The name of the secret containing cloud provider specific credentials.") + cmd.Flags().StringVar(&o.Duration, "duration", "30s", "Supported formats of the duration are: ms / s / m / h.") + + cmd.Flags().BoolVar(&o.AutoApprove, "auto-approve", false, "Skip interactive approval before create secret.") + cmd.Flags().StringVar(&o.DryRun, "dry-run", "none", `Must be "client", or "server". If with client strategy, only print the object that would be sent, and no data is actually sent. If with server strategy, submit the server-side request, but no data is persistent.`) + cmd.Flags().Lookup("dry-run").NoOptDefVal = Unchanged + printer.AddOutputFlagForCreate(cmd, &o.Format) + + util.CheckErr(cmd.MarkFlagRequired("cloud-provider")) + util.CheckErr(cmd.MarkFlagRequired("region")) + + // register flag completion func + registerFlagCompletionFunc(cmd, o.Factory) +} + +func (o *NodeChaoOptions) Validate() error { + if ok, err := IsRegularMatch(o.Duration); !ok { + return err + } + + if len(o.Args) == 0 { + return fmt.Errorf("node instance is required") + } + + switch o.CloudProvider { + case cp.AWS: + if o.Project != "" { + return fmt.Errorf("--project is not supported when cloud provider is aws") + } + if o.Action == DetachVolume && o.VolumeIDs == nil { + return fmt.Errorf("--volume-id is required when cloud provider is aws") + } + if o.Action == DetachVolume && len(o.DeviceNames) != len(o.VolumeIDs) { + return fmt.Errorf("the number of volume-id must be equal to the number of device-name") + } + case cp.GCP: + if o.Project == "" { + return fmt.Errorf("--project is required when cloud provider is gcp") + } + if o.VolumeIDs != nil { + return fmt.Errorf(" --volume-id is not supported when cloud provider is gcp") + } + default: + return fmt.Errorf("cloud provider type, one of %v", supportedCloudProviders) + } + + if o.DeviceNames != nil && len(o.Args) != len(o.DeviceNames) { + return fmt.Errorf("the number of device-name must be equal to the number of node") + } + return nil +} + +func (o *NodeChaoOptions) Complete(action string) error { + if o.CloudProvider == cp.AWS { + o.GVR = GetGVR(Group, Version, ResourceAWSChaos) + o.Kind = KindAWSChaos + if o.SecretName == "" { + o.SecretName = AWSSecretName + } + switch action { + case Stop: + o.Action = string(v1alpha1.Ec2Stop) + case Restart: + o.Action = string(v1alpha1.Ec2Restart) + case DetachVolume: + o.Action = string(v1alpha1.DetachVolume) + } + } else if o.CloudProvider == cp.GCP { + o.GVR = GetGVR(Group, Version, ResourceGCPChaos) + o.Kind = KindGCPChaos + if o.SecretName == "" { + o.SecretName = GCPSecretName + } + switch action { + case Stop: + o.Action = string(v1alpha1.NodeStop) + case Restart: + o.Action = string(v1alpha1.NodeReset) + case DetachVolume: + o.Action = string(v1alpha1.DiskLoss) + } + } + return nil +} + +func (o *NodeChaoOptions) PreCreate(obj *unstructured.Unstructured) error { + var c v1alpha1.InnerObject + + if o.CloudProvider == cp.AWS { + c = &v1alpha1.AWSChaos{} + } else if o.CloudProvider == cp.GCP { + c = &v1alpha1.GCPChaos{} + } + + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} + +func (o *NodeChaoOptions) CreateSecret(testEnv bool) error { + if testEnv { + return nil + } + + if o.DryRun != "none" { + return nil + } + + config, err := o.Factory.ToRESTConfig() + if err != nil { + return err + } + + clientSet, err := kubernetes.NewForConfig(config) + if err != nil { + return err + } + + // Check if Secret already exists + secretClient := clientSet.CoreV1().Secrets(o.Namespace) + _, err = secretClient.Get(context.TODO(), o.SecretName, metav1.GetOptions{}) + if err == nil { + fmt.Printf("Secret %s exists under %s namespace.\n", o.SecretName, o.Namespace) + return nil + } else if !k8serrors.IsNotFound(err) { + return err + } + + if err := o.confirmToContinue(); err != nil { + return err + } + + switch o.CloudProvider { + case "aws": + if err := handleAWS(clientSet, o.Namespace, o.SecretName); err != nil { + return err + } + case "gcp": + if err := handleGCP(clientSet, o.Namespace, o.SecretName); err != nil { + return err + } + default: + return fmt.Errorf("unknown cloud provider:%s", o.CloudProvider) + } + return nil +} + +func (o *NodeChaoOptions) confirmToContinue() error { + if !o.AutoApprove { + printer.Warning(o.Out, "A secret will be created for the cloud account to access %s, do you want to continue to create this secret: %s ?\n Only 'yes' will be accepted to confirm.\n\n", o.CloudProvider, o.SecretName) + entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() + if entered != "yes" { + fmt.Fprintf(o.Out, "\nCancel automatic secert creation. You will not be able to access the nodes on the cluster.\n") + return cmdutil.ErrExit + } + } + fmt.Fprintf(o.Out, "Continue to create secret: %s\n", o.SecretName) + return nil +} + +func handleAWS(clientSet *kubernetes.Clientset, namespace, secretName string) error { + accessKeyID, secretAccessKey, err := readAWSCredentials() + if err != nil { + return err + } + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + "aws_access_key_id": accessKeyID, + "aws_secret_access_key": secretAccessKey, + }, + } + + createdSecret, err := clientSet.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + if err != nil { + return err + } + + fmt.Printf("Secret %s created successfully\n", createdSecret.Name) + return nil +} + +func handleGCP(clientSet *kubernetes.Clientset, namespace, secretName string) error { + home, err := os.UserHomeDir() + if err != nil { + return err + } + + filePath := filepath.Join(home, ".config", "gcloud", "application_default_credentials.json") + data, err := ioutil.ReadFile(filePath) + jsonData := string(data) + fmt.Println(jsonData) + if err != nil { + return err + } + encodedData := base64.StdEncoding.EncodeToString([]byte(jsonData)) + + secret := &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: secretName, + Namespace: namespace, + }, + Type: corev1.SecretTypeOpaque, + StringData: map[string]string{ + "service_account": encodedData, + }, + } + + createdSecret, err := clientSet.CoreV1().Secrets(namespace).Create(context.TODO(), secret, metav1.CreateOptions{}) + if err != nil { + return err + } + + fmt.Printf("Secret %s created successfully\n", createdSecret.Name) + return nil +} + +func readAWSCredentials() (string, string, error) { + home, err := os.UserHomeDir() + if err != nil { + return "", "", err + } + filePath := filepath.Join(home, ".aws", "credentials") + file, err := os.Open(filePath) + if err != nil { + return "", "", err + } + defer func(file *os.File) { + err := file.Close() + if err != nil { + fmt.Printf("unable to close file: %s", err) + } + }(file) + + // Read file content line by line using bufio.Scanner + scanner := bufio.NewScanner(file) + accessKeyID := "" + secretAccessKey := "" + + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "aws_access_key_id") { + accessKeyID = strings.TrimSpace(strings.SplitN(line, "=", 2)[1]) + } else if strings.HasPrefix(line, "aws_secret_access_key") { + secretAccessKey = strings.TrimSpace(strings.SplitN(line, "=", 2)[1]) + } + } + + if scanner.Err() != nil { + return "", "", scanner.Err() + } + + if accessKeyID == "" || secretAccessKey == "" { + return "", "", fmt.Errorf("unable to find valid AWS access key information") + } + + return accessKeyID, secretAccessKey, nil +} diff --git a/internal/cli/cmd/fault/fault_node_test.go b/internal/cli/cmd/fault/fault_node_test.go new file mode 100644 index 000000000..c9b6b978f --- /dev/null +++ b/internal/cli/cmd/fault/fault_node_test.go @@ -0,0 +1,104 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Node", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + Context("test fault node", func() { + It("fault node stop", func() { + inputs := [][]string{ + {"-c=aws", "--region=cn-northwest-1", "--dry-run=client"}, + {"-c=aws", "--region=cn-northwest-1", "--secret=test-secret", "--dry-run=client"}, + {"-c=gcp", "--region=us-central1-c", "--project=apecloud-platform-engineering", "--dry-run=client"}, + } + o := NewNodeOptions(tf, streams) + cmd := o.NewCobraCommand(Stop, StopShort) + o.AddCommonFlag(cmd) + + o.Args = []string{"node1", "node2"} + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.Execute(Stop, o.Args, true)).Should(Succeed()) + } + }) + + It("fault node restart", func() { + inputs := [][]string{ + {"-c=aws", "--region=cn-northwest-1", "--dry-run=client"}, + {"-c=aws", "--region=cn-northwest-1", "--secret=test-secret", "--dry-run=client"}, + {"-c=gcp", "--region=us-central1-c", "--project=apecloud-platform-engineering", "--dry-run=client"}, + } + o := NewNodeOptions(tf, streams) + cmd := o.NewCobraCommand(Restart, RestartShort) + o.AddCommonFlag(cmd) + + o.Args = []string{"node1", "node2"} + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.Execute(Restart, o.Args, true)).Should(Succeed()) + } + }) + + It("fault node detach-volume", func() { + inputs := [][]string{ + {"-c=aws", "--region=cn-northwest-1", "--volume-id=v1,v2", "--device-name=/d1,/d2", "--dry-run=client"}, + {"-c=aws", "--region=cn-northwest-1", "--volume-id=v1,v2", "--device-name=/d1,/d2", "--secret=test-secret", "--dry-run=client"}, + {"-c=gcp", "--region=us-central1-c", "--project=apecloud-platform-engineering", "--device-name=/d1,/d2", "--dry-run=client"}, + } + o := NewNodeOptions(tf, streams) + cmd := o.NewCobraCommand(DetachVolume, DetachVolumeShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringSliceVar(&o.VolumeIDs, "volume-id", nil, "The volume id of the ec2.") + cmd.Flags().StringSliceVar(&o.DeviceNames, "device-name", nil, "The device name of the volume.") + + o.Args = []string{"node1", "node2"} + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.Execute(DetachVolume, o.Args, true)).Should(Succeed()) + o.VolumeIDs = nil + o.DeviceNames = nil + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_pod.go b/internal/cli/cmd/fault/fault_pod.go new file mode 100644 index 000000000..3a64aa580 --- /dev/null +++ b/internal/cli/cmd/fault/fault_pod.go @@ -0,0 +1,186 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var faultPodExample = templates.Examples(` + # kill all pods in default namespace + kbcli fault pod kill + + # kill any pod in default namespace + kbcli fault pod kill --mode=one + + # kill two pods in default namespace + kbcli fault pod kill --mode=fixed --value=2 + + # kill 50% pods in default namespace + kbcli fault pod kill --mode=percentage --value=50 + + # kill mysql-cluster-mysql-0 pod in default namespace + kbcli fault pod kill mysql-cluster-mysql-0 + + # kill all pods in default namespace + kbcli fault pod kill --ns-fault="default" + + # --label is required to specify the pods that need to be killed. + kbcli fault pod kill --label statefulset.kubernetes.io/pod-name=mysql-cluster-mysql-2 + + # kill pod under the specified node. + kbcli fault pod kill --node=minikube-m02 + + # kill pod under the specified node-label. + kbcli fault pod kill --node-label=kubernetes.io/arch=arm64 + + # Allow the experiment to last for one minute. + kbcli fault pod failure --duration=1m + + # kill container in pod + kbcli fault pod kill-container mysql-cluster-mysql-0 --container=mysql +`) + +type PodChaosOptions struct { + // GracePeriod waiting time, after which fault injection is performed + GracePeriod int64 `json:"gracePeriod"` + ContainerNames []string `json:"containerNames,omitempty"` + + FaultBaseOptions +} + +func NewPodChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *PodChaosOptions { + o := &PodChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplatePodChaos, + GVR: GetGVR(Group, Version, ResourcePodChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewPodChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "pod", + Short: "Pod chaos.", + } + cmd.AddCommand( + NewPodKillCmd(f, streams), + NewPodFailureCmd(f, streams), + NewContainerKillCmd(f, streams), + ) + return cmd +} + +func NewPodKillCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewPodChaosOptions(f, streams, string(v1alpha1.PodKillAction)) + cmd := o.NewCobraCommand(Kill, KillShort) + + o.AddCommonFlag(cmd) + cmd.Flags().Int64VarP(&o.GracePeriod, "grace-period", "g", 0, "Grace period represents the duration in seconds before the pod should be killed") + + // register flag completion func + registerFlagCompletionFunc(cmd, f) + + return cmd +} + +func NewPodFailureCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewPodChaosOptions(f, streams, string(v1alpha1.PodFailureAction)) + cmd := o.NewCobraCommand(Failure, FailureShort) + + o.AddCommonFlag(cmd) + // register flag completion func + registerFlagCompletionFunc(cmd, f) + + return cmd +} + +func NewContainerKillCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewPodChaosOptions(f, streams, string(v1alpha1.ContainerKillAction)) + cmd := o.NewCobraCommand(KillContainer, KillContainerShort) + + o.AddCommonFlag(cmd) + cmd.Flags().StringArrayVarP(&o.ContainerNames, "container", "c", nil, "the name of the container you want to kill, such as mysql, prometheus.") + + util.CheckErr(cmd.MarkFlagRequired("container")) + + // register flag completion func + registerFlagCompletionFunc(cmd, f) + + return cmd +} + +func (o *PodChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultPodExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *PodChaosOptions) AddCommonFlag(cmd *cobra.Command) { + o.FaultBaseOptions.AddCommonFlag(cmd) +} + +func (o *PodChaosOptions) Validate() error { + return o.BaseValidate() +} + +func (o *PodChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *PodChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.PodChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_pod_test.go b/internal/cli/cmd/fault/fault_pod_test.go new file mode 100644 index 000000000..b1e55d3b8 --- /dev/null +++ b/internal/cli/cmd/fault/fault_pod_test.go @@ -0,0 +1,96 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault POD", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + Context("test fault pod", func() { + It("fault pod kill", func() { + inputs := [][]string{ + {"--dry-run=client"}, + {"--mode=one", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--dry-run=client"}, + {"--grace-period=5", "--dry-run=client"}, + {"--ns-fault=kb-system", "--dry-run=client"}, + {"--node=minikube-m02", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--dry-run=client"}, + } + o := NewPodChaosOptions(tf, streams, string(v1alpha1.PodKillAction)) + cmd := o.NewCobraCommand(Kill, KillShort) + o.AddCommonFlag(cmd) + cmd.Flags().Int64VarP(&o.GracePeriod, "grace-period", "g", 0, "Grace period represents the duration in seconds before the pod should be killed") + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + + It("fault pod kill-container", func() { + inputs := [][]string{ + {"--container=mysql", "--container=config-manager", "--dry-run=client"}, + } + o := NewPodChaosOptions(tf, streams, string(v1alpha1.ContainerKillAction)) + cmd := o.NewCobraCommand(KillContainer, KillContainerShort) + o.AddCommonFlag(cmd) + cmd.Flags().StringArrayVarP(&o.ContainerNames, "container", "c", nil, "the name of the container you want to kill, such as mysql, prometheus.") + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_stress.go b/internal/cli/cmd/fault/fault_stress.go new file mode 100644 index 000000000..6e1cc932b --- /dev/null +++ b/internal/cli/cmd/fault/fault_stress.go @@ -0,0 +1,146 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "fmt" + + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" +) + +var faultStressExample = templates.Examples(` + # Affects the first container in default namespace's all pods.Making CPU load up to 50%, and the memory up to 100MB. + kbcli fault stress --cpu-worker=2 --cpu-load=50 --memory-worker=1 --memory-size=100Mi + + # Affects the first container in mycluster-mysql-0 pod. Making the CPU load up to 50%, and the memory up to 500MB. + kbcli fault stress mycluster-mysql-0 --cpu-worker=2 --cpu-load=50 + + # Affects the mysql container in mycluster-mysql-0 pod. Making the memory up to 500MB. + kbcli fault stress mycluster-mysql-0 --memory-worker=2 --memory-size=500Mi -c=mysql +`) + +type CPU struct { + Workers int `json:"workers"` + Load int `json:"load"` +} + +type Memory struct { + Workers int `json:"workers"` + Size string `json:"size"` +} + +type Stressors struct { + CPU `json:"cpu"` + Memory `json:"memory"` +} + +type StressChaosOptions struct { + Stressors `json:"stressors"` + ContainerNames []string `json:"containerNames,omitempty"` + + FaultBaseOptions +} + +func NewStressChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *StressChaosOptions { + o := &StressChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateStressChaos, + GVR: GetGVR(Group, Version, ResourceStressChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewStressChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewStressChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(Stress, StressShort) + + o.AddCommonFlag(cmd, f) + return cmd +} + +func (o *StressChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultStressExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *StressChaosOptions) AddCommonFlag(cmd *cobra.Command, f cmdutil.Factory) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().IntVar(&o.CPU.Workers, "cpu-worker", 0, `Specifies the number of threads that exert CPU pressure.`) + cmd.Flags().IntVar(&o.CPU.Load, "cpu-load", 0, `Specifies the percentage of CPU occupied. 0 means no extra load added, 100 means full load. The total load is workers * load.`) + cmd.Flags().IntVar(&o.Memory.Workers, "memory-worker", 0, `Specifies the number of threads that apply memory pressure.`) + cmd.Flags().StringVar(&o.Memory.Size, "memory-size", "", `Specify the size of the allocated memory or the percentage of the total memory, and the sum of the allocated memory is size. For example:256MB or 25%`) + cmd.Flags().StringArrayVarP(&o.ContainerNames, "container", "c", nil, "The name of the container, such as mysql, prometheus.If it's empty, the first container will be injected.") + + // register flag completion func + registerFlagCompletionFunc(cmd, f) +} + +func (o *StressChaosOptions) Validate() error { + if o.Memory.Workers == 0 && o.CPU.Workers == 0 { + return fmt.Errorf("the CPU or Memory workers must have at least one greater than 0, Use --cpu-workers or --memory-workers to specify") + } + + return o.BaseValidate() +} + +func (o *StressChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *StressChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.StressChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_stress_test.go b/internal/cli/cmd/fault/fault_stress_test.go new file mode 100644 index 000000000..533527f74 --- /dev/null +++ b/internal/cli/cmd/fault/fault_stress_test.go @@ -0,0 +1,78 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Stress", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + Context("test fault stress", func() { + + It("fault stress", func() { + inputs := [][]string{ + {"--mode=one", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--ns-fault=kb-system", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--node=minikube-m02", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--cpu-worker=2", "--cpu-load=50", "--dry-run=client"}, + {"--memory-worker=2", "--memory-size=500Mi", "-c=mysql", "--dry-run=client"}, + {"--cpu-worker=2", "--cpu-load=50", "--memory-worker=1", "--memory-size=100Mi", "--dry-run=client"}, + {"--cpu-worker=2", "--cpu-load=50", "--memory-worker=1", "--memory-size=100Mi", "--dry-run=client", "--container=mysql"}, + } + o := NewStressChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Stress, StressShort) + o.AddCommonFlag(cmd, tf) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/fault_time.go b/internal/cli/cmd/fault/fault_time.go new file mode 100644 index 000000000..42950aa41 --- /dev/null +++ b/internal/cli/cmd/fault/fault_time.go @@ -0,0 +1,135 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "github.com/chaos-mesh/chaos-mesh/api/v1alpha1" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var faultTimeExample = templates.Examples(` + # Affects the first container in default namespace's all pods.Shifts the clock back five seconds. + kbcli fault time --time-offset=-5s + + # Affects the first container in default namespace's all pods. + kbcli fault time --time-offset=-5m5s + + # Affects the first container in mycluster-mysql-0 pod. Shifts the clock forward five seconds. + kbcli fault time mycluster-mysql-0 --time-offset=+5s50ms + + # Affects the mysql container in mycluster-mysql-0 pod. Shifts the clock forward five seconds. + kbcli fault time mycluster-mysql-0 --time-offset=+5s -c=mysql + + # The clock that specifies the effect of time offset is CLOCK_REALTIME. + kbcli fault time mycluster-mysql-0 --time-offset=+5s --clock-id=CLOCK_REALTIME -c=mysql +`) + +type TimeChaosOptions struct { + TimeOffset string `json:"timeOffset"` + + ClockIds []string `json:"clockIds,omitempty"` + + ContainerNames []string `json:"containerNames,omitempty"` + + FaultBaseOptions +} + +func NewTimeChaosOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, action string) *TimeChaosOptions { + o := &TimeChaosOptions{ + FaultBaseOptions: FaultBaseOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: CueTemplateTimeChaos, + GVR: GetGVR(Group, Version, ResourceTimeChaos), + }, + Action: action, + }, + } + o.CreateOptions.PreCreate = o.PreCreate + o.CreateOptions.Options = o + return o +} + +func NewTimeChaosCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := NewTimeChaosOptions(f, streams, "") + cmd := o.NewCobraCommand(Time, TimeShort) + + o.AddCommonFlag(cmd, f) + return cmd +} + +func (o *TimeChaosOptions) NewCobraCommand(use, short string) *cobra.Command { + return &cobra.Command{ + Use: use, + Short: short, + Example: faultTimeExample, + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.CreateOptions.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Run()) + }, + } +} + +func (o *TimeChaosOptions) AddCommonFlag(cmd *cobra.Command, f cmdutil.Factory) { + o.FaultBaseOptions.AddCommonFlag(cmd) + + cmd.Flags().StringVar(&o.TimeOffset, "time-offset", "", "Specifies the length of the time offset. For example: -5s, -10m100ns.") + cmd.Flags().StringArrayVar(&o.ClockIds, "clock-id", nil, `Specifies the clock on which the time offset acts.If it's empty, it will be set to ['CLOCK_REALTIME'].See clock_gettime [https://man7.org/linux/man-pages/man2/clock_gettime.2.html] document for details.`) + cmd.Flags().StringArrayVarP(&o.ContainerNames, "container", "c", nil, `Specifies the injected container name. For example: mysql. If it's empty, the first container will be injected.`) + + util.CheckErr(cmd.MarkFlagRequired("time-offset")) + + // register flag completion func + registerFlagCompletionFunc(cmd, f) +} + +func (o *TimeChaosOptions) Validate() error { + return o.BaseValidate() +} + +func (o *TimeChaosOptions) Complete() error { + return o.BaseComplete() +} + +func (o *TimeChaosOptions) PreCreate(obj *unstructured.Unstructured) error { + c := &v1alpha1.TimeChaos{} + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, c); err != nil { + return err + } + + data, e := runtime.DefaultUnstructuredConverter.ToUnstructured(c) + if e != nil { + return e + } + obj.SetUnstructuredContent(data) + return nil +} diff --git a/internal/cli/cmd/fault/fault_time_test.go b/internal/cli/cmd/fault/fault_time_test.go new file mode 100644 index 000000000..ef6b108f7 --- /dev/null +++ b/internal/cli/cmd/fault/fault_time_test.go @@ -0,0 +1,76 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Fault Time", func() { + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + ) + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + Context("test fault time", func() { + + It("fault time", func() { + inputs := [][]string{ + {"--mode=one", "--time-offset=-5s", "--dry-run=client"}, + {"--mode=fixed", "--value=2", "--time-offset=-5s", "--dry-run=client"}, + {"--mode=fixed-percent", "--value=50", "--time-offset=-5s", "--dry-run=client"}, + {"--mode=random-max-percent", "--value=50", "--time-offset=-5s", "--dry-run=client"}, + {"--ns-fault=kb-system", "--time-offset=-5s", "--dry-run=client"}, + {"--node=minikube-m02", "--time-offset=-5s", "--dry-run=client"}, + {"--label=app.kubernetes.io/component=mysql", "--time-offset=-5s", "--dry-run=client"}, + {"--node-label=kubernetes.io/arch=arm64", "--time-offset=-5s", "--dry-run=client"}, + {"--annotation=example-annotation=group-a", "--time-offset=-5s", "--dry-run=client"}, + {"--time-offset=-5s", "--dry-run=client"}, + {"--time-offset=+5s", "--clock-id=CLOCK_REALTIME", "-c=mysql", "--dry-run=client"}, + } + o := NewTimeChaosOptions(tf, streams, "") + cmd := o.NewCobraCommand(Time, TimeShort) + o.AddCommonFlag(cmd, tf) + + for _, input := range inputs { + Expect(cmd.Flags().Parse(input)).Should(Succeed()) + Expect(o.CreateOptions.Complete()) + Expect(o.Complete()).Should(Succeed()) + Expect(o.Validate()).Should(Succeed()) + Expect(o.Run()).Should(Succeed()) + } + }) + }) +}) diff --git a/internal/cli/cmd/fault/suite_test.go b/internal/cli/cmd/fault/suite_test.go new file mode 100644 index 000000000..7f1e8aba4 --- /dev/null +++ b/internal/cli/cmd/fault/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package fault + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestDashboard(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Fault Suite") +} diff --git a/internal/cli/cmd/kubeblocks/config.go b/internal/cli/cmd/kubeblocks/config.go new file mode 100644 index 000000000..ebbfabbb4 --- /dev/null +++ b/internal/cli/cmd/kubeblocks/config.go @@ -0,0 +1,222 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package kubeblocks + +import ( + "context" + "strings" + "time" + + "github.com/spf13/cobra" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + deploymentutil "k8s.io/kubectl/pkg/util/deployment" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/util/helm" + "github.com/apecloud/kubeblocks/internal/constant" +) + +var showAllConfig = false +var keyWhiteList = []string{ + "addonController", + "dataProtection", + "affinity", + "tolerations", +} + +var backupConfigExample = templates.Examples(` + # Enable the snapshot-controller and volume snapshot, to support snapshot backup. + kbcli kubeblocks config --set snapshot-controller.enabled=true + + Options Parameters: + # If you have already installed a snapshot-controller, only enable the snapshot backup feature + dataProtection.enableVolumeSnapshot=true + + # the global pvc name which persistent volume claim to store the backup data. + # replace the pvc name when it is empty in the backup policy. + dataProtection.backupPVCName=backup-data + + # the init capacity of pvc for creating the pvc, e.g. 10Gi. + # replace the init capacity when it is empty in the backup policy. + dataProtection.backupPVCInitCapacity=100Gi + + # the pvc storage class name. replace the storageClassName when it is unset in the backup policy. + dataProtection.backupPVCStorageClassName=csi-s3 + + # the pvc creation policy. + # if the storageClass supports dynamic provisioning, recommend "IfNotPresent" policy. + # otherwise, using "Never" policy. only affect the backupPolicy automatically created by KubeBlocks. + dataProtection.backupPVCCreatePolicy=Never + + # the configmap name of the pv template. if the csi-driver does not support dynamic provisioning, + # you can provide a configmap which contains key "persistentVolume" and value of the persistentVolume struct. + dataProtection.backupPVConfigMapName=pv-template + + # the configmap namespace of the pv template. + dataProtection.backupPVConfigMapNamespace=default + `) + +var describeConfigExample = templates.Examples(` + # Describe the KubeBlocks config. + kbcli kubeblocks describe-config +`) + +// NewConfigCmd creates the config command +func NewConfigCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := &InstallOptions{ + Options: Options{ + IOStreams: streams, + Wait: true, + }, + } + + cmd := &cobra.Command{ + Use: "config", + Short: "KubeBlocks config.", + Example: backupConfigExample, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.Complete(f, cmd)) + util.CheckErr(o.Upgrade()) + util.CheckErr(markKubeBlocksPodsToLoadConfigMap(o.Client)) + }, + } + helm.AddValueOptionsFlags(cmd.Flags(), &o.ValueOpts) + return cmd +} + +func NewDescribeConfigCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := &InstallOptions{ + Options: Options{ + IOStreams: streams, + }, + } + var output printer.Format + cmd := &cobra.Command{ + Use: "describe-config", + Short: "describe KubeBlocks config.", + Example: describeConfigExample, + Args: cobra.NoArgs, + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.Complete(f, cmd)) + util.CheckErr(describeConfig(o, output, getHelmValues)) + }, + } + printer.AddOutputFlag(cmd, &output) + cmd.Flags().BoolVarP(&showAllConfig, "all", "A", false, "show all kubeblocks configs value") + return cmd +} + +// getHelmValues gets all kubeblocks values by helm and filter the addons values +func getHelmValues(release string, opt *Options) (map[string]interface{}, error) { + if len(opt.HelmCfg.Namespace()) == 0 { + namespace, err := util.GetKubeBlocksNamespace(opt.Client) + if err != nil { + return nil, err + } + opt.HelmCfg.SetNamespace(namespace) + } + values, err := helm.GetValues(release, opt.HelmCfg) + if err != nil { + return nil, err + } + // filter the addons values + list, err := opt.Dynamic.Resource(types.AddonGVR()).List(context.Background(), metav1.ListOptions{}) + if err != nil { + return nil, err + } + for _, item := range list.Items { + delete(values, item.GetName()) + } + if showAllConfig { + return values, nil + } + res := make(map[string]interface{}) + for i := range keyWhiteList { + res[keyWhiteList[i]] = values[keyWhiteList[i]] + } + return res, nil +} + +type fn func(release string, opt *Options) (map[string]interface{}, error) + +// describeConfig outputs the configs got by the fn in specified format +func describeConfig(o *InstallOptions, format printer.Format, f fn) error { + values, err := f(types.KubeBlocksReleaseName, &o.Options) + if err != nil { + return err + } + printer.PrintHelmValues(values, format, o.Out) + return nil +} + +// markKubeBlocksPodsToLoadConfigMap marks an annotation of the KubeBlocks pods to load the projected volumes of configmap. +// kubelet periodically requeues the Pod every 60-90 seconds, exactly the time it takes for Secret/ConfigMaps can be loaded in the config volumes. +func markKubeBlocksPodsToLoadConfigMap(client kubernetes.Interface) error { + deploy, err := util.GetKubeBlocksDeploy(client) + if err != nil { + return err + } + if deploy == nil { + return nil + } + pods, err := client.CoreV1().Pods(deploy.Namespace).List(context.Background(), metav1.ListOptions{ + LabelSelector: "app.kubernetes.io/name=" + types.KubeBlocksChartName, + }) + if err != nil { + return err + } + if len(pods.Items) == 0 { + return nil + } + condition := deploymentutil.GetDeploymentCondition(deploy.Status, appsv1.DeploymentProgressing) + if condition == nil { + return nil + } + podBelongToKubeBlocks := func(pod corev1.Pod) bool { + for _, v := range pod.OwnerReferences { + if v.Kind == constant.ReplicaSetKind && strings.Contains(condition.Message, v.Name) { + return true + } + } + return false + } + for _, pod := range pods.Items { + belongToKubeBlocks := podBelongToKubeBlocks(pod) + if !belongToKubeBlocks { + continue + } + // mark the pod to load configmap + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations[types.ReloadConfigMapAnnotationKey] = time.Now().Format(time.RFC3339Nano) + _, _ = client.CoreV1().Pods(deploy.Namespace).Update(context.TODO(), &pod, metav1.UpdateOptions{}) + } + return nil +} diff --git a/internal/cli/cmd/kubeblocks/config_test.go b/internal/cli/cmd/kubeblocks/config_test.go new file mode 100644 index 000000000..7ea25b3bb --- /dev/null +++ b/internal/cli/cmd/kubeblocks/config_test.go @@ -0,0 +1,213 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package kubeblocks + +import ( + "bytes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "helm.sh/helm/v3/pkg/cli/values" + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util/helm" + "github.com/apecloud/kubeblocks/version" +) + +var _ = Describe("backupconfig", func() { + var streams genericclioptions.IOStreams + var tf *cmdtesting.TestFactory + var o *InstallOptions + var out *bytes.Buffer + + mockDeploy := func() *appsv1.Deployment { + deploy := &appsv1.Deployment{} + deploy.SetLabels(map[string]string{ + "app.kubernetes.io/name": types.KubeBlocksChartName, + "app.kubernetes.io/version": "0.3.0", + }) + deploy.Spec.Template.Spec.Containers = []corev1.Container{ + { + Name: "kb", + Env: []corev1.EnvVar{ + { + Name: "CM_NAMESPACE", + Value: "default", + }, + { + Name: "VOLUMESNAPSHOT", + Value: "true", + }, + }, + }, + } + return deploy + } + + mockHelmConfig := func(release string, opt *Options) (map[string]interface{}, error) { + return map[string]interface{}{ + "updateStrategy": map[string]interface{}{ + "rollingUpdate": map[string]interface{}{ + "maxSurge": 1, + "maxUnavailable": "40%", + }, + "type": "RollingUpdate", + }, + "podDisruptionBudget": map[string]interface{}{ + "minAvailable": 1, + }, + "loggerSettings": map[string]interface{}{ + "developmentMode": false, + "encoder": "console", + "timeEncoding": "iso8601", + }, + "priorityClassName": nil, + "nameOverride": "", + "fullnameOverride": "", + "dnsPolicy": "ClusterFirst", + "replicaCount": 1, + "hostNetwork": false, + "keepAddons": false, + }, nil + } + + BeforeEach(func() { + streams, _, out, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + // use a fake URL to test + types.KubeBlocksChartName = testing.KubeBlocksChartName + types.KubeBlocksChartURL = testing.KubeBlocksChartURL + o = &InstallOptions{ + Options: Options{ + IOStreams: streams, + HelmCfg: helm.NewFakeConfig(testing.Namespace), + Namespace: "default", + Client: testing.FakeClientSet(mockDeploy()), + Dynamic: testing.FakeDynamicClient(), + }, + Version: version.DefaultKubeBlocksVersion, + Monitor: true, + ValueOpts: values.Options{Values: []string{"snapshot-controller.enabled=true"}}, + } + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("run config cmd", func() { + cmd := NewConfigCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + Expect(o.PreCheck()).Should(HaveOccurred()) + }) + + Context("run describe config cmd", func() { + var output printer.Format + + It("describe-config --output table/wide", func() { + output = printer.Table + err := describeConfig(o, output, mockHelmConfig) + Expect(err).Should(Succeed()) + expect := `KEY VALUE +dnsPolicy "ClusterFirst" +fullnameOverride "" +hostNetwork false +keepAddons false +loggerSettings.developmentMode false +loggerSettings.encoder "console" +loggerSettings.timeEncoding "iso8601" +nameOverride "" +podDisruptionBudget.minAvailable 1 +priorityClassName +replicaCount 1 +updateStrategy.rollingUpdate {"maxSurge":1,"maxUnavailable":"40%"} +updateStrategy.type "RollingUpdate" +` + Expect(out.String()).Should(Equal(expect)) + }) + + It("describe-config --output json", func() { + output = printer.JSON + expect := `{ + "dnsPolicy": "ClusterFirst", + "fullnameOverride": "", + "hostNetwork": false, + "keepAddons": false, + "loggerSettings": { + "developmentMode": false, + "encoder": "console", + "timeEncoding": "iso8601" + }, + "nameOverride": "", + "podDisruptionBudget": { + "minAvailable": 1 + }, + "priorityClassName": null, + "replicaCount": 1, + "updateStrategy": { + "rollingUpdate": { + "maxSurge": 1, + "maxUnavailable": "40%" + }, + "type": "RollingUpdate" + } +} +` + err := describeConfig(o, output, mockHelmConfig) + Expect(err).Should(Succeed()) + Expect(out.String()).Should(Equal(expect)) + }) + + It("describe-config --output yaml", func() { + output = printer.YAML + expect := `dnsPolicy: ClusterFirst +fullnameOverride: "" +hostNetwork: false +keepAddons: false +loggerSettings: + developmentMode: false + encoder: console + timeEncoding: iso8601 +nameOverride: "" +podDisruptionBudget: + minAvailable: 1 +priorityClassName: null +replicaCount: 1 +updateStrategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 40% + type: RollingUpdate +` + err := describeConfig(o, output, mockHelmConfig) + Expect(err).Should(Succeed()) + Expect(out.String()).Should(Equal(expect)) + }) + }) +}) diff --git a/internal/cli/cmd/kubeblocks/data/ack_hostpreflight.yaml b/internal/cli/cmd/kubeblocks/data/ack_hostpreflight.yaml new file mode 100644 index 000000000..a41ffdc63 --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/ack_hostpreflight.yaml @@ -0,0 +1,20 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: host-utility +spec: + collectors: + analyzers: + extendCollectors: + - hostUtility : + collectorName: aliyun-cli + utilityName: aliyun + extendAnalyzers: + - hostUtility: + checkName: aliyunCli-Check + collectorName: aliyun-cli + outcomes: + - pass: + message: aliyun-cli has been installed + - warn: + message: aliyun-cli isn't installed \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/ack_preflight.yaml b/internal/cli/cmd/kubeblocks/data/ack_preflight.yaml new file mode 100644 index 000000000..d893b65fc --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/ack_preflight.yaml @@ -0,0 +1,49 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: Preflight +metadata: + name: kubeblocks_preflight +spec: + collectors: + - clusterInfo: {} + analyzers: + - clusterVersion: + checkName: ACK-Version + outcomes: + - fail: + when: "< 1.22.0" + message: This application requires at least Kubernetes 1.20.0 or later, and recommends 1.22.0 + uri: https://www.kubernetes.io + - pass: + when: ">= 1.22.0" + message: Your cluster meets the recommended and required versions(>= 1.22.0) of Kubernetes + uri: https://www.kubernetes.io + - nodeResources: + checkName: At-Least-3-Nodes + outcomes: + - warn: + when: "count() < 3" + message: This application requires at least 3 nodes + - pass: + message: This cluster has enough nodes + extendAnalyzers: + - clusterAccess: + checkName: Check-K8S-Access + outcomes: + - fail: + message: K8s cluster access fail + - pass: + message: K8s cluster access ok + - taint: + checkName: Required-Taint-Match + outcomes: + - fail: + message: All nodes had taints that the pod didn't tolerate + - pass: + message: The taint matching succeeded + - storageClass: + checkName: Required-Default-SC + outcomes: + - warn: + message: The default storage class was not found. To learn more details, please check https://help.aliyun.com/document_detail/189288.html; Alternatively use option --set storageClass= when creating cluster + - pass: + message: Default storage class is the presence, and all good on storage classes \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/eks_preflight.yaml b/internal/cli/cmd/kubeblocks/data/eks_preflight.yaml index d50337f1e..31d38557f 100644 --- a/internal/cli/cmd/kubeblocks/data/eks_preflight.yaml +++ b/internal/cli/cmd/kubeblocks/data/eks_preflight.yaml @@ -25,14 +25,6 @@ spec: message: This application requires at least 3 nodes - pass: message: This cluster has enough nodes. - - storageClass: - checkName: Required-GP3-SC - storageClassName: "gp3" - outcomes: - - fail: - message: The gp3 storage class was not found - - pass: - message: gp3 is the presence, and all good on storage classes - deploymentStatus: checkName: AWS-Load-Balancer-Check name: aws-load-balancer-controller @@ -40,20 +32,34 @@ spec: outcomes: - warn: when: "absent" # note that the "absent" failure state must be listed first if used. - message: The aws-load-balancer-controller deployment is not present. + message: The aws-load-balancer-controller deployment is not present - warn: when: "< 1" - message: The aws-load-balancer-controller deployment does not have any ready replicas. + message: The aws-load-balancer-controller deployment does not have any ready replicas - warn: when: "= 1" - message: The aws-load-balancer-controller deployment has only a single ready replica. + message: The aws-load-balancer-controller deployment has only a single ready replica - pass: - message: There are multiple replicas of the aws-load-balancer-controller deployment ready. + message: There are multiple replicas of the aws-load-balancer-controller deployment ready extendAnalyzers: - clusterAccess: checkName: Check-K8S-Access outcomes: - fail: - message: k8s cluster access fail + message: K8s cluster access fail + - pass: + message: K8s cluster access ok + - taint: + checkName: Required-Taint-Match + outcomes: + - fail: + message: All nodes had taints that the pod didn't tolerate + - pass: + message: The taint matching succeeded + - storageClass: + checkName: Required-Default-SC + outcomes: + - warn: + message: The default storage class was not found. To learn more details, please check https://docs.aws.amazon.com/zh_cn/eks/latest/userguide/storage-classes.html; Alternatively use option --set storageClass= when creating cluster - pass: - message: k8s cluster access ok \ No newline at end of file + message: Default storage class is the presence, and all good on storage classes \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/gke_hostpreflight.yaml b/internal/cli/cmd/kubeblocks/data/gke_hostpreflight.yaml new file mode 100644 index 000000000..1a8ccc863 --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/gke_hostpreflight.yaml @@ -0,0 +1,20 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: host-utility +spec: + collectors: + analyzers: + extendCollectors: + - hostUtility : + collectorName: gcloud-cli + utilityName: gcloud + extendAnalyzers: + - hostUtility: + checkName: gcloudCli-Check + collectorName: gcloud-cli + outcomes: + - pass: + message: gcloud-cli has been installed + - warn: + message: gcloud-cli isn't installed \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/gke_preflight.yaml b/internal/cli/cmd/kubeblocks/data/gke_preflight.yaml new file mode 100644 index 000000000..554c7d66c --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/gke_preflight.yaml @@ -0,0 +1,49 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: Preflight +metadata: + name: kubeblocks_preflight +spec: + collectors: + - clusterInfo: {} + analyzers: + - clusterVersion: + checkName: GKE-Version + outcomes: + - fail: + when: "< 1.22.0" + message: This application requires at least Kubernetes 1.20.0 or later, and recommends 1.22.0 + uri: https://www.kubernetes.io + - pass: + when: ">= 1.22.0" + message: Your cluster meets the recommended and required versions(>= 1.22.0) of Kubernetes + uri: https://www.kubernetes.io + - nodeResources: + checkName: At-Least-3-Nodes + outcomes: + - warn: + when: "count() < 3" + message: This application requires at least 3 nodes + - pass: + message: This cluster has enough nodes + extendAnalyzers: + - clusterAccess: + checkName: Check-K8S-Access + outcomes: + - fail: + message: K8s cluster access fail + - pass: + message: K8s cluster access ok + - taint: + checkName: Required-Taint-Match + outcomes: + - fail: + message: All nodes had taints that the pod didn't tolerate + - pass: + message: The taint matching succeeded + - storageClass: + checkName: Required-Default-SC + outcomes: + - warn: + message: The default storage class was not found. To learn more details, please check https://cloud.google.com/anthos/clusters/docs/on-prem/latest/how-to/default-storage-class; Alternatively use option --set storageClass= when creating cluster + - pass: + message: Default storage class is the presence, and all good on storage classes \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/tke_hostpreflight.yaml b/internal/cli/cmd/kubeblocks/data/tke_hostpreflight.yaml new file mode 100644 index 000000000..acd2238f1 --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/tke_hostpreflight.yaml @@ -0,0 +1,20 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: HostPreflight +metadata: + name: host-utility +spec: + collectors: + analyzers: + extendCollectors: + - hostUtility : + collectorName: txcloud-cli + utilityName: tccli + extendAnalyzers: + - hostUtility: + checkName: txcloudCli-Check + collectorName: txcloud-cli + outcomes: + - pass: + message: txcloud-cli has been installed + - warn: + message: txcloud-cli isn't installed \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/data/tke_preflight.yaml b/internal/cli/cmd/kubeblocks/data/tke_preflight.yaml new file mode 100644 index 000000000..816d618b2 --- /dev/null +++ b/internal/cli/cmd/kubeblocks/data/tke_preflight.yaml @@ -0,0 +1,49 @@ +apiVersion: troubleshoot.sh/v1beta2 +kind: Preflight +metadata: + name: kubeblocks_preflight +spec: + collectors: + - clusterInfo: {} + analyzers: + - clusterVersion: + checkName: GKE-Version + outcomes: + - fail: + when: "< 1.22.0" + message: This application requires at least Kubernetes 1.20.0 or later, and recommends 1.22.0 + uri: https://www.kubernetes.io + - pass: + when: ">= 1.22.0" + message: Your cluster meets the recommended and required versions(>= 1.22.0) of Kubernetes + uri: https://www.kubernetes.io + - nodeResources: + checkName: At-Least-3-Nodes + outcomes: + - warn: + when: "count() < 3" + message: This application requires at least 3 nodes + - pass: + message: This cluster has enough nodes + extendAnalyzers: + - clusterAccess: + checkName: Check-K8S-Access + outcomes: + - fail: + message: K8s cluster access fail + - pass: + message: K8s cluster access ok + - taint: + checkName: Required-Taint-Match + outcomes: + - fail: + message: All nodes had taints that the pod didn't tolerate + - pass: + message: The taint matching succeeded + - storageClass: + checkName: Required-Default-SC + outcomes: + - warn: + message: The default storage class was not found. To learn more details, please check https://cloud.tencent.com/document/product/457/44235; Alternatively use option --set storageClass= when creating cluster + - pass: + message: Default storage class is the presence, and all good on storage classes \ No newline at end of file diff --git a/internal/cli/cmd/kubeblocks/install.go b/internal/cli/cmd/kubeblocks/install.go index 06f09ecef..247a5f2f4 100644 --- a/internal/cli/cmd/kubeblocks/install.go +++ b/internal/cli/cmd/kubeblocks/install.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -19,21 +22,23 @@ package kubeblocks import ( "bytes" "context" + "encoding/json" "fmt" "sort" "strings" "time" - "github.com/briandowns/spinner" + "github.com/pkg/errors" + "github.com/replicatedhq/troubleshoot/pkg/preflight" "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/spf13/viper" - "golang.org/x/exp/slices" + "golang.org/x/exp/maps" "helm.sh/helm/v3/pkg/cli/values" "helm.sh/helm/v3/pkg/repo" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" @@ -42,8 +47,8 @@ import ( "k8s.io/kubectl/pkg/util/templates" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" - "github.com/apecloud/kubeblocks/internal/cli/cmd/cluster" "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" @@ -51,7 +56,10 @@ import ( ) const ( - kMonitorParam = "prometheus.enabled=%[1]t,grafana.enabled=%[1]t" + kMonitorParam = "prometheus.enabled=%[1]t,grafana.enabled=%[1]t" + kNodeAffinity = "affinity.nodeAffinity=%s" + kPodAntiAffinity = "affinity.podAntiAffinity=%s" + kTolerations = "tolerations=%s" ) type Options struct { @@ -59,10 +67,12 @@ type Options struct { HelmCfg *helm.Config - // Namespace is the current namespace that the command is running + // Namespace is the current namespace the command running in Namespace string Client kubernetes.Interface Dynamic dynamic.Interface + Timeout time.Duration + Wait bool } type InstallOptions struct { @@ -73,14 +83,26 @@ type InstallOptions struct { CreateNamespace bool Check bool ValueOpts values.Options - timeout time.Duration + + // ConfiguredOptions is the options that kubeblocks + PodAntiAffinity string + TopologyKeys []string + NodeLabels map[string]string + TolerationsRaw []string +} + +type addonStatus struct { + allEnabled bool + allDisabled bool + hasFailed bool + outputMsg string } var ( installExample = templates.Examples(` - # Install KubeBlocks, the default version is same with the kbcli version, the default namespace is kb-system + # Install KubeBlocks, the default version is same with the kbcli version, the default namespace is kb-system kbcli kubeblocks install - + # Install KubeBlocks with specified version kbcli kubeblocks install --version=0.4.0 @@ -89,6 +111,10 @@ var ( # Install KubeBlocks with other settings, for example, set replicaCount to 3 kbcli kubeblocks install --set replicaCount=3`) + + spinnerMsg = func(format string, a ...any) spinner.Option { + return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...))) + } ) func newInstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -98,6 +124,13 @@ func newInstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr }, } + p := &PreflightOptions{ + PreflightFlags: preflight.NewPreflightFlags(), + IOStreams: streams, + } + *p.Interactive = false + *p.Format = "kbcli" + cmd := &cobra.Command{ Use: "install", Short: "Install KubeBlocks.", @@ -105,6 +138,8 @@ func newInstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr Example: installExample, Run: func(cmd *cobra.Command, args []string) { util.CheckErr(o.Complete(f, cmd)) + util.CheckErr(o.PreCheck()) + util.CheckErr(p.Preflight(f, args, o.ValueOpts)) util.CheckErr(o.Install()) }, } @@ -112,8 +147,14 @@ func newInstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr cmd.Flags().BoolVar(&o.Monitor, "monitor", true, "Auto install monitoring add-ons including prometheus, grafana and alertmanager-webhook-adaptor") cmd.Flags().StringVar(&o.Version, "version", version.DefaultKubeBlocksVersion, "KubeBlocks version") cmd.Flags().BoolVar(&o.CreateNamespace, "create-namespace", false, "Create the namespace if not present") - cmd.Flags().BoolVar(&o.Check, "check", true, "Check kubernetes environment before install") - cmd.Flags().DurationVar(&o.timeout, "timeout", 1800*time.Second, "Time to wait for installing KubeBlocks") + cmd.Flags().BoolVar(&o.Check, "check", true, "Check kubernetes environment before installation") + cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for installing KubeBlocks, such as --timeout=10m") + cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be ready, including all the auto installed add-ons. It will wait for a --timeout period") + cmd.Flags().BoolVar(&p.force, flagForce, p.force, "If present, just print fail item and continue with the following steps") + cmd.Flags().StringVar(&o.PodAntiAffinity, "pod-anti-affinity", "", "Pod anti-affinity type, one of: (Preferred, Required)") + cmd.Flags().StringArrayVar(&o.TopologyKeys, "topology-keys", nil, "Topology keys for affinity") + cmd.Flags().StringToStringVar(&o.NodeLabels, "node-labels", nil, "Node label selector") + cmd.Flags().StringSliceVar(&o.TolerationsRaw, "tolerations", nil, `Tolerations for Kubeblocks, such as '"dev=true:NoSchedule,large=true:NoSchedule"'`) helm.AddValueOptionsFlags(cmd.Flags(), &o.ValueOpts) return cmd @@ -121,6 +162,12 @@ func newInstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command) error { var err error + + // default write log to file + if err = util.EnableLogToFile(cmd.Flags()); err != nil { + fmt.Fprintf(o.Out, "Failed to enable the log file %s", err.Error()) + } + if o.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace(); err != nil { return err } @@ -153,17 +200,20 @@ func (o *Options) Complete(f cmdutil.Factory, cmd *cobra.Command) error { return err } -func (o *InstallOptions) Install() error { +func (o *InstallOptions) PreCheck() error { // check if KubeBlocks has been installed - versionInfo, err := util.GetVersionInfo(o.Client) + v, err := util.GetVersionInfo(o.Client) if err != nil { return err } - if v := versionInfo[util.KubeBlocksApp]; len(v) > 0 { - printer.Warning(o.Out, "KubeBlocks %s already exists, repeated installation is not supported.\n\n", v) + // Todo: KubeBlocks maybe already installed but it's status could be Failed. + // For example: 'kbcli playground init' in windows will fail and try 'kbcli playground init' again immediately, + // kbcli will output SUCCESSFULLY, however the addon csi is still failed and KubeBlocks is not installed SUCCESSFULLY + if v.KubeBlocks != "" { + printer.Warning(o.Out, "KubeBlocks %s already exists, repeated installation is not supported.\n\n", v.KubeBlocks) fmt.Fprintln(o.Out, "If you want to upgrade it, please use \"kbcli kubeblocks upgrade\".") - return nil + return cmdutil.ErrExit } // check whether the namespace exists @@ -177,43 +227,80 @@ func (o *InstallOptions) Install() error { return err } - if err = o.preCheck(versionInfo); err != nil { + if err = o.checkVersion(v); err != nil { return err } + return nil +} +func (o *InstallOptions) Install() error { + var err error // add monitor parameters o.ValueOpts.Values = append(o.ValueOpts.Values, fmt.Sprintf(kMonitorParam, o.Monitor)) + // add pod anti-affinity + if o.PodAntiAffinity != "" || len(o.TopologyKeys) > 0 { + podAntiAffinityJSON, err := json.Marshal(util.BuildPodAntiAffinity(o.PodAntiAffinity, o.TopologyKeys)) + if err != nil { + return err + } + o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kPodAntiAffinity, podAntiAffinityJSON)) + } + + // add node affinity + if len(o.NodeLabels) > 0 { + nodeLabelsJSON, err := json.Marshal(util.BuildNodeAffinity(o.NodeLabels)) + if err != nil { + return err + } + o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kNodeAffinity, string(nodeLabelsJSON))) + } + + // parse tolerations and add to values + if len(o.TolerationsRaw) > 0 { + tolerations, err := util.BuildTolerations(o.TolerationsRaw) + if err != nil { + return err + } + tolerationsJSON, err := json.Marshal(tolerations) + if err != nil { + return err + } + o.ValueOpts.JSONValues = append(o.ValueOpts.JSONValues, fmt.Sprintf(kTolerations, string(tolerationsJSON))) + } + // add helm repo - spinner := printer.Spinner(o.Out, "%-50s", "Add and update repo "+types.KubeBlocksRepoName) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Add and update repo "+types.KubeBlocksRepoName)) + defer s.Fail() // Add repo, if exists, will update it if err = helm.AddRepo(&repo.Entry{Name: types.KubeBlocksRepoName, URL: util.GetHelmChartRepoURL()}); err != nil { return err } - spinner(true) + s.Success() - // install KubeBlocks chart - spinner = printer.Spinner(o.Out, "%-50s", "Install KubeBlocks "+o.Version) - defer spinner(false) + // install KubeBlocks + s = spinner.New(o.Out, spinnerMsg("Install KubeBlocks "+o.Version)) + defer s.Fail() if err = o.installChart(); err != nil { return err } - spinner(true) + s.Success() // wait for auto-install addons to be ready if err = o.waitAddonsEnabled(); err != nil { - return err - } - - // create VolumeSnapshotClass - if err = o.createVolumeSnapshotClass(); err != nil { + fmt.Fprintf(o.Out, "Failed to wait for auto-install addons to be enabled, run \"kbcli kubeblocks status\" to check the status\n") return err } if !o.Quiet { - fmt.Fprintf(o.Out, "\nKubeBlocks %s installed to namespace %s SUCCESSFULLY!\n", - o.Version, o.HelmCfg.Namespace()) + msg := fmt.Sprintf("\nKubeBlocks %s installed to namespace %s SUCCESSFULLY!\n", o.Version, o.HelmCfg.Namespace()) + if !o.Wait { + msg = fmt.Sprintf(` +KubeBlocks %s is installing to namespace %s. +You can check the KubeBlocks status by running "kbcli kubeblocks status" +`, o.Version, o.HelmCfg.Namespace()) + } + fmt.Fprint(o.Out, msg) o.printNotes() } return nil @@ -221,127 +308,93 @@ func (o *InstallOptions) Install() error { // waitAddonsEnabled waits for auto-install addons status to be enabled func (o *InstallOptions) waitAddonsEnabled() error { - addons := make(map[string]bool) - checkAddons := func() (bool, error) { - allEnabled := true - objects, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{ - LabelSelector: buildAddonLabelSelector(), + if !o.Wait { + return nil + } + + addons := make(map[string]*extensionsv1alpha1.Addon) + fetchAddons := func() error { + objs, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{ + LabelSelector: buildKubeBlocksSelectorLabels(), }) if err != nil && !apierrors.IsNotFound(err) { - return false, err + return err } - if objects == nil || len(objects.Items) == 0 { + if objs == nil || len(objs.Items) == 0 { klog.V(1).Info("No Addons found") - return true, nil + return nil } - for _, obj := range objects.Items { - addon := extensionsv1alpha1.Addon{} - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &addon); err != nil { - return false, err + for _, obj := range objs.Items { + addon := &extensionsv1alpha1.Addon{} + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil { + return err } if addon.Status.ObservedGeneration == 0 { klog.V(1).Infof("Addon %s is not observed yet", addon.Name) - allEnabled = false continue } - installable := false - if addon.Spec.InstallSpec != nil { - installable = addon.Spec.Installable.AutoInstall - } - - klog.V(1).Infof("Addon: %s, enabled: %v, status: %s, auto-install: %v", - addon.Name, addon.Spec.InstallSpec.GetEnabled(), addon.Status.Phase, installable) - // addon is enabled, then check its status + // addon should be auto installed, check its status if addon.Spec.InstallSpec.GetEnabled() { - addons[addon.Name] = true + addons[addon.Name] = addon if addon.Status.Phase != extensionsv1alpha1.AddonEnabled { - klog.V(1).Infof("Addon %s is not enabled yet", addon.Name) - addons[addon.Name] = false - allEnabled = false + klog.V(1).Infof("Addon %s is not enabled yet, status %s", addon.Name, addon.Status.Phase) + } + if addon.Status.Phase == extensionsv1alpha1.AddonFailed { + klog.V(1).Infof("Addon %s failed:", addon.Name) + for _, c := range addon.Status.Conditions { + klog.V(1).Infof(" %s: %s", c.Reason, c.Message) + } } } } - return allEnabled, nil + return nil } - okMsg := func(msg string) string { - return fmt.Sprintf("%-50s %s\n", msg, printer.BoldGreen("OK")) - } - failMsg := func(msg string) string { - return fmt.Sprintf("%-50s %s\n", msg, printer.BoldRed("FAIL")) - } suffixMsg := func(msg string) string { - return fmt.Sprintf(" %-50s", msg) + return fmt.Sprintf("%-50s", msg) } // create spinner - msg := "Wait for addons to be ready" - s := spinner.New(spinner.CharSets[11], 100*time.Millisecond) - s.Writer = o.Out - _ = s.Color("cyan") - s.Suffix = suffixMsg(msg) - s.Start() - - var prevUnready []string - // check addon installing progress - checkProgress := func() { - if len(addons) == 0 { - return - } - unready := make([]string, 0) - ready := make([]string, 0) - for k, v := range addons { - if v { - ready = append(ready, k) - } else { - unready = append(unready, k) - } - } - sort.Strings(unready) - s.Suffix = suffixMsg(fmt.Sprintf("%s\n %s", msg, strings.Join(unready, "\n "))) - for _, r := range ready { - if !slices.Contains(prevUnready, r) { - continue - } - s.FinalMSG = okMsg("Addon " + r) - s.Stop() - s.Suffix = suffixMsg(fmt.Sprintf("%s\n %s", msg, strings.Join(unready, "\n "))) - s.Start() - } - prevUnready = unready - } - + msg := "" + header := "Wait for addons to be enabled" + failedErr := errors.New("some addons are failed to be enabled") + s := spinner.New(o.Out, spinnerMsg(header)) var ( - allEnabled bool - err error + err error + spinnerDone = func() { + s.SetFinalMsg(msg) + s.Done("") + fmt.Fprintln(o.Out) + } ) - // wait for all auto-install addons to be enabled - for i := 0; i < viper.GetInt("KB_WAIT_ADDON_TIMES"); i++ { - allEnabled, err = checkAddons() - if err != nil { - s.FinalMSG = failMsg(msg) - s.Stop() - return err + // wait all addons to be enabled, or timeout + if err = wait.PollImmediate(5*time.Second, o.Timeout, func() (bool, error) { + if err = fetchAddons(); err != nil || len(addons) == 0 { + return false, err } - checkProgress() - if allEnabled { - s.FinalMSG = okMsg(msg) - s.Stop() - return nil + status := checkAddons(maps.Values(addons), true) + msg = suffixMsg(fmt.Sprintf("%s\n %s", header, status.outputMsg)) + s.SetMessage(msg) + if status.allEnabled { + spinnerDone() + return true, nil + } else if status.hasFailed { + return false, failedErr } - time.Sleep(5 * time.Second) + return false, nil + }); err != nil { + spinnerDone() + printAddonMsg(o.Out, maps.Values(addons), true) + return err } - // timeout to wait for all auto-install addons to be enabled - s.FinalMSG = fmt.Sprintf("%-50s %s\n", msg, printer.BoldRed("TIMEOUT")) - s.Stop() return nil } -func (o *InstallOptions) preCheck(versionInfo map[util.AppName]string) error { +func (o *InstallOptions) checkVersion(v util.Version) error { if !o.Check { return nil } @@ -355,21 +408,21 @@ func (o *InstallOptions) preCheck(versionInfo map[util.AppName]string) error { } versionErr := fmt.Errorf("failed to get kubernetes version") - k8sVersionStr, ok := versionInfo[util.KubernetesApp] - if !ok { + k8sVersionStr := v.Kubernetes + if k8sVersionStr == "" { return versionErr } - version := util.GetK8sVersion(k8sVersionStr) - if len(version) == 0 { + semVer := util.GetK8sSemVer(k8sVersionStr) + if len(semVer) == 0 { return versionErr } // output kubernetes version - fmt.Fprintf(o.Out, "Kubernetes version %s\n", ""+version) + fmt.Fprintf(o.Out, "Kubernetes version %s\n", ""+semVer) // disable or enable some features according to the kubernetes environment - provider, err := util.GetK8sProvider(version, o.Client) + provider, err := util.GetK8sProvider(k8sVersionStr, o.Client) if err != nil { return fmt.Errorf("failed to get kubernetes provider: %v", err) } @@ -378,7 +431,7 @@ func (o *InstallOptions) preCheck(versionInfo map[util.AppName]string) error { } // check kbcli version, now do nothing - fmt.Fprintf(o.Out, "kbcli version %s\n", versionInfo[util.KBCLIApp]) + fmt.Fprintf(o.Out, "kbcli version %s\n", v.Cli) return nil } @@ -413,7 +466,7 @@ func (o *InstallOptions) checkRemainedResource() error { // the addon resources. objs, err := getKBObjects(o.Dynamic, ns, nil) if err != nil { - fmt.Fprintf(o.ErrOut, "Check whether there are resources left by KubeBlocks before: %s\n", err.Error()) + fmt.Fprintf(o.ErrOut, "Failed to get resources left by KubeBlocks before: %s\n", err.Error()) } res := getRemainedResource(objs) @@ -463,57 +516,17 @@ Note: Monitoring add-ons are not installed. } } -func (o *InstallOptions) createVolumeSnapshotClass() error { - createFunc := func() error { - options := cluster.CreateVolumeSnapshotClassOptions{} - options.BaseOptions.Dynamic = o.Dynamic - options.BaseOptions.IOStreams = o.IOStreams - options.BaseOptions.Quiet = true - - spinner := printer.Spinner(o.Out, "%-50s", "Configure VolumeSnapshotClass") - defer spinner(false) - - if err := options.Complete(); err != nil { - return err - } - if err := options.Create(); err != nil { - return err - } - spinner(true) - return nil - } - - var sets []string - for _, set := range o.ValueOpts.Values { - splitSet := strings.Split(set, ",") - sets = append(sets, splitSet...) - } - for _, set := range sets { - if set != "snapshot-controller.enabled=true" { - continue - } - - if err := createFunc(); err != nil { - return err - } else { - // only need to create once - return nil - } - } - return nil -} - func (o *InstallOptions) buildChart() *helm.InstallOpts { return &helm.InstallOpts{ Name: types.KubeBlocksChartName, Chart: types.KubeBlocksChartName + "/" + types.KubeBlocksChartName, - Wait: true, + Wait: o.Wait, Version: o.Version, Namespace: o.HelmCfg.Namespace(), ValueOpts: &o.ValueOpts, TryTimes: 2, CreateNamespace: o.CreateNamespace, - Timeout: o.timeout, + Timeout: o.Timeout, Atomic: true, } } diff --git a/internal/cli/cmd/kubeblocks/install_test.go b/internal/cli/cmd/kubeblocks/install_test.go index 4e257c490..09fa7ac02 100644 --- a/internal/cli/cmd/kubeblocks/install_test.go +++ b/internal/cli/cmd/kubeblocks/install_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -23,7 +26,6 @@ import ( . "github.com/onsi/gomega" "github.com/spf13/cobra" - "helm.sh/helm/v3/pkg/cli/values" "k8s.io/cli-runtime/pkg/genericclioptions" clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" @@ -88,30 +90,13 @@ var _ = Describe("kubeblocks install", func() { CreateNamespace: true, } Expect(o.Install()).Should(HaveOccurred()) - Expect(len(o.ValueOpts.Values)).To(Equal(1)) + Expect(o.ValueOpts.Values).Should(HaveLen(1)) Expect(o.ValueOpts.Values[0]).To(Equal(fmt.Sprintf(kMonitorParam, true))) Expect(o.installChart()).Should(HaveOccurred()) o.printNotes() }) - It("create volumeSnapshotClass", func() { - o := &InstallOptions{ - Options: Options{ - IOStreams: streams, - HelmCfg: helm.NewFakeConfig(namespace), - Namespace: "default", - Client: testing.FakeClientSet(), - Dynamic: testing.FakeDynamicClient(testing.FakeVolumeSnapshotClass()), - }, - Version: version.DefaultKubeBlocksVersion, - Monitor: true, - CreateNamespace: true, - ValueOpts: values.Options{Values: []string{"snapshot-controller.enabled=true"}}, - } - Expect(o.createVolumeSnapshotClass()).Should(HaveOccurred()) - }) - - It("preCheck", func() { + It("checkVersion", func() { o := &InstallOptions{ Options: Options{ IOStreams: genericclioptions.NewTestIOStreamsDiscard(), @@ -120,18 +105,15 @@ var _ = Describe("kubeblocks install", func() { Check: true, } By("kubernetes version is empty") - versionInfo := map[util.AppName]string{} - Expect(o.preCheck(versionInfo).Error()).Should(ContainSubstring("failed to get kubernetes version")) - - versionInfo[util.KubernetesApp] = "" - Expect(o.preCheck(versionInfo).Error()).Should(ContainSubstring("failed to get kubernetes version")) + v := util.Version{} + Expect(o.checkVersion(v).Error()).Should(ContainSubstring("failed to get kubernetes version")) By("kubernetes is provided by cloud provider") - versionInfo[util.KubernetesApp] = "v1.25.0-eks" - Expect(o.preCheck(versionInfo)).Should(Succeed()) + v.Kubernetes = "v1.25.0-eks" + Expect(o.checkVersion(v)).Should(Succeed()) By("kubernetes is not provided by cloud provider") - versionInfo[util.KubernetesApp] = "v1.25.0" - Expect(o.preCheck(versionInfo)).Should(Succeed()) + v.Kubernetes = "v1.25.0" + Expect(o.checkVersion(v)).Should(Succeed()) }) }) diff --git a/internal/cli/cmd/kubeblocks/kubeblocks.go b/internal/cli/cmd/kubeblocks/kubeblocks.go index a97f0bf82..256e8a913 100644 --- a/internal/cli/cmd/kubeblocks/kubeblocks.go +++ b/internal/cli/cmd/kubeblocks/kubeblocks.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -35,8 +38,9 @@ func NewKubeBlocksCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *c newUninstallCmd(f, streams), newListVersionsCmd(streams), newStatusCmd(f, streams), + NewConfigCmd(f, streams), + NewDescribeConfigCmd(f, streams), + NewPreflightCmd(f, streams), ) - // add preflight cmd - cmd.AddCommand(NewPreflightCmd(f, streams)) return cmd } diff --git a/internal/cli/cmd/kubeblocks/kubeblocks_objects.go b/internal/cli/cmd/kubeblocks/kubeblocks_objects.go index a5ccc51b7..584d9feff 100644 --- a/internal/cli/cmd/kubeblocks/kubeblocks_objects.go +++ b/internal/cli/cmd/kubeblocks/kubeblocks_objects.go @@ -1,24 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks import ( "context" - "fmt" "strings" corev1 "k8s.io/api/core/v1" @@ -35,13 +37,20 @@ import ( extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/constant" ) +type resourceScope string + +const ( + ResourceScopeGlobal resourceScope = "global" + ResourceScopeLocal resourceScope = "namespaced" +) + type kbObjects map[schema.GroupVersionResource]*unstructured.UnstructuredList var ( + // addon resources resourceGVRs = []schema.GroupVersionResource{ types.DeployGVR(), types.StatefulSetGVR(), @@ -52,7 +61,7 @@ var ( } ) -// getKBObjects returns all KubeBlocks objects include addons objects +// getKBObjects returns all KubeBlocks objects including addons objects func getKBObjects(dynamic dynamic.Interface, namespace string, addons []*extensionsv1alpha1.Addon) (kbObjects, error) { var ( err error @@ -94,52 +103,17 @@ func getKBObjects(dynamic dynamic.Interface, namespace string, addons []*extensi } } - getWebhooks := func(gvr schema.GroupVersionResource) { - objs, err := dynamic.Resource(gvr).List(ctx, metav1.ListOptions{}) - if err != nil { - appendErr(err) - return - } - result := &unstructured.UnstructuredList{} - for _, obj := range objs.Items { - if !strings.Contains(obj.GetName(), strings.ToLower(string(util.KubeBlocksApp))) { - continue - } - result.Items = append(result.Items, obj) - } - kbObjs[gvr] = result - } - getWebhooks(types.ValidatingWebhookConfigurationGVR()) - getWebhooks(types.MutatingWebhookConfigurationGVR()) - - // get cluster roles and cluster role bindings by label - getRBACResources := func(labelSelector string, gvr schema.GroupVersionResource, global bool) { + getObjectsByLabels := func(labelSelector string, gvr schema.GroupVersionResource, scope resourceScope) { ns := namespace - if global { + if scope == ResourceScopeGlobal { ns = metav1.NamespaceAll } + + klog.V(1).Infof("search objects by labels, namespace: %s, name: %s, gvr: %s", labelSelector, gvr, scope) objs, err := dynamic.Resource(gvr).Namespace(ns).List(context.TODO(), metav1.ListOptions{ LabelSelector: labelSelector, }) - if err != nil { - appendErr(err) - return - } - - if _, ok := kbObjs[gvr]; !ok { - kbObjs[gvr] = &unstructured.UnstructuredList{} - } - target := kbObjs[gvr] - target.Items = append(target.Items, objs.Items...) - } - getRBACResources(buildAddonLabelSelector(), types.ClusterRoleGVR(), true) - getRBACResources(buildAddonLabelSelector(), types.ClusterRoleBindingGVR(), true) - // get objects by label selector - getObjects := func(labelSelector string, gvr schema.GroupVersionResource) { - objs, err := dynamic.Resource(gvr).Namespace(namespace).List(context.TODO(), metav1.ListOptions{ - LabelSelector: labelSelector, - }) if err != nil { appendErr(err) return @@ -150,10 +124,16 @@ func getKBObjects(dynamic dynamic.Interface, namespace string, addons []*extensi } target := kbObjs[gvr] target.Items = append(target.Items, objs.Items...) + if klog.V(1).Enabled() { + for _, item := range objs.Items { + klog.Infof("\tget object: %s, %s, %s", item.GetNamespace(), item.GetKind(), item.GetName()) + } + } } // get object by name - getObject := func(name string, gvr schema.GroupVersionResource) { + getObjectByName := func(name string, gvr schema.GroupVersionResource) { + klog.V(1).Infof("search object by name, namespace: %s, name: %s, gvr: %s ", namespace, name, gvr) obj, err := dynamic.Resource(gvr).Namespace(namespace).Get(context.TODO(), name, metav1.GetOptions{}) if err != nil { appendErr(err) @@ -164,23 +144,29 @@ func getKBObjects(dynamic dynamic.Interface, namespace string, addons []*extensi } target := kbObjs[gvr] target.Items = append(target.Items, *obj) + klog.V(1).Infof("\tget object: %s, %s, %s", obj.GetNamespace(), obj.GetKind(), obj.GetName()) } + // get RBAC resources, such as ClusterRole, ClusterRoleBinding, Role, RoleBinding, ServiceAccount + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.ClusterRoleGVR(), ResourceScopeGlobal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.ClusterRoleBindingGVR(), ResourceScopeGlobal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.RoleGVR(), ResourceScopeLocal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.RoleBindingGVR(), ResourceScopeLocal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.ServiceAccountGVR(), ResourceScopeLocal) + // get webhooks + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.ValidatingWebhookConfigurationGVR(), ResourceScopeGlobal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.MutatingWebhookConfigurationGVR(), ResourceScopeGlobal) + // get configmap for config template + getObjectsByLabels(buildConfigTypeSelectorLabels(), types.ConfigmapGVR(), ResourceScopeLocal) + getObjectsByLabels(buildKubeBlocksSelectorLabels(), types.ConfigmapGVR(), ResourceScopeLocal) // get resources which label matches app.kubernetes.io/instance=kubeblocks or // label matches release=kubeblocks, like prometheus-server for _, selector := range buildResourceLabelSelectors(addons) { for _, gvr := range resourceGVRs { - getObjects(selector, gvr) + getObjectsByLabels(selector, gvr, ResourceScopeLocal) } } - // build label selector - configMapLabelSelector := fmt.Sprintf("%s=%s", constant.CMConfigurationTypeLabelKey, constant.ConfigTemplateType) - - // get configmap - getObjects(configMapLabelSelector, types.ConfigmapGVR()) - getObjects(buildAddonLabelSelector(), types.ConfigmapGVR()) - // get PVs by PVC if pvcs, ok := kbObjs[types.PVCGVR()]; ok { for _, obj := range pvcs.Items { @@ -189,10 +175,9 @@ func getKBObjects(dynamic dynamic.Interface, namespace string, addons []*extensi appendErr(err) continue } - getObject(pvc.Spec.VolumeName, types.PVGVR()) + getObjectByName(pvc.Spec.VolumeName, types.PVGVR()) } } - return kbObjs, utilerrors.NewAggregate(allErrs) } diff --git a/internal/cli/cmd/kubeblocks/kubeblocks_objects_test.go b/internal/cli/cmd/kubeblocks/kubeblocks_objects_test.go index bc6f63c2f..6bbc14e7f 100644 --- a/internal/cli/cmd/kubeblocks/kubeblocks_objects_test.go +++ b/internal/cli/cmd/kubeblocks/kubeblocks_objects_test.go @@ -1,37 +1,46 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks import ( + "context" + + "github.com/google/uuid" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/client-go/dynamic" + "k8s.io/apimachinery/pkg/runtime/schema" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/constant" ) var _ = Describe("kubeblocks objects", func() { + It("delete objects", func() { dynamic := testing.FakeDynamicClient() Expect(deleteObjects(dynamic, types.DeployGVR(), nil)).Should(Succeed()) @@ -92,20 +101,65 @@ var _ = Describe("kubeblocks objects", func() { } for _, c := range testCases { - client := mockDynamicClientWithCRD(c.clusterDef, c.clusterVersion, c.backupTool) + objects := mockCRD() + objects = append(objects, testing.FakeVolumeSnapshotClass()) + objects = append(objects, c.clusterDef, c.clusterVersion, c.backupTool) + client := testing.FakeDynamicClient(objects...) objs, _ := getKBObjects(client, "", nil) Expect(removeCustomResources(client, objs)).Should(Succeed()) } }) It("delete crd", func() { - dynamic := mockDynamicClientWithCRD() + objects := mockCRD() + objects = append(objects, testing.FakeVolumeSnapshotClass()) + dynamic := testing.FakeDynamicClient(objects...) objs, _ := getKBObjects(dynamic, "", nil) Expect(deleteObjects(dynamic, types.CRDGVR(), objs[types.CRDGVR()])).Should(Succeed()) }) + + It("test getKBObjects", func() { + objects := mockCRD() + objects = append(objects, mockCRs()...) + objects = append(objects, testing.FakeVolumeSnapshotClass()) + objects = append(objects, mockRBACResources()...) + objects = append(objects, mockConfigMaps()...) + dynamic := testing.FakeDynamicClient(objects...) + objs, _ := getKBObjects(dynamic, "", nil) + + tmp, err := dynamic.Resource(types.ClusterRoleGVR()).Namespace(metav1.NamespaceAll). + List(context.TODO(), metav1.ListOptions{LabelSelector: buildKubeBlocksSelectorLabels()}) + Expect(err).ShouldNot(HaveOccurred()) + Expect(tmp.Items).Should(HaveLen(1)) + // verify crds + Expect(objs[types.CRDGVR()].Items).Should(HaveLen(4)) + // verify crs + for _, gvr := range []schema.GroupVersionResource{types.ClusterDefGVR(), types.ClusterVersionGVR()} { + objlist, ok := objs[gvr] + Expect(ok).Should(BeTrue()) + Expect(objlist.Items).Should(HaveLen(1)) + } + + // verify rbac info + for _, gvr := range []schema.GroupVersionResource{types.RoleGVR(), types.ClusterRoleBindingGVR(), types.ServiceAccountGVR()} { + objlist, ok := objs[gvr] + Expect(ok).Should(BeTrue()) + Expect(objlist.Items).Should(HaveLen(1), gvr.String()) + } + // verify cofnig tpl + for _, gvr := range []schema.GroupVersionResource{types.ConfigmapGVR()} { + objlist, ok := objs[gvr] + Expect(ok).Should(BeTrue()) + Expect(objlist.Items).Should(HaveLen(1), gvr.String()) + } + }) }) -func mockDynamicClientWithCRD(objects ...runtime.Object) dynamic.Interface { +func mockName() string { + return uuid.NewString() +} + +func mockCRD() []runtime.Object { clusterCRD := v1.CustomResourceDefinition{ TypeMeta: metav1.TypeMeta{ Kind: "CustomResourceDefinition", @@ -159,9 +213,34 @@ func mockDynamicClientWithCRD(objects ...runtime.Object) dynamic.Interface { }, Status: v1.CustomResourceDefinitionStatus{}, } + return []runtime.Object{&clusterCRD, &clusterDefCRD, &clusterVersionCRD, &backupToolCRD} +} + +func mockCRs() []runtime.Object { + allObjects := make([]runtime.Object, 0) + allObjects = append(allObjects, testing.FakeClusterDef()) + allObjects = append(allObjects, testing.FakeClusterVersion()) + return allObjects +} + +func mockRBACResources() []runtime.Object { + sa := testing.FakeServiceAccount(mockName()) - allObjs := []runtime.Object{&clusterCRD, &clusterDefCRD, &clusterVersionCRD, &backupToolCRD, - testing.FakeVolumeSnapshotClass()} - allObjs = append(allObjs, objects...) - return testing.FakeDynamicClient(allObjs...) + cluserRole := testing.FakeClusterRole(mockName()) + cluserRoleBinding := testing.FakeClusterRoleBinding(mockName(), sa, cluserRole) + + role := testing.FakeRole(mockName()) + roleBinding := testing.FakeRoleBinding(mockName(), sa, role) + + return []runtime.Object{sa, cluserRole, cluserRoleBinding, role, roleBinding} +} + +func mockConfigMaps() []runtime.Object { + obj := testing.FakeConfigMap(mockName()) + // add a config tpl label + if obj.ObjectMeta.Labels == nil { + obj.ObjectMeta.Labels = make(map[string]string) + } + obj.ObjectMeta.Labels[constant.CMConfigurationTypeLabelKey] = constant.ConfigTemplateType + return []runtime.Object{obj} } diff --git a/internal/cli/cmd/kubeblocks/kubeblocks_test.go b/internal/cli/cmd/kubeblocks/kubeblocks_test.go index 2b373fe64..002619112 100644 --- a/internal/cli/cmd/kubeblocks/kubeblocks_test.go +++ b/internal/cli/cmd/kubeblocks/kubeblocks_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks diff --git a/internal/cli/cmd/kubeblocks/list_versions.go b/internal/cli/cmd/kubeblocks/list_versions.go index 799a035e7..4e7750dbc 100644 --- a/internal/cli/cmd/kubeblocks/list_versions.go +++ b/internal/cli/cmd/kubeblocks/list_versions.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -37,7 +40,7 @@ const ( var ( listVersionsExample = templates.Examples(` - # list KubeBlocks release version + # list KubeBlocks release versions kbcli kubeblocks list-versions # list KubeBlocks versions including development versions, such as alpha, beta and release candidate diff --git a/internal/cli/cmd/kubeblocks/list_versions_test.go b/internal/cli/cmd/kubeblocks/list_versions_test.go index 93bc75f0c..a485d31db 100644 --- a/internal/cli/cmd/kubeblocks/list_versions_test.go +++ b/internal/cli/cmd/kubeblocks/list_versions_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks diff --git a/internal/cli/cmd/kubeblocks/preflight.go b/internal/cli/cmd/kubeblocks/preflight.go index a6452a1e1..3b5378be8 100644 --- a/internal/cli/cmd/kubeblocks/preflight.go +++ b/internal/cli/cmd/kubeblocks/preflight.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -22,28 +25,27 @@ import ( "fmt" "os" "os/signal" + "strings" - "github.com/ahmetalpbalkan/go-cursor" - "github.com/fatih/color" "github.com/pkg/errors" analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" "github.com/replicatedhq/troubleshoot/pkg/preflight" "github.com/spf13/cobra" "golang.org/x/sync/errgroup" + "helm.sh/helm/v3/pkg/cli/values" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/util" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" kbpreflight "github.com/apecloud/kubeblocks/internal/preflight" - kbinteractive "github.com/apecloud/kubeblocks/internal/preflight/interactive" ) const ( - flagInteractive = "interactive" - flagFormat = "format" flagCollectorImage = "collector-image" flagCollectorPullPolicy = "collector-pullpolicy" flagCollectWithoutPermissions = "collect-without-permissions" @@ -54,6 +56,12 @@ const ( flagDebug = "debug" flagNamespace = "namespace" flagVerbose = "verbose" + flagForce = "force" + flagFormat = "format" + + PreflightPattern = "data/%s_preflight.yaml" + HostPreflightPattern = "data/%s_hostpreflight.yaml" + PreflightMessage = "Kubernetes cluster preflight" ) var ( @@ -73,11 +81,6 @@ var ( kbcli kubeblocks preflight preflight-check.yaml --interactive=true`) ) -const ( - EKSHostPreflight = "data/eks_hostpreflight.yaml" - EKSPreflight = "data/eks_preflight.yaml" -) - // PreflightOptions declares the arguments accepted by the preflight command type PreflightOptions struct { factory cmdutil.Factory @@ -87,6 +90,8 @@ type PreflightOptions struct { checkYamlData [][]byte namespace string verbose bool + force bool + ValueOpts values.Options } func NewPreflightCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -100,17 +105,14 @@ func NewPreflightCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *co Short: "Run and retrieve preflight checks for KubeBlocks.", Example: preflightExample, Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(p.complete(f, args)) - util.CheckErr(p.validate()) - util.CheckErr(p.run()) + util.CheckErr(p.Preflight(f, args, values.Options{})) }, } // add flags - cmd.Flags().BoolVar(p.Interactive, flagInteractive, false, "interactive preflights, default value is false") - cmd.Flags().StringVar(p.Format, flagFormat, "yaml", "output format, one of human, json, yaml. only used when interactive is set to false, default format is yaml") + cmd.Flags().StringVar(p.Format, flagFormat, "yaml", "output format, one of json, yaml. only used when interactive is set to false, default format is yaml") cmd.Flags().StringVar(p.CollectorImage, flagCollectorImage, *p.CollectorImage, "the full name of the collector image to use") cmd.Flags().StringVar(p.CollectorPullPolicy, flagCollectorPullPolicy, *p.CollectorPullPolicy, "the pull policy of the collector image") - cmd.Flags().BoolVar(p.CollectWithoutPermissions, flagCollectWithoutPermissions, *p.CollectWithoutPermissions, "always run preflight checks even if some require permissions that preflight does not have") + cmd.Flags().BoolVar(p.CollectWithoutPermissions, flagCollectWithoutPermissions, *p.CollectWithoutPermissions, "always run preflight checks even if some required permissions that preflight does not have") cmd.Flags().StringVar(p.Selector, flagSelector, *p.Selector, "selector (label query) to filter remote collection nodes on.") cmd.Flags().StringVar(p.SinceTime, flagSinceTime, *p.SinceTime, "force pod logs collectors to return logs after a specific date (RFC3339)") cmd.Flags().StringVar(p.Since, flagSince, *p.Since, "force pod logs collectors to return logs newer than a relative duration like 5s, 2m, or 3h.") @@ -123,47 +125,66 @@ func NewPreflightCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *co func LoadVendorCheckYaml(vendorName util.K8sProvider) ([][]byte, error) { var yamlDataList [][]byte - switch vendorName { - case util.EKSProvider: - if data, err := defaultVendorYamlData.ReadFile(EKSHostPreflight); err == nil { - yamlDataList = append(yamlDataList, data) - } - if data, err := defaultVendorYamlData.ReadFile(EKSPreflight); err == nil { - yamlDataList = append(yamlDataList, data) - } - case util.UnknownProvider: - fallthrough - default: - fmt.Println("unsupported k8s provider, and the validation of provider will coming soon") - return yamlDataList, errors.New("no supported provider") + if data, err := defaultVendorYamlData.ReadFile(newPreflightPath(vendorName)); err == nil { + yamlDataList = append(yamlDataList, data) + } + if data, err := defaultVendorYamlData.ReadFile(newHostPreflightPath(vendorName)); err == nil { + yamlDataList = append(yamlDataList, data) + } + if len(yamlDataList) == 0 { + return yamlDataList, errors.New("unsupported k8s provider") } return yamlDataList, nil } -func (p *PreflightOptions) complete(factory cmdutil.Factory, args []string) error { +func (p *PreflightOptions) Preflight(f cmdutil.Factory, args []string, opts values.Options) error { + // if force flag set, skip preflight + + if p.force { + return nil + } + p.ValueOpts = opts + + var err error + if err = p.complete(f, args); err != nil { + if intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeSkipPreflight) { + return nil + } + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) + } + if err = p.run(); err != nil { + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) + } + return nil +} + +func (p *PreflightOptions) complete(f cmdutil.Factory, args []string) error { // default no args, and run default validating vendor if len(args) == 0 { - clientSet, err := factory.KubernetesClientSet() + clientSet, err := f.KubernetesClientSet() if err != nil { - return errors.New("init k8s client failed, and please check kubeconfig") + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, "init k8s client failed, please check the kubeconfig") } versionInfo, err := util.GetVersionInfo(clientSet) if err != nil { - return errors.New("get k8s version of server failed, and please check your k8s accessibility") + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, "get k8s version of server failed, please check accessibility to k8s") } - vendorName, err := util.GetK8sProvider(versionInfo[util.KubernetesApp], clientSet) + vendorName, err := util.GetK8sProvider(versionInfo.Kubernetes, clientSet) if err != nil { - return errors.New("get k8s cloud provider failed, and please check your k8s accessibility") + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, "get k8s cloud provider failed, please check accessibility to k8s") } p.checkYamlData, err = LoadVendorCheckYaml(vendorName) if err != nil { - return err + return intctrlutil.NewError(intctrlutil.ErrorTypeSkipPreflight, err.Error()) } - color.New(color.FgCyan).Printf("current provider %s. collecting and analyzing data will take 10-20 seconds... \n", vendorName) } else { p.checkFileList = args - color.New(color.FgCyan).Println("collecting and analyzing data will take 10-20 seconds...") } + if len(p.checkFileList) < 1 && len(p.checkYamlData) < 1 { + return intctrlutil.NewError(intctrlutil.ErrorTypeSkipPreflight, "must specify at least one checks yaml") + } + + p.factory = f // conceal warning logs rest.SetDefaultWarningHandler(rest.NoWarnings{}) go func() { @@ -175,13 +196,6 @@ func (p *PreflightOptions) complete(factory cmdutil.Factory, args []string) erro return nil } -func (p *PreflightOptions) validate() error { - if len(p.checkFileList) < 1 && len(p.checkYamlData) < 1 { - return fmt.Errorf("must specify at least one checks yaml") - } - return nil -} - func (p *PreflightOptions) run() error { var ( kbPreflight *preflightv1beta2.Preflight @@ -191,10 +205,6 @@ func (p *PreflightOptions) run() error { preflightName string err error ) - if *p.Interactive { - fmt.Print(cursor.Hide()) - defer fmt.Print(cursor.Show()) - } // set progress chan progressCh := make(chan interface{}) defer close(progressCh) @@ -205,30 +215,34 @@ func (p *PreflightOptions) run() error { progressCollections.Go(CollectProgress(ctx, progressCh, p.verbose)) // 1. load yaml if kbPreflight, kbHostPreflight, preflightName, err = kbpreflight.LoadPreflightSpec(p.checkFileList, p.checkYamlData); err != nil { - return err + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) } // 2. collect data - collectResults, err = kbpreflight.CollectPreflight(ctx, kbPreflight, kbHostPreflight, progressCh) + s := spinner.New(p.Out, spinner.WithMessage(fmt.Sprintf("%-50s", "Collecting data from cluster"))) + collectResults, err = kbpreflight.CollectPreflight(p.factory, &p.ValueOpts, ctx, kbPreflight, kbHostPreflight, progressCh) if err != nil { - return err + s.Fail() + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) } + s.Success() + // 3. analyze data for _, res := range collectResults { analyzeResults = append(analyzeResults, res.Analyze()...) } cancelFunc() if err := progressCollections.Wait(); err != nil { - return err + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) } // 4. display analyzed data if len(analyzeResults) == 0 { - return errors.New("no data has been collected") + fmt.Fprintln(p.Out, "no data has been collected") + return nil } - if *p.Interactive { - return kbinteractive.ShowInteractiveResults(preflightName, analyzeResults, *p.Output) - } else { - return kbpreflight.ShowTextResults(preflightName, analyzeResults, *p.Format, p.verbose) + if err = kbpreflight.ShowTextResults(preflightName, analyzeResults, *p.Format, p.verbose, p.Out); err != nil { + return intctrlutil.NewError(intctrlutil.ErrorTypePreflightCommon, err.Error()) } + return nil } func CollectProgress(ctx context.Context, progressCh <-chan interface{}, verbose bool) func() error { @@ -252,3 +266,11 @@ func CollectProgress(ctx context.Context, progressCh <-chan interface{}, verbose } } } + +func newPreflightPath(vendorName util.K8sProvider) string { + return fmt.Sprintf(PreflightPattern, strings.ToLower(string(vendorName))) +} + +func newHostPreflightPath(vendorName util.K8sProvider) string { + return fmt.Sprintf(HostPreflightPattern, strings.ToLower(string(vendorName))) +} diff --git a/internal/cli/cmd/kubeblocks/preflight_test.go b/internal/cli/cmd/kubeblocks/preflight_test.go index ce0d860f9..cd9577194 100644 --- a/internal/cli/cmd/kubeblocks/preflight_test.go +++ b/internal/cli/cmd/kubeblocks/preflight_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -90,10 +93,8 @@ var _ = Describe("Preflight API Test", func() { PreflightFlags: preflight.NewPreflightFlags(), } Expect(p.complete(tf, nil)).Should(HaveOccurred()) - Expect(p.validate()).Should(HaveOccurred()) Expect(p.complete(tf, []string{"file1", "file2"})).Should(Succeed()) Expect(len(p.checkFileList)).Should(Equal(2)) - Expect(p.validate()).Should(Succeed()) }) It("run test", func() { @@ -110,12 +111,6 @@ var _ = Describe("Preflight API Test", func() { err := p.run() g.Expect(err).NotTo(HaveOccurred()) }).Should(Succeed()) - By("non-interactive mode, and expect error") - p.checkFileList = []string{"../../testing/testdata/hostpreflight_nil.yaml"} - Eventually(func(g Gomega) { - err := p.run() - g.Expect(err).To(HaveOccurred()) - }).Should(Succeed()) }) It("LoadVendorCheckYaml test, and expect fail", func() { @@ -129,4 +124,12 @@ var _ = Describe("Preflight API Test", func() { Expect(err).NotTo(HaveOccurred()) Expect(len(res)).Should(Equal(2)) }) + It("newPreflightPath test, and expect success", func() { + res := newPreflightPath("test") + Expect(res).Should(Equal("data/test_preflight.yaml")) + }) + It("newHostPreflightPath test, and expect success", func() { + res := newHostPreflightPath("test") + Expect(res).Should(Equal("data/test_hostpreflight.yaml")) + }) }) diff --git a/internal/cli/cmd/kubeblocks/status.go b/internal/cli/cmd/kubeblocks/status.go index da9889134..197b917f7 100644 --- a/internal/cli/cmd/kubeblocks/status.go +++ b/internal/cli/cmd/kubeblocks/status.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -19,6 +22,7 @@ package kubeblocks import ( "context" "fmt" + "strconv" "strings" "github.com/containerd/stargz-snapshotter/estargz/errorutil" @@ -30,7 +34,6 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/constant" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -40,31 +43,34 @@ import ( "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" + "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" - "k8s.io/metrics/pkg/apis/metrics/v1beta1" metrics "k8s.io/metrics/pkg/client/clientset/versioned" + + tablePrinter "github.com/jedib0t/go-pretty/v6/table" + text "github.com/jedib0t/go-pretty/v6/text" ) var ( infoExample = templates.Examples(` # list workloads owned by KubeBlocks kbcli kubeblocks status - + # list all resources owned by KubeBlocks, such as workloads, cluster definitions, backup template. kbcli kubeblocks status --all`) ) var ( - selectorList = []metav1.ListOptions{{LabelSelector: types.InstanceLabelSelector}, {LabelSelector: types.ReleaseLabelSelector}} - kubeBlocksWorkloads = []schema.GroupVersionResource{ types.DeployGVR(), types.StatefulSetGVR(), + types.DaemonSetGVR(), + types.JobGVR(), + types.CronJobGVR(), } kubeBlocksGlobalCustomResources = []schema.GroupVersionResource{ - types.BackupPolicyTemplateGVR(), types.BackupToolGVR(), types.ClusterDefGVR(), types.ClusterVersionGVR(), @@ -77,6 +83,17 @@ var ( types.ServiceGVR(), } + kubeBlocksClusterRBAC = []schema.GroupVersionResource{ + types.ClusterRoleGVR(), + types.ClusterRoleBindingGVR(), + } + + kubeBlocksNamespacedRBAC = []schema.GroupVersionResource{ + types.RoleGVR(), + types.RoleBindingGVR(), + types.ServiceAccountGVR(), + } + kubeBlocksStorages = []schema.GroupVersionResource{ types.PVCGVR(), } @@ -85,16 +102,18 @@ var ( types.ConfigmapGVR(), types.SecretGVR(), } + notAvailable = "N/A" ) type statusOptions struct { genericclioptions.IOStreams - client kubernetes.Interface - dynamic dynamic.Interface - mc metrics.Interface - showAll bool - ns string - addons []*extensionsv1alpha1.Addon + client kubernetes.Interface + dynamic dynamic.Interface + mc metrics.Interface + showAll bool + ns string + addons []*extensionsv1alpha1.Addon + selectorList []metav1.ListOptions } func newStatusCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { @@ -135,13 +154,6 @@ func (o *statusOptions) complete(f cmdutil.Factory) error { if err != nil { return err } - o.ns = metav1.NamespaceAll - return nil -} - -func (o *statusOptions) run() error { - ctx, cancel := context.WithCancel(context.Background()) - defer cancel() o.ns, _ = util.GetKubeBlocksNamespace(o.client) if o.ns == "" { @@ -151,6 +163,17 @@ func (o *statusOptions) run() error { fmt.Fprintf(o.Out, "Kuberblocks is deployed in namespace: %s\n", o.ns) } + o.selectorList = []metav1.ListOptions{ + {LabelSelector: fmt.Sprintf("%s=%s", constant.AppManagedByLabelKey, constant.AppName)}, // app.kubernetes.io/managed-by=kubeblocks + } + + return nil +} + +func (o *statusOptions) run() error { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + allErrs := make([]error, 0) o.buildSelectorList(ctx, &allErrs) o.showWorkloads(ctx, &allErrs) @@ -159,6 +182,7 @@ func (o *statusOptions) run() error { if o.showAll { o.showKubeBlocksResources(ctx, &allErrs) o.showKubeBlocksConfig(ctx, &allErrs) + o.showKubeBlocksRBAC(ctx, &allErrs) o.showKubeBlocksStorage(ctx, &allErrs) o.showHelmResources(ctx, &allErrs) } @@ -179,23 +203,56 @@ func (o *statusOptions) buildSelectorList(ctx context.Context, allErrs *[]error) addons = append(addons, addon) } } - // build addon instance selector o.addons = addons - - var selectors []metav1.ListOptions for _, selector := range buildResourceLabelSelectors(addons) { - selectors = append(selectors, metav1.ListOptions{LabelSelector: selector}) + o.selectorList = append(o.selectorList, metav1.ListOptions{LabelSelector: selector}) } - selectorList = selectors } func (o *statusOptions) showAddons() { fmt.Fprintln(o.Out, "\nKubeBlocks Addons:") tbl := printer.NewTablePrinter(o.Out) - tbl.SetHeader("NAME", "STATUS", "TYPE") + + tbl.Tbl.SetColumnConfigs([]tablePrinter.ColumnConfig{ + { + Name: "STATUS", + Transformer: func(val interface{}) string { + var ok bool + var addonPhase extensionsv1alpha1.AddonPhase + if addonPhase, ok = val.(extensionsv1alpha1.AddonPhase); !ok { + return fmt.Sprint(val) + } + var color text.Color + switch addonPhase { + case extensionsv1alpha1.AddonEnabled: + color = text.FgGreen + case extensionsv1alpha1.AddonFailed: + color = text.FgRed + case extensionsv1alpha1.AddonDisabled: + color = text.Faint + case extensionsv1alpha1.AddonEnabling, extensionsv1alpha1.AddonDisabling: + color = text.FgCyan + default: + return fmt.Sprint(addonPhase) + } + return color.Sprint(addonPhase) + }, + }, + }, + ) + + tbl.SetHeader("NAME", "STATUS", "TYPE", "PROVIDER") + + var provider string + var ok bool for _, addon := range o.addons { - tbl.AddRow(addon.Name, addon.Status.Phase, addon.Spec.Type) + if addon.Labels == nil { + provider = notAvailable + } else if provider, ok = addon.Labels[constant.AddonProviderLableKey]; !ok { + provider = notAvailable + } + tbl.AddRow(addon.Name, addon.Status.Phase, addon.Spec.Type, provider) } tbl.Print() } @@ -205,7 +262,7 @@ func (o *statusOptions) showKubeBlocksResources(ctx context.Context, allErrs *[] tblPrinter := printer.NewTablePrinter(o.Out) tblPrinter.SetHeader("KIND", "NAME") - unstructuredList := listResourceByGVR(ctx, o.dynamic, metav1.NamespaceAll, kubeBlocksGlobalCustomResources, selectorList, allErrs) + unstructuredList := listResourceByGVR(ctx, o.dynamic, metav1.NamespaceAll, kubeBlocksGlobalCustomResources, o.selectorList, allErrs) for _, resourceList := range unstructuredList { for _, resource := range resourceList.Items { tblPrinter.AddRow(resource.GetKind(), resource.GetName()) @@ -218,12 +275,38 @@ func (o *statusOptions) showKubeBlocksConfig(ctx context.Context, allErrs *[]err fmt.Fprintln(o.Out, "\nKubeBlocks Configurations:") tblPrinter := printer.NewTablePrinter(o.Out) tblPrinter.SetHeader("NAMESPACE", "KIND", "NAME") - unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksConfigurations, selectorList, allErrs) + unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksConfigurations, o.selectorList, allErrs) + for _, resourceList := range unstructuredList { + for _, resource := range resourceList.Items { + tblPrinter.AddRow(resource.GetNamespace(), resource.GetKind(), resource.GetName()) + } + } + tblPrinter.Print() +} + +func (o *statusOptions) showKubeBlocksRBAC(ctx context.Context, allErrs *[]error) { + fmt.Fprintln(o.Out, "\nKubeBlocks Global RBAC:") + tblPrinter := printer.NewTablePrinter(o.Out) + tblPrinter.SetHeader("KIND", "NAME") + unstructuredList := listResourceByGVR(ctx, o.dynamic, metav1.NamespaceAll, kubeBlocksClusterRBAC, o.selectorList, allErrs) + for _, resourceList := range unstructuredList { + for _, resource := range resourceList.Items { + tblPrinter.AddRow(resource.GetKind(), resource.GetName()) + } + } + + tblPrinter.Print() + + fmt.Fprintln(o.Out, "\nKubeBlocks Namespaced RBAC:") + tblPrinter = printer.NewTablePrinter(o.Out) + tblPrinter.SetHeader("NAMESPACE", "KIND", "NAME") + unstructuredList = listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksNamespacedRBAC, o.selectorList, allErrs) for _, resourceList := range unstructuredList { for _, resource := range resourceList.Items { tblPrinter.AddRow(resource.GetNamespace(), resource.GetKind(), resource.GetName()) } } + tblPrinter.Print() } @@ -242,7 +325,7 @@ func (o *statusOptions) showKubeBlocksStorage(ctx context.Context, allErrs *[]er tblPrinter.AddRow(pvc.GetNamespace(), pvc.Kind, pvc.GetName(), pvc.Status.Capacity.Storage()) } - unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksStorages, selectorList, allErrs) + unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksStorages, o.selectorList, allErrs) for _, resourceList := range unstructuredList { for _, resource := range resourceList.Items { switch resource.GetKind() { @@ -262,13 +345,17 @@ func (o *statusOptions) showHelmResources(ctx context.Context, allErrs *[]error) tblPrinter := printer.NewTablePrinter(o.Out) tblPrinter.SetHeader("NAMESPACE", "KIND", "NAME", "STATUS") - helmLabel := func(name string) string { - return fmt.Sprintf("%s=%s,%s=%s", "name", name, "owner", "helm") + helmLabel := func(name []string) string { + return fmt.Sprintf("%s in (%s),%s=%s", "name", strings.Join(name, ","), "owner", "helm") } - selectors := []metav1.ListOptions{{LabelSelector: types.KubeBlocksHelmLabel}} + // init helm release list with 'kubeblocks' + helmReleaseList := []string{types.KubeBlocksChartName} + // add one release with name = $kubeblocks-addons$ for _, addon := range o.addons { - selectors = append(selectors, metav1.ListOptions{LabelSelector: helmLabel(util.BuildAddonReleaseName(addon.Name))}) + helmReleaseList = append(helmReleaseList, util.BuildAddonReleaseName(addon.Name)) } + // label selector 'owner=helm,name in (kubeblocks,kb-addon-mongodb,kb-addon-redis...)' + selectors := []metav1.ListOptions{{LabelSelector: helmLabel(helmReleaseList)}} unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, helmConfigurations, selectors, allErrs) for _, resourceList := range unstructuredList { for _, resource := range resourceList.Items { @@ -282,96 +369,160 @@ func (o *statusOptions) showHelmResources(ctx context.Context, allErrs *[]error) func (o *statusOptions) showWorkloads(ctx context.Context, allErrs *[]error) { fmt.Fprintln(o.Out, "\nKubeBlocks Workloads:") tblPrinter := printer.NewTablePrinter(o.Out) - tblPrinter.SetHeader("NAMESPACE", "KIND", "NAME", "READY PODS", "CPU(cores)", "MEMORY(bytes)") + tblPrinter.Tbl.SetColumnConfigs([]tablePrinter.ColumnConfig{ + { + Name: "READY PODS", + Transformer: func(val interface{}) (valStr string) { + var ok bool + if valStr, ok = val.(string); !ok { + return fmt.Sprint(val) + } + if valStr == notAvailable || len(valStr) == 0 { + return valStr + } + // split string by '/' + podsInfo := strings.Split(valStr, "/") + if len(podsInfo) != 2 { + return valStr + } + readyPods, totalPods := int(0), int(0) + readyPods, _ = strconv.Atoi(podsInfo[0]) + totalPods, _ = strconv.Atoi(podsInfo[1]) + + var color text.Color + if readyPods != totalPods { + color = text.FgRed + } else { + color = text.FgGreen + } + return color.Sprint(valStr) + }, + }, + }, + ) - unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksWorkloads, selectorList, allErrs) + tblPrinter.SetHeader("NAMESPACE", "KIND", "NAME", "READY PODS", "CPU(cores)", "MEMORY(bytes)", "CREATED-AT") - cpuMap, memMap := computeMetricByWorkloads(ctx, o.ns, unstructuredList, o.mc, allErrs) + unstructuredList := listResourceByGVR(ctx, o.dynamic, o.ns, kubeBlocksWorkloads, o.selectorList, allErrs) - renderDeploy := func(raw *unstructured.Unstructured) { - deploy := &appsv1.Deployment{} - err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, deploy) - if err != nil { - appendErrIgnoreNotFound(allErrs, err) - return + cpuMap, memMap, readyMap := computeMetricByWorkloads(ctx, o.ns, unstructuredList, o.mc, allErrs) + + for _, workload := range unstructuredList { + for _, resource := range workload.Items { + createdAt := resource.GetCreationTimestamp() + name := resource.GetName() + row := []interface{}{resource.GetNamespace(), resource.GetKind(), name, readyMap[name], cpuMap[name], memMap[name], util.TimeFormat(&createdAt)} + tblPrinter.AddRow(row...) } - name := deploy.GetName() - tblPrinter.AddRow(deploy.GetNamespace(), deploy.Kind, deploy.GetName(), - fmt.Sprintf("%d/%d", deploy.Status.ReadyReplicas, deploy.Status.Replicas), - cpuMap[name], memMap[name]) } + tblPrinter.Print() +} - renderStatefulSet := func(raw *unstructured.Unstructured) { - sts := &appsv1.StatefulSet{} - err := runtime.DefaultUnstructuredConverter.FromUnstructured(raw.Object, sts) - if err != nil { - appendErrIgnoreNotFound(allErrs, err) - return - } - name := sts.GetName() - tblPrinter.AddRow(sts.GetNamespace(), sts.Kind, sts.GetName(), - fmt.Sprintf("%d/%d", sts.Status.ReadyReplicas, sts.Status.Replicas), - cpuMap[name], memMap[name]) +func getNestedSelectorAsString(obj map[string]interface{}, fields ...string) (string, error) { + val, found, err := unstructured.NestedStringMap(obj, fields...) + if !found || err != nil { + return "", fmt.Errorf("failed to get selector for %v, using field %s", obj, fields) + } + // convert it to string + var pair []string + for k, v := range val { + pair = append(pair, fmt.Sprintf("%s=%s", k, v)) } + return strings.Join(pair, ","), nil +} - for _, workload := range unstructuredList { - for _, resource := range workload.Items { - switch resource.GetKind() { - case constant.DeploymentKind: - renderDeploy(&resource) - case constant.StatefulSetKind: - renderStatefulSet(&resource) - default: - err := fmt.Errorf("unsupported worklkoad type: %s", resource.GetKind()) - appendErrIgnoreNotFound(allErrs, err) - } +func getNestedInt64(obj map[string]interface{}, fields ...string) int64 { + val, found, err := unstructured.NestedInt64(obj, fields...) + if !found || err != nil { + if klog.V(1).Enabled() { + klog.Errorf("failed to get int64 for %s, using field %s", obj, fields) } } - tblPrinter.Print() + return val } -func computeMetricByWorkloads(ctx context.Context, ns string, workloads []*unstructured.UnstructuredList, mc metrics.Interface, allErrs *[]error) (cpuMetricMap, memMetricMap map[string]string) { +func computeMetricByWorkloads(ctx context.Context, ns string, workloads []*unstructured.UnstructuredList, mc metrics.Interface, allErrs *[]error) (cpuMetricMap, memMetricMap, readyMap map[string]string) { cpuMetricMap = make(map[string]string) memMetricMap = make(map[string]string) + readyMap = make(map[string]string) - podsMetrics, err := mc.MetricsV1beta1().PodMetricses(ns).List(ctx, metav1.ListOptions{}) - if err != nil { - appendErrIgnoreNotFound(allErrs, err) - return - } - - computeResources := func(name string, podsMetrics *v1beta1.PodMetricsList) { - cpuUsage, memUsage := int64(0), int64(0) - for _, pod := range podsMetrics.Items { - if strings.HasPrefix(pod.Name, name) { + computeMetrics := func(namespace, name string, matchLabels string) { + if pods, err := mc.MetricsV1beta1().PodMetricses(namespace).List(ctx, metav1.ListOptions{LabelSelector: matchLabels}); err != nil { + if klog.V(1).Enabled() { + klog.Errorf("faied to get pod metrics for %s/%s, selector: , error: %v", namespace, name, matchLabels, err) + } + } else { + cpuUsage, memUsage := int64(0), int64(0) + for _, pod := range pods.Items { for _, container := range pod.Containers { cpuUsage += container.Usage.Cpu().MilliValue() - memUsage += container.Usage.Memory().Value() / (1024 * 1024) + memUsage += container.Usage.Memory().Value() / 1024 / 1024 } } + cpuMetricMap[name] = fmt.Sprintf("%dm", cpuUsage) + memMetricMap[name] = fmt.Sprintf("%dMi", memUsage) } - cpuMetricMap[name] = fmt.Sprintf("%dm", cpuUsage) - memMetricMap[name] = fmt.Sprintf("%dMi", memUsage) } + computeWorkloadRunningMeta := func(resource *unstructured.Unstructured, getReadyRepilca func() []string, getTotalReplicas func() []string, getSelector func() []string) error { + name := resource.GetName() + + readyMap[name] = notAvailable + cpuMetricMap[name] = notAvailable + memMetricMap[name] = notAvailable + + if getReadyRepilca != nil && getTotalReplicas != nil { + readyReplicas := getNestedInt64(resource.Object, getReadyRepilca()...) + replicas := getNestedInt64(resource.Object, getTotalReplicas()...) + readyMap[name] = fmt.Sprintf("%d/%d", readyReplicas, replicas) + } + + if getSelector != nil { + if matchLabels, err := getNestedSelectorAsString(resource.Object, getSelector()...); err != nil { + return err + } else { + computeMetrics(resource.GetNamespace(), name, matchLabels) + } + } + return nil + } + + readyReplicas := func() []string { return []string{"status", "readyReplicas"} } + replicas := func() []string { return []string{"status", "replicas"} } + matchLabels := func() []string { return []string{"spec", "selector", "matchLabels"} } + daemonReady := func() []string { return []string{"status", "numberReady"} } + daemonTotal := func() []string { return []string{"status", "desiredNumberScheduled"} } + jobReady := func() []string { return []string{"status", "succeeded"} } + jobTotal := func() []string { return []string{"spec", "completions"} } + for _, workload := range workloads { for _, resource := range workload.Items { - name := resource.GetName() - if podsMetrics == nil { - cpuMetricMap[name] = "N/A" - memMetricMap[name] = "N/A" - continue + var err error + switch resource.GetKind() { + case constant.DeploymentKind, constant.StatefulSetKind: + err = computeWorkloadRunningMeta(&resource, readyReplicas, replicas, matchLabels) + case constant.DaemonSetKind: + err = computeWorkloadRunningMeta(&resource, daemonReady, daemonTotal, matchLabels) + case constant.JobKind: + err = computeWorkloadRunningMeta(&resource, jobReady, jobTotal, matchLabels) + case constant.CronJobKind: + err = computeWorkloadRunningMeta(&resource, nil, nil, nil) + default: + err = fmt.Errorf("unsupported workload kind: %s, name: %s", resource.GetKind(), resource.GetName()) + } + if err != nil { + appendErrIgnoreNotFound(allErrs, err) } - computeResources(name, podsMetrics) } } - return cpuMetricMap, memMetricMap + return cpuMetricMap, memMetricMap, readyMap } func listResourceByGVR(ctx context.Context, client dynamic.Interface, namespace string, gvrlist []schema.GroupVersionResource, selector []metav1.ListOptions, allErrs *[]error) []*unstructured.UnstructuredList { unstructuredList := make([]*unstructured.UnstructuredList, 0) for _, gvr := range gvrlist { for _, labelSelector := range selector { + klog.V(1).Infof("listResourceByGVR: namespace=%s, gvrlist=%v, selector=%v", namespace, gvr, labelSelector) resource, err := client.Resource(gvr).Namespace(namespace).List(ctx, labelSelector) if err != nil { appendErrIgnoreNotFound(allErrs, err) diff --git a/internal/cli/cmd/kubeblocks/status_test.go b/internal/cli/cmd/kubeblocks/status_test.go index 5df1db11f..29b767981 100644 --- a/internal/cli/cmd/kubeblocks/status_test.go +++ b/internal/cli/cmd/kubeblocks/status_test.go @@ -1,47 +1,104 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks import ( "context" + "net/http" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/spf13/cobra" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/constant" ) var _ = Describe("kubeblocks status", func() { - var cmd *cobra.Command - var streams genericclioptions.IOStreams - var tf *cmdtesting.TestFactory + var ( + namespace = "test" + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + stsName = "test-sts" + deployName = "test-deploy" + ) BeforeEach(func() { - streams, _, _, _ = genericclioptions.NewTestIOStreams() - tf = cmdtesting.NewTestFactory().WithNamespace(namespace) - tf.Client = &clientfake.RESTClient{} + tf = cmdtesting.NewTestFactory().WithNamespace("test") + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + // add workloads + extraLabels := map[string]string{ + "appName": "JohnSnow", + "slogan": "YouknowNothing", + } + + deploy := testing.FakeDeploy(deployName, namespace, extraLabels) + deploymentList := &appsv1.DeploymentList{} + deploymentList.Items = []appsv1.Deployment{*deploy} + + sts := testing.FakeStatefulSet(stsName, namespace, extraLabels) + statefulSetList := &appsv1.StatefulSetList{} + statefulSetList.Items = []appsv1.StatefulSet{*sts} + stsPods := testing.FakePodForSts(sts) + + job := testing.FakeJob("test-job", namespace, extraLabels) + jobList := &batchv1.JobList{} + jobList.Items = []batchv1.Job{*job} + + cronjob := testing.FakeCronJob("test-cronjob", namespace, extraLabels) + cronjobList := &batchv1.CronJobList{} + cronjobList.Items = []batchv1.CronJob{*cronjob} + + httpResp := func(obj runtime.Object) *http.Response { + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, obj)} + } + + tf.UnstructuredClient = &clientfake.RESTClient{ + GroupVersion: schema.GroupVersion{Group: types.AppsAPIGroup, Version: types.AppsAPIVersion}, + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + urlPrefix := "/api/v1/namespaces/" + namespace + return map[string]*http.Response{ + urlPrefix + "/deployments": httpResp(deploymentList), + urlPrefix + "/statefulsets": httpResp(statefulSetList), + urlPrefix + "/jobs": httpResp(jobList), + urlPrefix + "/cronjobs": httpResp(cronjobList), + urlPrefix + "/pods": httpResp(stsPods), + }[req.URL.Path], nil + }), + } + + tf.Client = tf.UnstructuredClient + tf.FakeDynamicClient = testing.FakeDynamicClient(deploy, sts) + streams = genericclioptions.NewTestIOStreamsDiscard() }) AfterEach(func() { @@ -50,7 +107,7 @@ var _ = Describe("kubeblocks status", func() { It("pre-run status", func() { var cfg string - cmd = newStatusCmd(tf, streams) + cmd := newStatusCmd(tf, streams) Expect(cmd).ShouldNot(BeNil()) Expect(cmd.HasSubCommands()).Should(BeFalse()) @@ -68,37 +125,39 @@ var _ = Describe("kubeblocks status", func() { Expect(o.showAll).To(Equal(false)) }) - It("run status", func() { - ns := "demo" - - mockDeploy := func() *appsv1.Deployment { - deploy := &appsv1.Deployment{} - deploy.SetNamespace(ns) - deploy.SetLabels(map[string]string{ - "app.kubernetes.io/name": types.KubeBlocksChartName, - "app.kubernetes.io/version": "latest", - }) - return deploy - } - + It("list resources", func() { + clientSet, _ := tf.KubernetesClientSet() o := &statusOptions{ IOStreams: streams, - ns: ns, - client: testing.FakeClientSet(mockDeploy()), + ns: namespace, + client: clientSet, mc: testing.FakeMetricsClientSet(), - dynamic: testing.FakeDynamicClient(mockDeploy()), + dynamic: tf.FakeDynamicClient, showAll: true, } By("make sure mocked deploy is injected") ctx := context.Background() - deploys, err := o.dynamic.Resource(types.DeployGVR()).Namespace(ns).List(ctx, metav1.ListOptions{}) + deploys, err := o.dynamic.Resource(types.DeployGVR()).Namespace(namespace).List(ctx, metav1.ListOptions{}) Expect(err).Should(Succeed()) Expect(len(deploys.Items)).Should(BeEquivalentTo(1)) + statefulsets, err := o.dynamic.Resource(types.StatefulSetGVR()).Namespace(namespace).List(ctx, metav1.ListOptions{}) + Expect(err).Should(Succeed()) + Expect(len(statefulsets.Items)).Should(BeEquivalentTo(1)) + By("check deployment can be hit by selector") allErrs := make([]error, 0) - unstructuredList := listResourceByGVR(ctx, o.dynamic, ns, kubeBlocksWorkloads, selectorList, &allErrs) - Expect(len(unstructuredList)).Should(BeEquivalentTo(len(kubeBlocksWorkloads) * len(selectorList))) + o.buildSelectorList(ctx, &allErrs) + unstructuredList := listResourceByGVR(ctx, o.dynamic, namespace, kubeBlocksWorkloads, o.selectorList, &allErrs) + // will list update to five types of worklaods + Expect(len(unstructuredList)).Should(BeEquivalentTo(5)) + for _, list := range unstructuredList { + if list.GetKind() == constant.DeploymentKind || list.GetKind() == constant.StatefulSetKind || list.GetKind() == constant.JobKind || list.GetKind() == constant.CronJobKind { + Expect(len(list.Items)).Should(BeEquivalentTo(1)) + } else { + Expect(len(list.Items)).Should(BeEquivalentTo(0)) + } + } Expect(o.run()).To(Succeed()) }) }) diff --git a/internal/cli/cmd/kubeblocks/suite_test.go b/internal/cli/cmd/kubeblocks/suite_test.go index d53d1305b..c352a773a 100644 --- a/internal/cli/cmd/kubeblocks/suite_test.go +++ b/internal/cli/cmd/kubeblocks/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks diff --git a/internal/cli/cmd/kubeblocks/uninstall.go b/internal/cli/cmd/kubeblocks/uninstall.go index e702f3572..cb8d66c98 100644 --- a/internal/cli/cmd/kubeblocks/uninstall.go +++ b/internal/cli/cmd/kubeblocks/uninstall.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -26,7 +29,7 @@ import ( "github.com/pkg/errors" "github.com/spf13/cobra" - "github.com/spf13/viper" + "golang.org/x/exp/maps" "helm.sh/helm/v3/pkg/repo" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,18 +37,19 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" k8sapitypes "k8s.io/apimachinery/pkg/types" utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apimachinery/pkg/util/wait" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" "k8s.io/klog/v2" cmdutil "k8s.io/kubectl/pkg/cmd/util" "k8s.io/kubectl/pkg/util/templates" - "k8s.io/utils/strings/slices" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" - "github.com/apecloud/kubeblocks/internal/constant" ) var ( @@ -90,55 +94,25 @@ func newUninstallCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *co cmd.Flags().BoolVar(&o.removePVs, "remove-pvs", false, "Remove PersistentVolume or not") cmd.Flags().BoolVar(&o.removePVCs, "remove-pvcs", false, "Remove PersistentVolumeClaim or not") cmd.Flags().BoolVar(&o.RemoveNamespace, "remove-namespace", false, "Remove default created \"kb-system\" namespace or not") + cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for uninstalling KubeBlocks, such as --timeout=5m") + cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be uninstalled, including all the add-ons. It will wait for a --timeout period") return cmd } func (o *UninstallOptions) PreCheck() error { // wait user to confirm if !o.AutoApprove { - printer.Warning(o.Out, "uninstall will remove all KubeBlocks resources.\n") + printer.Warning(o.Out, "this action will remove all KubeBlocks resources.\n") if err := confirmUninstall(o.In); err != nil { return err } } - preCheckList := []string{ - "clusters.apps.kubeblocks.io", - } - ctx := context.Background() - - // delete crds - crs := map[string][]string{} - crdList, err := o.Dynamic.Resource(types.CRDGVR()).List(ctx, metav1.ListOptions{}) - if err != nil { + // check if there is any resource should be removed first, if so, return error + // and ask user to remove them manually + if err := checkResources(o.Dynamic); err != nil { return err } - for _, crd := range crdList.Items { - // find kubeblocks crds - if strings.Contains(crd.GetName(), constant.APIGroup) && - slices.Contains(preCheckList, crd.GetName()) { - gvr, err := getGVRByCRD(&crd) - if err != nil { - return err - } - // find custom resource - objList, err := o.Dynamic.Resource(*gvr).List(ctx, metav1.ListOptions{}) - if err != nil { - return err - } - for _, item := range objList.Items { - crs[crd.GetName()] = append(crs[crd.GetName()], item.GetName()) - } - } - } - - if len(crs) > 0 { - errMsg := bytes.NewBufferString("failed to uninstall, the following custom resources need to be removed first:\n") - for k, v := range crs { - errMsg.WriteString(fmt.Sprintf(" %s: %s\n", k, strings.Join(v, " "))) - } - return errors.Errorf(errMsg.String()) - } // verify where kubeblocks is installed kbNamespace, err := util.GetKubeBlocksNamespace(o.Client) @@ -148,29 +122,35 @@ func (o *UninstallOptions) PreCheck() error { fmt.Fprintf(o.Out, "to find out the namespace where KubeBlocks is installed, please use:\n\t'kbcli kubeblocks status'\n") fmt.Fprintf(o.Out, "to uninstall KubeBlocks completely, please use:\n\t`kbcli kubeblocks uninstall -n `\n") } - } else if o.Namespace != kbNamespace { - o.Namespace = kbNamespace + } + + o.Namespace = kbNamespace + if kbNamespace != "" { fmt.Fprintf(o.Out, "Uninstall KubeBlocks in namespace \"%s\"\n", kbNamespace) } + return nil } func (o *UninstallOptions) Uninstall() error { - printSpinner := func(spinner func(result bool), err error) { + printSpinner := func(s spinner.Interface, err error) { if err == nil || apierrors.IsNotFound(err) || strings.Contains(err.Error(), "release: not found") { - spinner(true) + s.Success() return } - spinner(false) + s.Fail() fmt.Fprintf(o.Out, " %s\n", err.Error()) } - newSpinner := func(msg string) func(result bool) { - return printer.Spinner(o.Out, fmt.Sprintf("%-50s", msg)) + newSpinner := func(msg string) spinner.Interface { + return spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", msg))) } // uninstall all KubeBlocks addons - printSpinner(newSpinner("Uninstall KubeBlocks addons"), o.uninstallAddons()) + if err := o.uninstallAddons(); err != nil { + fmt.Fprintf(o.Out, "Failed to uninstall addons, run \"kbcli kubeblocks uninstall\" to retry.\n") + return err + } // uninstall helm release that will delete custom resources, but since finalizers is not empty, // custom resources will not be deleted, so we will remove finalizers later. @@ -178,8 +158,12 @@ func (o *UninstallOptions) Uninstall() error { chart := helm.InstallOpts{ Name: types.KubeBlocksChartName, Namespace: o.Namespace, + + // KubeBlocks chart has a hook to delete addons, but we have already deleted addons, + // and that webhook may fail, so we need to disable hooks. + DisableHooks: true, } - printSpinner(newSpinner("Uninstall helm release "+types.KubeBlocksChartName+" "+v[util.KubeBlocksApp]), + printSpinner(newSpinner("Uninstall helm release "+types.KubeBlocksReleaseName+" "+v.KubeBlocks), chart.Uninstall(o.HelmCfg)) // remove repo @@ -224,95 +208,160 @@ func (o *UninstallOptions) Uninstall() error { deleteNamespace(o.Client, types.DefaultNamespace)) } - fmt.Fprintln(o.Out, "Uninstall KubeBlocks done.") + if o.Wait { + fmt.Fprintln(o.Out, "Uninstall KubeBlocks done.") + } else { + fmt.Fprintf(o.Out, "KubeBlocks is uninstalling, run \"kbcli kubeblocks status -A\" to check kubeblocks resources.\n") + } return nil } -// uninstallAddons uninstall all KubeBlocks addons +// uninstallAddons uninstalls all KubeBlocks addons func (o *UninstallOptions) uninstallAddons() error { var ( allErrs []error - stop bool err error + header = "Wait for addons to be disabled" + s spinner.Interface + msg string ) - uninstallAddon := func(addon *extensionsv1alpha1.Addon) error { - klog.V(1).Infof("Uninstall %s", addon.Name) - if _, err := o.Dynamic.Resource(types.AddonGVR()).Patch(context.TODO(), addon.Name, k8sapitypes.JSONPatchType, - []byte("[{\"op\": \"replace\", \"path\": \"/spec/install/enabled\", \"value\": false }]"), - metav1.PatchOptions{}); err != nil && !apierrors.IsNotFound(err) { - return err - } - return nil - } - processAddons := func(processFn func(addon *extensionsv1alpha1.Addon) error) ([]*extensionsv1alpha1.Addon, error) { - var addons []*extensionsv1alpha1.Addon + addons := make(map[string]*extensionsv1alpha1.Addon) + processAddons := func(uninstall bool) error { objects, err := o.Dynamic.Resource(types.AddonGVR()).List(context.TODO(), metav1.ListOptions{ - LabelSelector: buildAddonLabelSelector(), + LabelSelector: buildKubeBlocksSelectorLabels(), }) if err != nil && !apierrors.IsNotFound(err) { klog.V(1).Infof("Failed to get KubeBlocks addons %s", err.Error()) allErrs = append(allErrs, err) - return nil, utilerrors.NewAggregate(allErrs) + return utilerrors.NewAggregate(allErrs) } if objects == nil { - return nil, nil + return nil } - // if all addons are disabled, then we will stop uninstalling addons - stop = true for _, obj := range objects.Items { - addon := extensionsv1alpha1.Addon{} - if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, &addon); err != nil { + addon := &extensionsv1alpha1.Addon{} + if err = runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, addon); err != nil { klog.V(1).Infof("Failed to convert KubeBlocks addon %s", err.Error()) allErrs = append(allErrs, err) continue } - klog.V(1).Infof("Addon: %s, enabled: %v, status: %s", - addon.Name, addon.Spec.InstallSpec.GetEnabled(), addon.Status.Phase) - addons = append(addons, &addon) - if addon.Status.Phase == extensionsv1alpha1.AddonDisabled { - continue - } - // if there is an enabled addon, then we will continue uninstalling addons - // and wait for a while to make sure all addons are disabled - stop = false - if processFn == nil { - continue - } - if err = processFn(&addon); err != nil && !apierrors.IsNotFound(err) { - klog.V(1).Infof("Failed to uninstall KubeBlocks addon %s", err.Error()) - allErrs = append(allErrs, err) + + if uninstall { + // we only need to uninstall addons that are not disabled + if addon.Spec.InstallSpec.IsDisabled() { + continue + } + addons[addon.Name] = addon + o.addons = append(o.addons, addon) + + // uninstall addons + if err = disableAddon(o.Dynamic, addon); err != nil { + klog.V(1).Infof("Failed to uninstall KubeBlocks addon %s %s", addon.Name, err.Error()) + allErrs = append(allErrs, err) + } + } else { + // update cached addon if exists + if _, ok := addons[addon.Name]; ok { + addons[addon.Name] = addon + } } } - return addons, utilerrors.NewAggregate(allErrs) + return utilerrors.NewAggregate(allErrs) + } + + suffixMsg := func(msg string) string { + return fmt.Sprintf("%-50s", msg) + } + + if !o.Wait { + s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", "Uninstall KubeBlocks addons"))) + } else { + s = spinner.New(o.Out, spinner.WithMessage(fmt.Sprintf("%-50s", header))) } // get all addons and uninstall them - if o.addons, err = processAddons(uninstallAddon); err != nil { + if err = processAddons(true); err != nil { + s.Fail() return err } - if len(o.addons) == 0 || stop { + if len(addons) == 0 || !o.Wait { + s.Success() return nil } + spinnerDone := func() { + s.SetFinalMsg(msg) + s.Done("") + fmt.Fprintln(o.Out) + } + // check if all addons are disabled, if so, then we will stop checking addons // status otherwise, we will wait for a while and check again - for i := 0; i < viper.GetInt("KB_WAIT_ADDON_TIMES"); i++ { - klog.V(1).Infof("Wait for %d seconds and check addons disabled again", 5) - time.Sleep(5 * time.Second) - // pass a nil processFn, we will only check addons status, do not try to - // uninstall addons again - if o.addons, err = processAddons(nil); err != nil { + if err = wait.PollImmediate(5*time.Second, o.Timeout, func() (bool, error) { + // we only check addons status, do not try to uninstall addons again + if err = processAddons(false); err != nil { + return false, err + } + status := checkAddons(maps.Values(addons), false) + msg = suffixMsg(fmt.Sprintf("%s\n %s", header, status.outputMsg)) + s.SetMessage(msg) + if status.allDisabled { + spinnerDone() + return true, nil + } else if status.hasFailed { + return false, errors.New("some addons are failed to disabled") + } + return false, nil + }); err != nil { + spinnerDone() + printAddonMsg(o.Out, maps.Values(addons), false) + allErrs = append(allErrs, err) + } + return utilerrors.NewAggregate(allErrs) +} + +func checkResources(dynamic dynamic.Interface) error { + ctx := context.Background() + gvrList := []schema.GroupVersionResource{ + types.ClusterGVR(), + types.BackupGVR(), + } + + crs := map[string][]string{} + for _, gvr := range gvrList { + objList, err := dynamic.Resource(gvr).List(ctx, metav1.ListOptions{}) + if err != nil && !apierrors.IsNotFound(err) { return err } - if stop { - return nil + + if objList == nil { + continue + } + + for _, item := range objList.Items { + crs[gvr.Resource] = append(crs[gvr.Resource], item.GetName()) } } - if !stop { - allErrs = append(allErrs, fmt.Errorf("failed to uninstall KubeBlocks addons")) + + if len(crs) > 0 { + errMsg := bytes.NewBufferString("failed to uninstall, the following resources need to be removed first\n") + for k, v := range crs { + errMsg.WriteString(fmt.Sprintf(" %s: %s\n", k, strings.Join(v, " "))) + } + return errors.Errorf(errMsg.String()) } - return utilerrors.NewAggregate(allErrs) + return nil +} + +func disableAddon(dynamic dynamic.Interface, addon *extensionsv1alpha1.Addon) error { + klog.V(1).Infof("Uninstall %s, status %s", addon.Name, addon.Status.Phase) + if _, err := dynamic.Resource(types.AddonGVR()).Patch(context.TODO(), addon.Name, k8sapitypes.JSONPatchType, + []byte("[{\"op\": \"replace\", \"path\": \"/spec/install/enabled\", \"value\": false }]"), + metav1.PatchOptions{}); err != nil && !apierrors.IsNotFound(err) { + return err + } + return nil } diff --git a/internal/cli/cmd/kubeblocks/uninstall_test.go b/internal/cli/cmd/kubeblocks/uninstall_test.go index 947f652c2..c8afd7213 100644 --- a/internal/cli/cmd/kubeblocks/uninstall_test.go +++ b/internal/cli/cmd/kubeblocks/uninstall_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -74,4 +77,9 @@ var _ = Describe("kubeblocks uninstall", func() { } Expect(o.Uninstall()).Should(Succeed()) }) + + It("checkResources", func() { + fakeDynamic := testing.FakeDynamicClient() + Expect(checkResources(fakeDynamic)).Should(Succeed()) + }) }) diff --git a/internal/cli/cmd/kubeblocks/upgrade.go b/internal/cli/cmd/kubeblocks/upgrade.go index 0bdfc037d..48ad331ee 100644 --- a/internal/cli/cmd/kubeblocks/upgrade.go +++ b/internal/cli/cmd/kubeblocks/upgrade.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -28,6 +31,7 @@ import ( "k8s.io/kubectl/pkg/util/templates" "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" @@ -62,7 +66,8 @@ func newUpgradeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobr cmd.Flags().StringVar(&o.Version, "version", "", "Set KubeBlocks version") cmd.Flags().BoolVar(&o.Check, "check", true, "Check kubernetes environment before upgrade") - cmd.Flags().DurationVar(&o.timeout, "timeout", 1800*time.Second, "Time to wait for upgrading KubeBlocks") + cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for upgrading KubeBlocks, such as --timeout=10m") + cmd.Flags().BoolVar(&o.Wait, "wait", true, "Wait for KubeBlocks to be ready. It will wait for a --timeout period") helm.AddValueOptionsFlags(cmd.Flags(), &o.ValueOpts) return cmd @@ -87,53 +92,48 @@ func (o *InstallOptions) Upgrade() error { } // check if KubeBlocks has been installed - versionInfo, err := util.GetVersionInfo(o.Client) + v, err := util.GetVersionInfo(o.Client) if err != nil { return err } - v := versionInfo[util.KubeBlocksApp] - if len(v) == 0 { + kbVersion := v.KubeBlocks + if kbVersion == "" { return errors.New("KubeBlocks does not exist, try to run \"kbcli kubeblocks install\" to install") } - if v == o.Version && helm.ValueOptsIsEmpty(&o.ValueOpts) { - fmt.Fprintf(o.Out, "Current version %s is the same as the upgraded version, no need to upgrade.\n", o.Version) + if kbVersion == o.Version && helm.ValueOptsIsEmpty(&o.ValueOpts) { + fmt.Fprintf(o.Out, "Current version %s is same with the upgraded version, no need to upgrade.\n", o.Version) return nil } fmt.Fprintf(o.Out, "Current KubeBlocks version %s.\n", v) - if err = o.preCheck(versionInfo); err != nil { + if err = o.checkVersion(v); err != nil { return err } // add helm repo - spinner := printer.Spinner(o.Out, "%-40s", "Add and update repo "+types.KubeBlocksChartName) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Add and update repo "+types.KubeBlocksChartName)) + defer s.Fail() // Add repo, if exists, will update it if err = helm.AddRepo(&repo.Entry{Name: types.KubeBlocksChartName, URL: util.GetHelmChartRepoURL()}); err != nil { return err } - spinner(true) + s.Success() // it's time to upgrade msg := "" if o.Version != "" { msg = "to " + o.Version } - spinner = printer.Spinner(o.Out, "%-40s", "Upgrading KubeBlocks "+msg) - defer spinner(false) + s = spinner.New(o.Out, spinnerMsg("Upgrading KubeBlocks "+msg)) + defer s.Fail() // upgrade KubeBlocks chart if err = o.upgradeChart(); err != nil { return err } // successfully upgraded - spinner(true) - - // create VolumeSnapshotClass - if err = o.createVolumeSnapshotClass(); err != nil { - return err - } + s.Success() if !o.Quiet { fmt.Fprintf(o.Out, "\nKubeBlocks has been upgraded %s SUCCESSFULLY!\n", msg) diff --git a/internal/cli/cmd/kubeblocks/upgrade_test.go b/internal/cli/cmd/kubeblocks/upgrade_test.go index 65fb4ec2c..97a677d53 100644 --- a/internal/cli/cmd/kubeblocks/upgrade_test.go +++ b/internal/cli/cmd/kubeblocks/upgrade_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks diff --git a/internal/cli/cmd/kubeblocks/util.go b/internal/cli/cmd/kubeblocks/util.go index ea5dae03b..c594ac3b4 100644 --- a/internal/cli/cmd/kubeblocks/util.go +++ b/internal/cli/cmd/kubeblocks/util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -20,9 +23,11 @@ import ( "context" "fmt" "io" + "sort" "strings" "github.com/Masterminds/semver/v3" + "github.com/jedib0t/go-pretty/v6/table" "github.com/pkg/errors" "golang.org/x/exp/slices" "helm.sh/helm/v3/pkg/repo" @@ -32,6 +37,7 @@ import ( "k8s.io/client-go/kubernetes" extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" @@ -138,9 +144,128 @@ func buildResourceLabelSelectors(addons []*extensionsv1alpha1.Addon) []string { return selectors } -// buildAddonLabelSelector builds labelSelector that can be used to get all build-in addons -func buildAddonLabelSelector() string { +// buildAddonLabelSelector builds labelSelector that can be used to get all kubeBlocks resources, +// including CRDs, addons (but not resources created by addons). +// and it should be consistent with the labelSelectors defined in chart. +// for example: +// {{- define "kubeblocks.selectorLabels" -}} +// app.kubernetes.io/name: {{ include "kubeblocks.name" . }} +// app.kubernetes.io/instance: {{ .Release.Name }} +// {{- end }} +func buildKubeBlocksSelectorLabels() string { return fmt.Sprintf("%s=%s,%s=%s", constant.AppInstanceLabelKey, types.KubeBlocksReleaseName, constant.AppNameLabelKey, types.KubeBlocksChartName) } + +// buildConfig builds labelSelector that can be used to get all configmaps that are used to store config templates. +// and it should be consistent with the labelSelectors defined +// in `configuration.updateConfigMapFinalizerImpl`. +func buildConfigTypeSelectorLabels() string { + return fmt.Sprintf("%s=%s", constant.CMConfigurationTypeLabelKey, constant.ConfigTemplateType) +} + +// printAddonMsg prints addon message when has failed addon or timeouts +func printAddonMsg(out io.Writer, addons []*extensionsv1alpha1.Addon, install bool) { + var ( + enablingAddons []string + disablingAddons []string + failedAddons []*extensionsv1alpha1.Addon + ) + + for _, addon := range addons { + switch addon.Status.Phase { + case extensionsv1alpha1.AddonEnabling: + enablingAddons = append(enablingAddons, addon.Name) + case extensionsv1alpha1.AddonDisabling: + disablingAddons = append(disablingAddons, addon.Name) + case extensionsv1alpha1.AddonFailed: + for _, c := range addon.Status.Conditions { + if c.Status == metav1.ConditionFalse { + failedAddons = append(failedAddons, addon) + break + } + } + } + } + + // print failed addon messages + if len(failedAddons) > 0 { + printFailedAddonMsg(out, failedAddons) + } + + // print enabling addon messages + if install && len(enablingAddons) > 0 { + fmt.Fprintf(out, "\nEnabling addons: %s\n", strings.Join(enablingAddons, ", ")) + fmt.Fprintf(out, "Please wait for a while and try to run \"kbcli addon list\" to check addons status.\n") + } + + if !install && len(disablingAddons) > 0 { + fmt.Fprintf(out, "\nDisabling addons: %s\n", strings.Join(disablingAddons, ", ")) + fmt.Fprintf(out, "Please wait for a while and try to run \"kbcli addon list\" to check addons status.\n") + } +} + +func printFailedAddonMsg(out io.Writer, addons []*extensionsv1alpha1.Addon) { + fmt.Fprintf(out, "\nFailed addons:\n") + tbl := printer.NewTablePrinter(out) + tbl.Tbl.SetColumnConfigs([]table.ColumnConfig{ + {Number: 4, WidthMax: 120}, + }) + tbl.SetHeader("NAME", "TIME", "REASON", "MESSAGE") + for _, addon := range addons { + var times, reasons, messages []string + for _, c := range addon.Status.Conditions { + if c.Status != metav1.ConditionFalse { + continue + } + times = append(times, util.TimeFormat(&c.LastTransitionTime)) + reasons = append(reasons, c.Reason) + messages = append(messages, c.Message) + } + tbl.AddRow(addon.Name, strings.Join(times, "\n"), strings.Join(reasons, "\n"), strings.Join(messages, "\n")) + } + tbl.Print() +} + +func checkAddons(addons []*extensionsv1alpha1.Addon, install bool) *addonStatus { + status := &addonStatus{ + allEnabled: true, + allDisabled: true, + hasFailed: false, + outputMsg: "", + } + + if len(addons) == 0 { + return status + } + + all := make([]string, 0) + for _, addon := range addons { + s := string(addon.Status.Phase) + switch addon.Status.Phase { + case extensionsv1alpha1.AddonEnabled: + if install { + s = printer.BoldGreen("OK") + } + status.allDisabled = false + case extensionsv1alpha1.AddonDisabled: + if !install { + s = printer.BoldGreen("OK") + } + status.allEnabled = false + case extensionsv1alpha1.AddonFailed: + status.hasFailed = true + status.allEnabled = false + status.allDisabled = false + case extensionsv1alpha1.AddonDisabling: + status.allDisabled = false + case extensionsv1alpha1.AddonEnabling: + status.allEnabled = false + } + all = append(all, fmt.Sprintf("%-48s %s", addon.Name, s)) + } + sort.Strings(all) + status.outputMsg = strings.Join(all, "\n ") + return status +} diff --git a/internal/cli/cmd/kubeblocks/util_test.go b/internal/cli/cmd/kubeblocks/util_test.go index 66bf3daea..4a8d74792 100644 --- a/internal/cli/cmd/kubeblocks/util_test.go +++ b/internal/cli/cmd/kubeblocks/util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package kubeblocks @@ -21,8 +24,11 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" ) @@ -72,4 +78,68 @@ var _ = Describe("kubeblocks", func() { _, _ = in.Write([]byte("uninstall-kubeblocks\n")) Expect(confirmUninstall(in)).Should(Succeed()) }) + + It("printAddonMsg", func() { + const ( + reason = "test-failed-reason" + ) + + fakeAddOn := func(name string, conditionTrue bool, msg string) *extensionsv1alpha1.Addon { + addon := &extensionsv1alpha1.Addon{} + addon.Name = name + addon.Status = extensionsv1alpha1.AddonStatus{} + if conditionTrue { + addon.Status.Phase = extensionsv1alpha1.AddonEnabled + } else { + addon.Status.Phase = extensionsv1alpha1.AddonFailed + addon.Status.Conditions = []metav1.Condition{ + { + Message: msg, + Reason: reason, + Status: metav1.ConditionFalse, + }, + { + Message: msg, + Reason: reason, + Status: metav1.ConditionFalse, + }, + } + } + return addon + } + + testCases := []struct { + desc string + addons []*extensionsv1alpha1.Addon + expected string + }{ + { + desc: "addons is nil", + addons: nil, + expected: "", + }, + { + desc: "addons without false condition", + addons: []*extensionsv1alpha1.Addon{ + fakeAddOn("addon", true, ""), + }, + expected: "", + }, + { + desc: "addons with false condition", + addons: []*extensionsv1alpha1.Addon{ + fakeAddOn("addon1", true, ""), + fakeAddOn("addon2", false, "failed to enable addon2"), + }, + expected: "failed to enable addon2", + }, + } + + for _, c := range testCases { + By(c.desc) + out := &bytes.Buffer{} + printAddonMsg(out, c.addons, true) + Expect(out.String()).To(ContainSubstring(c.expected)) + } + }) }) diff --git a/internal/cli/cmd/migration/base.go b/internal/cli/cmd/migration/base.go new file mode 100644 index 000000000..a30c4a584 --- /dev/null +++ b/internal/cli/cmd/migration/base.go @@ -0,0 +1,261 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "context" + "fmt" + "os" + "strings" + + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/types" + migrationv1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +const ( + MigrationTaskLabel = "datamigration.apecloud.io/migrationtask" + MigrationTaskStepAnnotation = "datamigration.apecloud.io/step" + SerialJobOrderAnnotation = "common.apecloud.io/serial_job_order" +) + +const ( + invalidMigrationCrdAdvice = "to use migration-related functions, please ensure that the addon of migration is enabled, use: 'kbcli addon enable migration' to enable the addon" +) + +// Endpoint +// Todo: For the source or target is cluster in KubeBlocks. A better way is to get secret from {$clustername}-conn-credential, so the username, password, addresses can be omitted + +type EndpointModel struct { + UserName string `json:"userName"` + Password string `json:"password"` + Address string `json:"address"` + // +optional + Database string `json:"databaseName,omitempty"` +} + +func (e *EndpointModel) BuildFromStr(msgArr *[]string, endpointStr string) error { + if endpointStr == "" { + BuildErrorMsg(msgArr, "endpoint string cannot be empty") + return nil + } + e.clear() + endpointStr = strings.TrimSpace(endpointStr) + accountURLPair := strings.Split(endpointStr, "@") + if len(accountURLPair) != 2 { + BuildErrorMsg(msgArr, "endpoint may not contain account info") + return nil + } + accountPair := strings.Split(accountURLPair[0], ":") + if len(accountPair) != 2 { + BuildErrorMsg(msgArr, "the account info in endpoint is invalid, should be like \"user:123456\"") + return nil + } + e.UserName = accountPair[0] + e.Password = accountPair[1] + if strings.LastIndex(accountURLPair[1], "/") != -1 { + addressDatabasePair := strings.Split(accountURLPair[1], "/") + e.Address = strings.Join(addressDatabasePair[:len(addressDatabasePair)-1], "/") + e.Database = addressDatabasePair[len(addressDatabasePair)-1] + } else { + e.Address = accountURLPair[1] + } + return nil +} + +func (e *EndpointModel) clear() { + e.Address = "" + e.Password = "" + e.UserName = "" + e.Database = "" +} + +// Migration Object + +type MigrationObjectModel struct { + WhiteList []DBObjectExpress `json:"whiteList"` +} + +type DBObjectExpress struct { + SchemaName string `json:"schemaName"` + // +optional + IsAll bool `json:"isAll"` + // +optional + TableList []TableObjectExpress `json:"tableList"` +} + +type TableObjectExpress struct { + TableName string `json:"tableName"` + // +optional + IsAll bool `json:"isAll"` +} + +func (m *MigrationObjectModel) BuildFromStrs(errMsgArr *[]string, objStrs []string) error { + if len(objStrs) == 0 { + BuildErrorMsg(errMsgArr, "migration object cannot be empty") + return nil + } + for _, str := range objStrs { + msg := "" + if str == "" { + msg = "the database or database.table in migration object cannot be empty" + } + dbTablePair := strings.Split(str, ".") + if len(dbTablePair) > 2 { + msg = fmt.Sprintf("[%s] is not a valid database or database.table", str) + } + if msg != "" { + BuildErrorMsg(errMsgArr, msg) + return nil + } + if len(dbTablePair) == 1 { + m.WhiteList = append(m.WhiteList, DBObjectExpress{ + SchemaName: str, + IsAll: true, + }) + } else { + dbObjPoint, err := m.ContainSchema(dbTablePair[0]) + if err != nil { + return err + } + if dbObjPoint != nil { + dbObjPoint.TableList = append(dbObjPoint.TableList, TableObjectExpress{ + TableName: dbTablePair[1], + IsAll: true, + }) + } else { + m.WhiteList = append(m.WhiteList, DBObjectExpress{ + SchemaName: dbTablePair[0], + TableList: []TableObjectExpress{{ + TableName: dbTablePair[1], + IsAll: true, + }}, + }) + } + } + } + return nil +} + +func (m *MigrationObjectModel) ContainSchema(schemaName string) (*DBObjectExpress, error) { + for i := 0; i < len(m.WhiteList); i++ { + if m.WhiteList[i].SchemaName == schemaName { + return &m.WhiteList[i], nil + } + } + return nil, nil +} + +func CliStepChangeToStructure() (map[string]string, []string) { + validStepMap := map[string]string{ + migrationv1.CliStepPreCheck.String(): migrationv1.CliStepPreCheck.String(), + migrationv1.CliStepInitStruct.String(): migrationv1.CliStepInitStruct.String(), + migrationv1.CliStepInitData.String(): migrationv1.CliStepInitData.String(), + migrationv1.CliStepCdc.String(): migrationv1.CliStepCdc.String(), + } + validStepKey := make([]string, 0) + for k := range validStepMap { + validStepKey = append(validStepKey, k) + } + return validStepMap, validStepKey +} + +type TaskTypeEnum string + +const ( + Initialization TaskTypeEnum = "initialization" + InitializationAndCdc TaskTypeEnum = "initialization-and-cdc" // default value +) + +func (s TaskTypeEnum) String() string { + return string(s) +} + +func IsMigrationCrdValidWithDynamic(dynamic *dynamic.Interface) (bool, error) { + resource := types.CustomResourceDefinitionGVR() + if err := APIResource(dynamic, &resource, "migrationtasks.datamigration.apecloud.io", "", nil); err != nil { + return false, err + } + if err := APIResource(dynamic, &resource, "migrationtemplates.datamigration.apecloud.io", "", nil); err != nil { + return false, err + } + if err := APIResource(dynamic, &resource, "serialjobs.common.apecloud.io", "", nil); err != nil { + return false, err + } + return true, nil +} + +func PrintCrdInvalidError(err error) { + if err == nil { + return + } + if !errors.IsNotFound(err) { + util.CheckErr(err) + } + fmt.Fprintf(os.Stderr, "hint: %s\n", invalidMigrationCrdAdvice) + os.Exit(cmdutil.DefaultErrorExitCode) +} + +func IsMigrationCrdValidWithFactory(factory cmdutil.Factory) (bool, error) { + dynamic, err := factory.DynamicClient() + if err != nil { + return false, err + } + return IsMigrationCrdValidWithDynamic(&dynamic) +} + +func APIResource(dynamic *dynamic.Interface, resource *schema.GroupVersionResource, name string, namespace string, res interface{}) error { + obj, err := (*dynamic).Resource(*resource).Namespace(namespace).Get(context.Background(), name, metav1.GetOptions{}, "") + if err != nil { + return err + } + if res != nil { + return runtime.DefaultUnstructuredConverter.FromUnstructured(obj.Object, res) + } + return nil +} + +func BuildErrorMsg(msgArr *[]string, msg string) { + if *msgArr == nil { + *msgArr = make([]string, 1) + } + *msgArr = append(*msgArr, msg) +} + +func BuildInitializationStepsOrder(task *migrationv1.MigrationTask, template *migrationv1.MigrationTemplate) []string { + stepMap := make(map[string]string) + for _, taskStep := range task.Spec.Initialization.Steps { + stepMap[taskStep.String()] = taskStep.String() + } + resultArr := make([]string, 0) + for _, stepModel := range template.Spec.Initialization.Steps { + if stepMap[stepModel.Step.String()] != "" { + resultArr = append(resultArr, stepModel.Step.CliString()) + } + } + return resultArr +} diff --git a/internal/cli/cmd/migration/base_test.go b/internal/cli/cmd/migration/base_test.go new file mode 100644 index 000000000..171555a08 --- /dev/null +++ b/internal/cli/cmd/migration/base_test.go @@ -0,0 +1,67 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + v1alpha1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" +) + +var _ = Describe("base", func() { + + Context("Basic function validate", func() { + + It("CliStepChangeToStructure", func() { + resultMap, resultKeyArr := CliStepChangeToStructure() + Expect(len(resultMap)).Should(Equal(4)) + Expect(len(resultKeyArr)).Should(Equal(4)) + }) + + It("BuildInitializationStepsOrder", func() { + task := &v1alpha1.MigrationTask{ + Spec: v1alpha1.MigrationTaskSpec{ + Initialization: v1alpha1.InitializationConfig{ + Steps: []v1alpha1.StepEnum{ + v1alpha1.StepFullLoad, + v1alpha1.StepStructPreFullLoad, + }, + }, + }, + } + template := &v1alpha1.MigrationTemplate{ + Spec: v1alpha1.MigrationTemplateSpec{ + Initialization: v1alpha1.InitializationModel{ + Steps: []v1alpha1.StepModel{ + {Step: v1alpha1.StepStructPreFullLoad}, + {Step: v1alpha1.StepFullLoad}, + }, + }, + }, + } + arr := BuildInitializationStepsOrder(task, template) + Expect(len(arr)).Should(Equal(2)) + Expect(arr[0]).Should(Equal(v1alpha1.StepStructPreFullLoad.CliString())) + Expect(arr[1]).Should(Equal(v1alpha1.StepFullLoad.CliString())) + }) + }) + +}) diff --git a/internal/cli/cmd/migration/cmd_builder.go b/internal/cli/cmd/migration/cmd_builder.go new file mode 100644 index 000000000..57d276b84 --- /dev/null +++ b/internal/cli/cmd/migration/cmd_builder.go @@ -0,0 +1,60 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" +) + +// NewMigrationCmd creates the cluster command +func NewMigrationCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "migration", + Short: "Data migration between two data sources.", + } + + groups := templates.CommandGroups{ + { + Message: "Basic Migration Commands:", + Commands: []*cobra.Command{ + NewMigrationCreateCmd(f, streams), + NewMigrationTemplatesCmd(f, streams), + NewMigrationListCmd(f, streams), + NewMigrationTerminateCmd(f, streams), + }, + }, + { + Message: "Migration Operation Commands:", + Commands: []*cobra.Command{ + NewMigrationDescribeCmd(f, streams), + NewMigrationLogsCmd(f, streams), + }, + }, + } + + // add subcommands + groups.Add(cmd) + templates.ActsAsRootCommand(cmd, nil, groups...) + + return cmd +} diff --git a/internal/cli/cmd/migration/cmd_builder_test.go b/internal/cli/cmd/migration/cmd_builder_test.go new file mode 100644 index 000000000..dc1105d26 --- /dev/null +++ b/internal/cli/cmd/migration/cmd_builder_test.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("cmd_builder", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + +}) diff --git a/internal/cli/cmd/migration/create.go b/internal/cli/cmd/migration/create.go new file mode 100644 index 000000000..a559dd223 --- /dev/null +++ b/internal/cli/cmd/migration/create.go @@ -0,0 +1,293 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "fmt" + "strings" + "time" + + "github.com/spf13/cobra" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/rand" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/create" + "github.com/apecloud/kubeblocks/internal/cli/types" + migrationv1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + AllStepsArr = []string{ + migrationv1.CliStepGlobal.String(), + migrationv1.CliStepPreCheck.String(), + migrationv1.CliStepCdc.String(), + migrationv1.CliStepInitStruct.String(), + migrationv1.CliStepInitData.String(), + } +) + +const ( + StringBoolTrue = "true" + StringBoolFalse = "false" +) + +type CreateMigrationOptions struct { + Template string `json:"template"` + TaskType string `json:"taskType,omitempty"` + Source string `json:"source"` + SourceEndpointModel EndpointModel `json:"sourceEndpointModel,omitempty"` + Sink string `json:"sink"` + SinkEndpointModel EndpointModel `json:"sinkEndpointModel,omitempty"` + MigrationObject []string `json:"migrationObject"` + MigrationObjectModel MigrationObjectModel `json:"migrationObjectModel,omitempty"` + Steps []string `json:"steps,omitempty"` + StepsModel []string `json:"stepsModel,omitempty"` + Tolerations []string `json:"tolerations,omitempty"` + TolerationModel map[string][]interface{} `json:"tolerationModel,omitempty"` + Resources []string `json:"resources,omitempty"` + ResourceModel map[string]interface{} `json:"resourceModel,omitempty"` + ServerID uint32 `json:"serverId,omitempty"` + create.CreateOptions `json:"-"` +} + +func NewMigrationCreateCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := &CreateMigrationOptions{ + CreateOptions: create.CreateOptions{ + Factory: f, + IOStreams: streams, + CueTemplateName: "migration_template.cue", + GVR: types.MigrationTaskGVR(), + }} + o.CreateOptions.Options = o + + cmd := &cobra.Command{ + Use: "create NAME", + Short: "Create a migration task.", + Example: CreateTemplate, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()), + Run: func(cmd *cobra.Command, args []string) { + o.Args = args + cmdutil.CheckErr(o.Complete()) + cmdutil.CheckErr(o.Validate()) + cmdutil.CheckErr(o.Run()) + }, + } + + cmd.Flags().StringVar(&o.Template, "template", "", "Specify migration template, run \"kbcli migration templates\" to show all available migration templates") + cmd.Flags().StringVar(&o.Source, "source", "", "Set the source database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}]'") + cmd.Flags().StringVar(&o.Sink, "sink", "", "Set the sink database information for migration.such as '{username}:{password}@{connection_address}:{connection_port}/[{database}]") + cmd.Flags().StringSliceVar(&o.MigrationObject, "migration-object", []string{}, "Set the data objects that need to be migrated,such as '\"db1.table1\",\"db2\"'") + cmd.Flags().StringSliceVar(&o.Steps, "steps", []string{}, "Set up migration steps,such as: precheck=true,init-struct=true,init-data=true,cdc=true") + cmd.Flags().StringSliceVar(&o.Tolerations, "tolerations", []string{}, "Tolerations for migration, such as '\"key=engineType,value=pg,operator=Equal,effect=NoSchedule\"'") + cmd.Flags().StringSliceVar(&o.Resources, "resources", []string{}, "Resources limit for migration, such as '\"cpu=3000m,memory=3Gi\"'") + + util.CheckErr(cmd.MarkFlagRequired("template")) + util.CheckErr(cmd.MarkFlagRequired("source")) + util.CheckErr(cmd.MarkFlagRequired("sink")) + util.CheckErr(cmd.MarkFlagRequired("migration-object")) + return cmd +} + +func (o *CreateMigrationOptions) Validate() error { + var err error + + if _, err = IsMigrationCrdValidWithDynamic(&o.Dynamic); err != nil { + PrintCrdInvalidError(err) + } + + if o.Template == "" { + return fmt.Errorf("migration template is needed, use \"kbcli migration templates\" to check and special one") + } + + errMsgArr := make([]string, 0) + // Source + o.SourceEndpointModel = EndpointModel{} + if err = o.SourceEndpointModel.BuildFromStr(&errMsgArr, o.Source); err != nil { + return err + } + // Sink + o.SinkEndpointModel = EndpointModel{} + if err = o.SinkEndpointModel.BuildFromStr(&errMsgArr, o.Sink); err != nil { + return err + } + + // MigrationObject + if err = o.MigrationObjectModel.BuildFromStrs(&errMsgArr, o.MigrationObject); err != nil { + return err + } + + // Steps & taskType + if err = o.BuildWithSteps(&errMsgArr); err != nil { + return err + } + + // Tolerations + if err = o.BuildWithTolerations(); err != nil { + return err + } + + // Resources + if err = o.BuildWithResources(); err != nil { + return err + } + + // RuntimeParams + if err = o.BuildWithRuntimeParams(); err != nil { + return err + } + + // Log errors if necessary + if len(errMsgArr) > 0 { + return fmt.Errorf(strings.Join(errMsgArr, ";\n")) + } + return nil +} + +func (o *CreateMigrationOptions) BuildWithSteps(errMsgArr *[]string) error { + taskType := InitializationAndCdc.String() + validStepMap, validStepKey := CliStepChangeToStructure() + enableCdc, enablePreCheck, enableInitStruct, enableInitData := StringBoolTrue, StringBoolTrue, StringBoolTrue, StringBoolTrue + if len(o.Steps) > 0 { + for _, step := range o.Steps { + stepArr := strings.Split(step, "=") + if len(stepArr) != 2 { + BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps setting is invalid", step)) + return nil + } + stepName := strings.ToLower(strings.TrimSpace(stepArr[0])) + enable := strings.ToLower(strings.TrimSpace(stepArr[1])) + if validStepMap[stepName] == "" { + BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps settings is invalid, the name should be one of: (%s)", step, validStepKey)) + return nil + } + if enable != StringBoolTrue && enable != StringBoolFalse { + BuildErrorMsg(errMsgArr, fmt.Sprintf("[%s] in steps settings is invalid, the value should be one of: (true false)", step)) + return nil + } + switch stepName { + case migrationv1.CliStepCdc.String(): + enableCdc = enable + case migrationv1.CliStepPreCheck.String(): + enablePreCheck = enable + case migrationv1.CliStepInitStruct.String(): + enableInitStruct = enable + case migrationv1.CliStepInitData.String(): + enableInitData = enable + } + } + + if enableInitData != StringBoolTrue { + BuildErrorMsg(errMsgArr, "step init-data is needed") + return nil + } + if enableCdc == StringBoolTrue { + taskType = InitializationAndCdc.String() + } else { + taskType = Initialization.String() + } + } + o.TaskType = taskType + o.StepsModel = []string{} + if enablePreCheck == StringBoolTrue { + o.StepsModel = append(o.StepsModel, migrationv1.StepPreCheck.String()) + } + if enableInitStruct == StringBoolTrue { + o.StepsModel = append(o.StepsModel, migrationv1.StepStructPreFullLoad.String()) + } + if enableInitData == StringBoolTrue { + o.StepsModel = append(o.StepsModel, migrationv1.StepFullLoad.String()) + } + return nil +} + +func (o *CreateMigrationOptions) BuildWithTolerations() error { + o.TolerationModel = o.buildTolerationOrResources(o.Tolerations) + tmp := make([]interface{}, 0) + for _, step := range AllStepsArr { + if o.TolerationModel[step] == nil { + o.TolerationModel[step] = tmp + } + } + return nil +} + +func (o *CreateMigrationOptions) BuildWithResources() error { + o.ResourceModel = make(map[string]interface{}) + for k, v := range o.buildTolerationOrResources(o.Resources) { + if len(v) >= 1 { + o.ResourceModel[k] = v[0] + } + } + for _, step := range AllStepsArr { + if o.ResourceModel[step] == nil { + o.ResourceModel[step] = v1.ResourceList{} + } + } + return nil +} + +func (o *CreateMigrationOptions) BuildWithRuntimeParams() error { + template := migrationv1.MigrationTemplate{} + templateGvr := types.MigrationTemplateGVR() + if err := APIResource(&o.CreateOptions.Dynamic, &templateGvr, o.Template, "", &template); err != nil { + return err + } + + // Generate random serverId for MySQL type database. Possible values are between 10001 and 2^32-10001 + if template.Spec.Source.DBType == migrationv1.MigrationDBTypeMySQL { + o.ServerID = o.generateRandomMySQLServerID() + } else { + o.ServerID = 10001 + } + + return nil +} + +func (o *CreateMigrationOptions) buildTolerationOrResources(raws []string) map[string][]interface{} { + results := make(map[string][]interface{}) + for _, raw := range raws { + step := migrationv1.CliStepGlobal.String() + tmpMap := map[string]interface{}{} + rawLoop: + for _, entries := range strings.Split(raw, ",") { + parts := strings.SplitN(entries, "=", 2) + k := strings.TrimSpace(parts[0]) + v := strings.TrimSpace(parts[1]) + if k == "step" { + switch v { + case migrationv1.CliStepPreCheck.String(), migrationv1.CliStepCdc.String(), migrationv1.CliStepInitStruct.String(), migrationv1.CliStepInitData.String(): + step = v + } + continue rawLoop + } + tmpMap[k] = v + } + results[step] = append(results[step], tmpMap) + } + return results +} + +func (o *CreateMigrationOptions) generateRandomMySQLServerID() uint32 { + rand.Seed(time.Now().UnixNano()) + return uint32(rand.Int63nRange(10001, 1<<32-10001)) +} diff --git a/internal/cli/cmd/migration/create_test.go b/internal/cli/cmd/migration/create_test.go new file mode 100644 index 000000000..1c7456852 --- /dev/null +++ b/internal/cli/cmd/migration/create_test.go @@ -0,0 +1,182 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "bytes" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/kubernetes/scheme" + cmdTest "k8s.io/kubectl/pkg/cmd/testing" + + app "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/cli/testing" + v1alpha1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" +) + +var ( + streams genericclioptions.IOStreams + out *bytes.Buffer + tf *cmdTest.TestFactory +) + +const ( + namespace = "test" +) + +var _ = Describe("create", func() { + o := &CreateMigrationOptions{} + + BeforeEach(func() { + streams, _, out, _ = genericclioptions.NewTestIOStreams() + tf = testing.NewTestFactory(namespace) + + _ = app.AddToScheme(scheme.Scheme) + + tf.Client = tf.UnstructuredClient + }) + + Context("Input params validate", func() { + var err error + errMsgArr := make([]string, 0, 3) + It("Endpoint with database", func() { + o.Source = "user:123456@127.0.0.1:5432/database" + err = o.SourceEndpointModel.BuildFromStr(&errMsgArr, o.Source) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.SourceEndpointModel.UserName).Should(Equal("user")) + Expect(o.SourceEndpointModel.Password).Should(Equal("123456")) + Expect(o.SourceEndpointModel.Address).Should(Equal("127.0.0.1:5432")) + Expect(o.SourceEndpointModel.Database).Should(Equal("database")) + Expect(len(errMsgArr)).Should(Equal(0)) + + o.Sink = "user:123456127.0.0.1:5432/database" + err = o.SinkEndpointModel.BuildFromStr(&errMsgArr, o.Sink) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(errMsgArr)).Should(Equal(1)) + }) + + It("Endpoint with no database", func() { + o.Source = "user:123456@127.0.0.1:3306" + errMsgArr := make([]string, 0, 3) + err = o.SourceEndpointModel.BuildFromStr(&errMsgArr, o.Source) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.SourceEndpointModel.UserName).Should(Equal("user")) + Expect(o.SourceEndpointModel.Password).Should(Equal("123456")) + Expect(o.SourceEndpointModel.Address).Should(Equal("127.0.0.1:3306")) + Expect(o.SourceEndpointModel.Database).Should(BeEmpty()) + Expect(len(errMsgArr)).Should(Equal(0)) + + o.Sink = "user:123456127.0.0.1:3306" + err = o.SinkEndpointModel.BuildFromStr(&errMsgArr, o.Sink) + Expect(err).ShouldNot(HaveOccurred()) + Expect(len(errMsgArr)).Should(Equal(1)) + }) + + It("MigrationObject", func() { + o.MigrationObject = []string{"schema_public.table1", "schema2.table2_1", "schema2.table2_2", "schema3"} + err = o.MigrationObjectModel.BuildFromStrs(&errMsgArr, o.MigrationObject) + Expect(err).ShouldNot(HaveOccurred()) + for _, obj := range o.MigrationObjectModel.WhiteList { + Expect(obj.SchemaName).Should(BeElementOf("schema_public", "schema2", "schema3")) + switch obj.SchemaName { + case "schema_public": + Expect(len(obj.TableList)).Should(Equal(1)) + Expect(obj.TableList[0].TableName).Should(Equal("table1")) + Expect(obj.TableList[0].IsAll).Should(BeTrue()) + case "schema2": + Expect(len(obj.TableList)).Should(Equal(2)) + for _, tb := range obj.TableList { + Expect(tb.TableName).Should(BeElementOf("table2_1", "table2_2")) + Expect(tb.IsAll).Should(BeTrue()) + } + case "schema3": + Expect(obj.IsAll).Should(BeTrue()) + } + } + }) + + It("Steps", func() { + o.Steps = make([]string, 0) + err = o.BuildWithSteps(&errMsgArr) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.TaskType).Should(Equal(InitializationAndCdc.String())) + Expect(o.StepsModel).Should(ContainElements(v1alpha1.StepPreCheck.String(), v1alpha1.StepStructPreFullLoad.String(), v1alpha1.StepFullLoad.String())) + o.Steps = []string{"precheck=true", "init-struct=false", "cdc=false"} + err = o.BuildWithSteps(&errMsgArr) + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.TaskType).Should(Equal(Initialization.String())) + Expect(o.StepsModel).Should(ContainElements(v1alpha1.StepPreCheck.String(), v1alpha1.StepFullLoad.String())) + }) + + It("Tolerations", func() { + o.Tolerations = []string{ + "step=global,key=engineType,value=pg,operator=Equal,effect=NoSchedule", + "step=init-data,key=engineType,value=pg1,operator=Equal,effect=NoSchedule", + "key=engineType,value=pg2,operator=Equal,effect=NoSchedule", + } + err = o.BuildWithTolerations() + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.TolerationModel[v1alpha1.CliStepGlobal.String()]).ShouldNot(BeEmpty()) + Expect(o.TolerationModel[v1alpha1.CliStepInitData.String()]).ShouldNot(BeEmpty()) + Expect(len(o.TolerationModel[v1alpha1.CliStepInitData.String()])).Should(Equal(1)) + Expect(len(o.TolerationModel[v1alpha1.CliStepGlobal.String()])).Should(Equal(2)) + Expect(len(o.TolerationModel[v1alpha1.CliStepPreCheck.String()])).Should(Equal(0)) + }) + + It("Resources", func() { + o.Resources = []string{ + "step=global,cpu=1000m,memory=1Gi", + "step=init-data,cpu=2000m,memory=2Gi", + "cpu=3000m,memory=3Gi", + } + err = o.BuildWithResources() + Expect(err).ShouldNot(HaveOccurred()) + Expect(o.ResourceModel[v1alpha1.CliStepGlobal.String()]).ShouldNot(BeEmpty()) + Expect(o.ResourceModel[v1alpha1.CliStepInitData.String()]).ShouldNot(BeEmpty()) + Expect(o.ResourceModel[v1alpha1.CliStepPreCheck.String()]).Should(BeEmpty()) + }) + + It("RuntimeParams", func() { + type void struct{} + var setValue void + serverIDSet := make(map[uint32]void) + + loopCount := 0 + for loopCount < 1000 { + newServerID := o.generateRandomMySQLServerID() + Expect(newServerID >= 10001 && newServerID <= 1<<32-10001).Should(BeTrue()) + serverIDSet[newServerID] = setValue + + loopCount += 1 + } + Expect(len(serverIDSet) > 500).Should(BeTrue()) + }) + }) + + Context("Mock run", func() { + It("test", func() { + cmd := NewMigrationCreateCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + }) +}) diff --git a/internal/cli/cmd/migration/describe.go b/internal/cli/cmd/migration/describe.go new file mode 100644 index 000000000..c9b835d5c --- /dev/null +++ b/internal/cli/cmd/migration/describe.go @@ -0,0 +1,304 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "context" + "fmt" + "io" + "sort" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + appv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + clientset "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/types" + v1alpha1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + newTbl = func(out io.Writer, title string, header ...interface{}) *printer.TablePrinter { + fmt.Fprintln(out, title) + tbl := printer.NewTablePrinter(out) + tbl.SetHeader(header...) + return tbl + } +) + +type describeOptions struct { + factory cmdutil.Factory + client clientset.Interface + dynamic dynamic.Interface + namespace string + + // resource type and names + gvr schema.GroupVersionResource + names []string + + *v1alpha1.MigrationObjects + genericclioptions.IOStreams +} + +func newOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *describeOptions { + return &describeOptions{ + factory: f, + IOStreams: streams, + gvr: types.MigrationTaskGVR(), + } +} + +func NewMigrationDescribeCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := newOptions(f, streams) + cmd := &cobra.Command{ + Use: "describe NAME", + Short: "Show details of a specific migration task.", + Example: DescribeExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(o.complete(args)) + util.CheckErr(o.run()) + }, + } + return cmd +} + +func (o *describeOptions) complete(args []string) error { + var err error + + if o.client, err = o.factory.KubernetesClientSet(); err != nil { + return err + } + + if o.dynamic, err = o.factory.DynamicClient(); err != nil { + return err + } + + if o.namespace, _, err = o.factory.ToRawKubeConfigLoader().Namespace(); err != nil { + return err + } + + if _, err = IsMigrationCrdValidWithDynamic(&o.dynamic); err != nil { + PrintCrdInvalidError(err) + } + + if len(args) == 0 { + return fmt.Errorf("migration task name should be specified") + } + o.names = args + return nil +} + +func (o *describeOptions) run() error { + for _, name := range o.names { + if err := o.describeMigration(name); err != nil { + return err + } + } + return nil +} + +func (o *describeOptions) describeMigration(name string) error { + var err error + if o.MigrationObjects, err = getMigrationObjects(o, name); err != nil { + return err + } + + // MigrationTask Summary + showTaskSummary(o.Task, o.Out) + + // MigrationTask Config + showTaskConfig(o.Task, o.Out) + + // MigrationTemplate Summary + showTemplateSummary(o.Template, o.Out) + + // Initialization Detail + showInitialization(o.Task, o.Template, o.Jobs, o.Out) + + switch o.Task.Spec.TaskType { + case v1alpha1.InitializationAndCdc, v1alpha1.CDC: + // Cdc Detail + showCdc(o.StatefulSets, o.Pods, o.Out) + + // Cdc Metrics + showCdcMetrics(o.Task, o.Out) + } + + fmt.Fprintln(o.Out) + + return nil +} + +func getMigrationObjects(o *describeOptions, taskName string) (*v1alpha1.MigrationObjects, error) { + obj := &v1alpha1.MigrationObjects{ + Task: &v1alpha1.MigrationTask{}, + Template: &v1alpha1.MigrationTemplate{}, + } + var err error + taskGvr := types.MigrationTaskGVR() + if err = APIResource(&o.dynamic, &taskGvr, taskName, o.namespace, obj.Task); err != nil { + return nil, err + } + templateGvr := types.MigrationTemplateGVR() + if err = APIResource(&o.dynamic, &templateGvr, obj.Task.Spec.Template, "", obj.Template); err != nil { + return nil, err + } + listOpts := func() metav1.ListOptions { + return metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", MigrationTaskLabel, taskName), + } + } + if obj.Jobs, err = o.client.BatchV1().Jobs(o.namespace).List(context.Background(), listOpts()); err != nil { + return nil, err + } + if obj.Pods, err = o.client.CoreV1().Pods(o.namespace).List(context.Background(), listOpts()); err != nil { + return nil, err + } + if obj.StatefulSets, err = o.client.AppsV1().StatefulSets(o.namespace).List(context.Background(), listOpts()); err != nil { + return nil, err + } + return obj, nil +} + +func showTaskSummary(task *v1alpha1.MigrationTask, out io.Writer) { + if task == nil { + return + } + title := fmt.Sprintf("Name: %s\t Status: %s", task.Name, task.Status.TaskStatus) + tbl := newTbl(out, title, "NAMESPACE", "CREATED-TIME", "START-TIME", "FINISHED-TIME") + tbl.AddRow(task.Namespace, util.TimeFormatWithDuration(&task.CreationTimestamp, time.Second), util.TimeFormatWithDuration(task.Status.StartTime, time.Second), util.TimeFormatWithDuration(task.Status.FinishTime, time.Second)) + tbl.Print() +} + +func showTaskConfig(task *v1alpha1.MigrationTask, out io.Writer) { + if task == nil { + return + } + tbl := newTbl(out, "\nMigration Config:") + tbl.AddRow("source", fmt.Sprintf("%s:%s@%s/%s", + task.Spec.SourceEndpoint.UserName, + task.Spec.SourceEndpoint.Password, + task.Spec.SourceEndpoint.Address, + task.Spec.SourceEndpoint.DatabaseName, + )) + tbl.AddRow("sink", fmt.Sprintf("%s:%s@%s/%s", + task.Spec.SinkEndpoint.UserName, + task.Spec.SinkEndpoint.Password, + task.Spec.SinkEndpoint.Address, + task.Spec.SinkEndpoint.DatabaseName, + )) + tbl.AddRow("migration objects", task.Spec.MigrationObj.String(true)) + tbl.Print() +} + +func showTemplateSummary(template *v1alpha1.MigrationTemplate, out io.Writer) { + if template == nil { + return + } + title := fmt.Sprintf("\nTemplate: %s\t", template.Name) + tbl := newTbl(out, title, "DATABASE-MAPPING", "STATUS") + tbl.AddRow(template.Spec.Description, template.Status.Phase) + tbl.Print() +} + +func showInitialization(task *v1alpha1.MigrationTask, template *v1alpha1.MigrationTemplate, jobList *batchv1.JobList, out io.Writer) { + if len(jobList.Items) == 0 { + return + } + sort.SliceStable(jobList.Items, func(i, j int) bool { + jobName1 := jobList.Items[i].Name + jobName2 := jobList.Items[j].Name + order1, _ := strconv.ParseInt(string([]byte(jobName1)[strings.LastIndex(jobName1, "-")+1:]), 10, 8) + order2, _ := strconv.ParseInt(string([]byte(jobName2)[strings.LastIndex(jobName2, "-")+1:]), 10, 8) + return order1 < order2 + }) + cliStepOrder := BuildInitializationStepsOrder(task, template) + tbl := newTbl(out, "\nInitialization:", "STEP", "NAMESPACE", "STATUS", "CREATED_TIME", "START-TIME", "FINISHED-TIME") + if len(cliStepOrder) != len(jobList.Items) { + return + } + for i, job := range jobList.Items { + tbl.AddRow(cliStepOrder[i], job.Namespace, getJobStatus(job.Status.Conditions), util.TimeFormatWithDuration(&job.CreationTimestamp, time.Second), util.TimeFormatWithDuration(job.Status.StartTime, time.Second), util.TimeFormatWithDuration(job.Status.CompletionTime, time.Second)) + } + tbl.Print() +} + +func showCdc(statefulSets *appv1.StatefulSetList, pods *v1.PodList, out io.Writer) { + if len(pods.Items) == 0 || len(statefulSets.Items) == 0 { + return + } + tbl := newTbl(out, "\nCdc:", "NAMESPACE", "STATUS", "CREATED_TIME", "START-TIME") + for _, pod := range pods.Items { + if pod.Annotations[MigrationTaskStepAnnotation] != v1alpha1.StepCdc.String() { + continue + } + tbl.AddRow(pod.Namespace, getCdcStatus(&statefulSets.Items[0], &pod), util.TimeFormatWithDuration(&pod.CreationTimestamp, time.Second), util.TimeFormatWithDuration(pod.Status.StartTime, time.Second)) + } + tbl.Print() +} + +func showCdcMetrics(task *v1alpha1.MigrationTask, out io.Writer) { + if task.Status.Cdc.Metrics == nil || len(task.Status.Cdc.Metrics) == 0 { + return + } + arr := make([]string, 0) + for mKey := range task.Status.Cdc.Metrics { + arr = append(arr, mKey) + } + sort.Strings(arr) + tbl := newTbl(out, "\nCdc Metrics:") + for _, k := range arr { + tbl.AddRow(k, task.Status.Cdc.Metrics[k]) + } + tbl.Print() +} + +func getJobStatus(conditions []batchv1.JobCondition) string { + if len(conditions) == 0 { + return "-" + } else { + return string(conditions[len(conditions)-1].Type) + } +} + +func getCdcStatus(statefulSet *appv1.StatefulSet, cdcPod *v1.Pod) v1.PodPhase { + if cdcPod.Status.Phase == v1.PodRunning && + statefulSet.Status.Replicas > statefulSet.Status.AvailableReplicas { + if time.Now().Unix()-statefulSet.CreationTimestamp.Time.Unix() < 10*60 { + return v1.PodPending + } else { + return v1.PodFailed + } + } else { + return cdcPod.Status.Phase + } +} diff --git a/internal/cli/cmd/migration/describe_test.go b/internal/cli/cmd/migration/describe_test.go new file mode 100644 index 000000000..1ce4890cd --- /dev/null +++ b/internal/cli/cmd/migration/describe_test.go @@ -0,0 +1,79 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("describe", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationDescribeCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + + It("func test", func() { + sts := appv1.StatefulSet{ + Status: appv1.StatefulSetStatus{ + Replicas: 1, + }, + } + pod := corev1.Pod{} + + sts.Status.AvailableReplicas = 0 + pod.Status.Phase = corev1.PodFailed + Expect(getCdcStatus(&sts, &pod)).Should(Equal(corev1.PodFailed)) + + sts.Status.AvailableReplicas = 1 + pod.Status.Phase = corev1.PodPending + Expect(getCdcStatus(&sts, &pod)).Should(Equal(corev1.PodPending)) + + sts.Status.AvailableReplicas = 1 + pod.Status.Phase = corev1.PodRunning + Expect(getCdcStatus(&sts, &pod)).Should(Equal(corev1.PodRunning)) + + sts.Status.AvailableReplicas = 0 + t1, _ := time.ParseDuration("-30m") + sts.CreationTimestamp = v1.NewTime(time.Now().Add(t1)) + pod.Status.Phase = corev1.PodRunning + Expect(getCdcStatus(&sts, &pod)).Should(Equal(corev1.PodFailed)) + + sts.Status.AvailableReplicas = 0 + sts.CreationTimestamp = v1.NewTime(time.Now()) + pod.Status.Phase = corev1.PodRunning + Expect(getCdcStatus(&sts, &pod)).Should(Equal(corev1.PodPending)) + }) + +}) diff --git a/internal/cli/cmd/migration/examples.go b/internal/cli/cmd/migration/examples.go new file mode 100644 index 000000000..7dd35f366 --- /dev/null +++ b/internal/cli/cmd/migration/examples.go @@ -0,0 +1,107 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import "k8s.io/kubectl/pkg/util/templates" + +// Cli Migration Command Examples +var ( + CreateTemplate = templates.Examples(` + # Create a migration task to migrate the entire database under mysql: mydb1 and mytable1 under database: mydb2 to the target mysql + kbcli migration create mytask --template apecloud-mysql2mysql + --source user:123456@127.0.0.1:3306 + --sink user:123456@127.0.0.1:3305 + --migration-object '"mydb1","mydb2.mytable1"' + + # Create a migration task to migrate the schema: myschema under database: mydb1 under PostgreSQL to the target PostgreSQL + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + + # Use prechecks, data initialization, CDC, but do not perform structure initialization + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --steps precheck=true,init-struct=false,init-data=true,cdc=true + + # Create a migration task with two tolerations + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --tolerations '"step=global,key=engineType,value=pg,operator=Equal,effect=NoSchedule","step=init-data,key=diskType,value=ssd,operator=Equal,effect=NoSchedule"' + + # Limit resource usage when performing data initialization + kbcli migration create mytask --template apecloud-pg2pg + --source user:123456@127.0.0.1:3306/mydb1 + --sink user:123456@127.0.0.1:3305/mydb1 + --migration-object '"myschema"' + --resources '"step=init-data,cpu=1000m,memory=1Gi"' + `) + DescribeExample = templates.Examples(` + # describe a specified migration task + kbcli migration describe mytask + `) + ListExample = templates.Examples(` + # list all migration tasks + kbcli migration list + + # list a single migration task with specified NAME + kbcli migration list mytask + + # list a single migration task in YAML output format + kbcli migration list mytask -o yaml + + # list a single migration task in JSON output format + kbcli migration list mytask -o json + + # list a single migration task in wide output format + kbcli migration list mytask -o wide + `) + TemplateExample = templates.Examples(` + # list all migration templates + kbcli migration templates + + # list a single migration template with specified NAME + kbcli migration templates mytemplate + + # list a single migration template in YAML output format + kbcli migration templates mytemplate -o yaml + + # list a single migration template in JSON output format + kbcli migration templates mytemplate -o json + + # list a single migration template in wide output format + kbcli migration templates mytemplate -o wide + `) + DeleteExample = templates.Examples(` + # terminate a migration task named mytask and delete resources in k8s without affecting source and target data in database + kbcli migration terminate mytask + `) + LogsExample = templates.Examples(` + # Logs when returning to the "init-struct" step from the migration task mytask + kbcli migration logs mytask --step init-struct + + # Logs only the most recent 20 lines when returning to the "cdc" step from the migration task mytask + kbcli migration logs mytask --step cdc --tail=20 + `) +) diff --git a/internal/cli/cmd/migration/list.go b/internal/cli/cmd/migration/list.go new file mode 100644 index 000000000..958b31df2 --- /dev/null +++ b/internal/cli/cmd/migration/list.go @@ -0,0 +1,50 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/list" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +func NewMigrationListCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := list.NewListOptions(f, streams, types.MigrationTaskGVR()) + cmd := &cobra.Command{ + Use: "list [NAME]", + Short: "List migration tasks.", + Example: ListExample, + Aliases: []string{"ls"}, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), + Run: func(cmd *cobra.Command, args []string) { + _, validErr := IsMigrationCrdValidWithFactory(o.Factory) + PrintCrdInvalidError(validErr) + o.Names = args + _, err := o.Run() + util.CheckErr(err) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/migration/list_test.go b/internal/cli/cmd/migration/list_test.go new file mode 100644 index 000000000..01be8a3b6 --- /dev/null +++ b/internal/cli/cmd/migration/list_test.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("list", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationListCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + +}) diff --git a/internal/cli/cmd/migration/logs.go b/internal/cli/cmd/migration/logs.go new file mode 100644 index 000000000..fe5ec056b --- /dev/null +++ b/internal/cli/cmd/migration/logs.go @@ -0,0 +1,228 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "context" + "fmt" + "strconv" + "strings" + "time" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/client-go/dynamic" + "k8s.io/client-go/kubernetes" + cmdlogs "k8s.io/kubectl/pkg/cmd/logs" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/polymorphichelpers" + + "github.com/apecloud/kubeblocks/internal/cli/exec" + "github.com/apecloud/kubeblocks/internal/cli/types" + migrationv1 "github.com/apecloud/kubeblocks/internal/cli/types/migrationapi" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type LogsOptions struct { + taskName string + step string + Client *kubernetes.Clientset + Dynamic dynamic.Interface + *exec.ExecOptions + logOptions cmdlogs.LogsOptions +} + +func NewMigrationLogsCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + l := &LogsOptions{ + ExecOptions: exec.NewExecOptions(f, streams), + logOptions: cmdlogs.LogsOptions{ + Tail: -1, + IOStreams: streams, + }, + } + cmd := &cobra.Command{ + Use: "logs NAME", + Short: "Access migration task log file.", + Example: LogsExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()), + Run: func(cmd *cobra.Command, args []string) { + util.CheckErr(l.ExecOptions.Complete()) + util.CheckErr(l.complete(f, cmd, args)) + util.CheckErr(l.validate()) + util.CheckErr(l.runLogs()) + }, + } + l.addFlags(cmd) + return cmd +} + +func (o *LogsOptions) addFlags(cmd *cobra.Command) { + cmd.Flags().StringVar(&o.step, "step", "", "Specify the step. Allow values: precheck,init-struct,init-data,cdc") + + o.logOptions.AddFlags(cmd) +} + +// complete customs complete function for logs +func (o *LogsOptions) complete(f cmdutil.Factory, cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("migration task name should be specified") + } + if len(args) > 0 { + o.taskName = args[0] + } + if o.step == "" { + return fmt.Errorf("migration task step should be specified") + } + var err error + o.logOptions.Namespace, _, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return err + } + + o.Dynamic, err = f.DynamicClient() + if err != nil { + return err + } + + o.Client, err = f.KubernetesClientSet() + if err != nil { + return err + } + + if _, err = IsMigrationCrdValidWithDynamic(&o.Dynamic); err != nil { + PrintCrdInvalidError(err) + } + + taskObj, err := o.getMigrationObjects(o.taskName) + if err != nil { + return fmt.Errorf("failed to find the migrationtask") + } + pod := o.getPodByStep(taskObj, strings.TrimSpace(o.step)) + if pod == nil { + return fmt.Errorf("migrationtask[%s] step[%s] 's pod not found", taskObj.Task.Name, o.step) + } + o.logOptions.RESTClientGetter = f + o.logOptions.LogsForObject = polymorphichelpers.LogsForObjectFn + o.logOptions.Object = pod + o.logOptions.Options, _ = o.logOptions.ToLogOptions() + o.Pod = pod + + return nil +} + +func (o *LogsOptions) validate() error { + if len(o.taskName) == 0 { + return fmt.Errorf("migration task name must be specified") + } + + if o.logOptions.LimitBytes < 0 { + return fmt.Errorf("--limit-bytes must be greater than 0") + } + if o.logOptions.Tail < -1 { + return fmt.Errorf("--tail must be greater than or equal to -1") + } + if len(o.logOptions.SinceTime) > 0 && o.logOptions.SinceSeconds != 0 { + return fmt.Errorf("at most one of `sinceTime` or `sinceSeconds` may be specified") + } + logsOptions, ok := o.logOptions.Options.(*corev1.PodLogOptions) + if !ok { + return fmt.Errorf("unexpected logs options object") + } + if logsOptions.SinceSeconds != nil && *logsOptions.SinceSeconds < int64(0) { + return fmt.Errorf("--since must be greater than 0") + } + if logsOptions.TailLines != nil && *logsOptions.TailLines < -1 { + return fmt.Errorf("--tail must be greater than or equal to -1") + } + return nil +} + +func (o *LogsOptions) getMigrationObjects(taskName string) (*migrationv1.MigrationObjects, error) { + obj := &migrationv1.MigrationObjects{ + Task: &migrationv1.MigrationTask{}, + Template: &migrationv1.MigrationTemplate{}, + } + var err error + taskGvr := types.MigrationTaskGVR() + if err = APIResource(&o.Dynamic, &taskGvr, taskName, o.logOptions.Namespace, obj.Task); err != nil { + return nil, err + } + templateGvr := types.MigrationTemplateGVR() + if err = APIResource(&o.Dynamic, &templateGvr, obj.Task.Spec.Template, "", obj.Template); err != nil { + return nil, err + } + listOpts := func() metav1.ListOptions { + return metav1.ListOptions{ + LabelSelector: fmt.Sprintf("%s=%s", MigrationTaskLabel, taskName), + } + } + if obj.Pods, err = o.Client.CoreV1().Pods(o.logOptions.Namespace).List(context.Background(), listOpts()); err != nil { + return nil, err + } + return obj, nil +} + +func (o *LogsOptions) runLogs() error { + requests, err := o.logOptions.LogsForObject(o.logOptions.RESTClientGetter, o.logOptions.Object, o.logOptions.Options, 60*time.Second, false) + if err != nil { + return err + } + for _, request := range requests { + if err := cmdlogs.DefaultConsumeRequest(request, o.Out); err != nil { + if !o.logOptions.IgnoreLogErrors { + return err + } + fmt.Fprintf(o.Out, "error: %v\n", err) + } + } + return nil +} + +func (o *LogsOptions) getPodByStep(taskObj *migrationv1.MigrationObjects, step string) *corev1.Pod { + if taskObj == nil || len(taskObj.Pods.Items) == 0 { + return nil + } + switch step { + case migrationv1.CliStepCdc.String(): + for _, pod := range taskObj.Pods.Items { + if pod.Annotations[MigrationTaskStepAnnotation] == migrationv1.StepCdc.String() { + return &pod + } + } + case migrationv1.CliStepPreCheck.String(), migrationv1.CliStepInitStruct.String(), migrationv1.CliStepInitData.String(): + stepArr := BuildInitializationStepsOrder(taskObj.Task, taskObj.Template) + orderNo := "-1" + for index, stepByTemplate := range stepArr { + if step == stepByTemplate { + orderNo = strconv.Itoa(index) + break + } + } + for _, pod := range taskObj.Pods.Items { + if pod.Annotations[SerialJobOrderAnnotation] != "" && + pod.Annotations[SerialJobOrderAnnotation] == orderNo { + return &pod + } + } + } + return nil +} diff --git a/internal/cli/cmd/migration/logs_test.go b/internal/cli/cmd/migration/logs_test.go new file mode 100644 index 000000000..d97bb04ef --- /dev/null +++ b/internal/cli/cmd/migration/logs_test.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("logs", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationLogsCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + +}) diff --git a/internal/cli/cmd/migration/suite_test.go b/internal/cli/cmd/migration/suite_test.go new file mode 100644 index 000000000..d682b3e74 --- /dev/null +++ b/internal/cli/cmd/migration/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestMigration(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Migration Suite") +} diff --git a/internal/cli/cmd/migration/templates.go b/internal/cli/cmd/migration/templates.go new file mode 100644 index 000000000..89163507e --- /dev/null +++ b/internal/cli/cmd/migration/templates.go @@ -0,0 +1,50 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/list" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +func NewMigrationTemplatesCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := list.NewListOptions(f, streams, types.MigrationTemplateGVR()) + cmd := &cobra.Command{ + Use: "templates [NAME]", + Short: "List migration templates.", + Example: TemplateExample, + Aliases: []string{"tp", "template"}, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, o.GVR), + Run: func(cmd *cobra.Command, args []string) { + _, validErr := IsMigrationCrdValidWithFactory(o.Factory) + PrintCrdInvalidError(validErr) + o.Names = args + _, err := o.Run() + util.CheckErr(err) + }, + } + o.AddFlags(cmd) + return cmd +} diff --git a/internal/cli/cmd/migration/templates_test.go b/internal/cli/cmd/migration/templates_test.go new file mode 100644 index 000000000..b7e5662a4 --- /dev/null +++ b/internal/cli/cmd/migration/templates_test.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("templates", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationTemplatesCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + +}) diff --git a/internal/cli/cmd/migration/terminate.go b/internal/cli/cmd/migration/terminate.go new file mode 100644 index 000000000..7b599e772 --- /dev/null +++ b/internal/cli/cmd/migration/terminate.go @@ -0,0 +1,57 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + + "github.com/apecloud/kubeblocks/internal/cli/delete" + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +func NewMigrationTerminateCmd(f cmdutil.Factory, streams genericclioptions.IOStreams) *cobra.Command { + o := delete.NewDeleteOptions(f, streams, types.MigrationTaskGVR()) + cmd := &cobra.Command{ + Use: "terminate NAME", + Short: "Delete migration task.", + Example: DeleteExample, + ValidArgsFunction: util.ResourceNameCompletionFunc(f, types.MigrationTaskGVR()), + Run: func(cmd *cobra.Command, args []string) { + _, validErr := IsMigrationCrdValidWithFactory(o.Factory) + PrintCrdInvalidError(validErr) + util.CheckErr(deleteMigrationTask(o, args)) + }, + } + o.AddFlags(cmd) + return cmd +} + +func deleteMigrationTask(o *delete.DeleteOptions, args []string) error { + if len(args) == 0 { + return fmt.Errorf("missing migration task name") + } + o.Names = args + return o.Run() +} diff --git a/internal/cli/cmd/migration/terminate_test.go b/internal/cli/cmd/migration/terminate_test.go new file mode 100644 index 000000000..bb443343a --- /dev/null +++ b/internal/cli/cmd/migration/terminate_test.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package migration + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" +) + +var _ = Describe("terminate", func() { + + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + It("command build", func() { + cmd := NewMigrationTerminateCmd(tf, streams) + Expect(cmd).ShouldNot(BeNil()) + }) + +}) diff --git a/internal/cli/cmd/options/options.go b/internal/cli/cmd/options/options.go index fd6ba5ca3..08172cf77 100644 --- a/internal/cli/cmd/options/options.go +++ b/internal/cli/cmd/options/options.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package options diff --git a/internal/cli/cmd/options/options_test.go b/internal/cli/cmd/options/options_test.go index f8265f3fd..d10a6740c 100644 --- a/internal/cli/cmd/options/options_test.go +++ b/internal/cli/cmd/options/options_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package options diff --git a/internal/cli/cmd/playground/base.go b/internal/cli/cmd/playground/base.go index dfaea297e..df1aa2912 100644 --- a/internal/cli/cmd/playground/base.go +++ b/internal/cli/cmd/playground/base.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -27,9 +30,10 @@ import ( type baseOptions struct { startTime time.Time + Timeout time.Duration // prevCluster is the previous cluster info prevCluster *cp.K8sClusterInfo - // kubeConfigPath is the tmp kubeconfig path that will be used when int and destroy + // kubeConfigPath is the tmp kubeconfig path that will be used when init and destroy kubeConfigPath string // stateFilePath is the state file path stateFilePath string diff --git a/internal/cli/cmd/playground/destroy.go b/internal/cli/cmd/playground/destroy.go index 72870977e..d2ed31ae1 100644 --- a/internal/cli/cmd/playground/destroy.go +++ b/internal/cli/cmd/playground/destroy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -40,6 +43,7 @@ import ( cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" "github.com/apecloud/kubeblocks/internal/cli/cmd/kubeblocks" "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" @@ -55,6 +59,12 @@ var ( type destroyOptions struct { genericclioptions.IOStreams baseOptions + + // purge resources, before destroying kubernetes cluster we should delete cluster and + // uninstall KubeBlocks + autoApprove bool + purge bool + timeout time.Duration } func newDestroyCmd(streams genericclioptions.IOStreams) *cobra.Command { @@ -63,24 +73,17 @@ func newDestroyCmd(streams genericclioptions.IOStreams) *cobra.Command { } cmd := &cobra.Command{ Use: "destroy", - Short: "Destroy the playground kubernetes cluster.", + Short: "Destroy the playground KubeBlocks and kubernetes cluster.", Example: destroyExample, Run: func(cmd *cobra.Command, args []string) { util.CheckErr(o.validate()) util.CheckErr(o.destroy()) }, } - return cmd -} -func newGuideCmd() *cobra.Command { - cmd := &cobra.Command{ - Use: "guide", - Short: "Display playground cluster user guide.", - Run: func(cmd *cobra.Command, args []string) { - printGuide() - }, - } + cmd.Flags().BoolVar(&o.purge, "purge", true, "Purge all resources before destroying kubernetes cluster, delete all clusters created by KubeBlocks and uninstall KubeBlocks.") + cmd.Flags().DurationVar(&o.timeout, "timeout", 300*time.Second, "Time to wait for installing KubeBlocks, such as --timeout=10m") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval before destroying the playground") return cmd } @@ -97,16 +100,16 @@ func (o *destroyOptions) destroy() error { // destroyLocal destroy local k3d cluster that will destroy all resources func (o *destroyOptions) destroyLocal() error { - provider := cp.NewLocalCloudProvider(o.Out, o.ErrOut) - spinner := printer.Spinner(o.Out, "%-50s", "Delete playground k3d cluster "+o.prevCluster.ClusterName) - defer spinner(false) + provider, _ := cp.New(cp.Local, "", o.Out, o.ErrOut) + s := spinner.New(o.Out, spinnerMsg("Delete playground k3d cluster "+o.prevCluster.ClusterName)) + defer s.Fail() if err := provider.DeleteK8sCluster(o.prevCluster); err != nil { if !strings.Contains(err.Error(), "no cluster found") && !strings.Contains(err.Error(), "does not exist") { return err } } - spinner(true) + s.Success() if err := o.removeKubeConfig(); err != nil { return err @@ -114,35 +117,34 @@ func (o *destroyOptions) destroyLocal() error { return o.removeStateFile() } -// destroyCloud destroy cloud kubernetes cluster, before destroy, we should delete +// destroyCloud destroys cloud kubernetes cluster, before destroying, we should delete // all clusters created by KubeBlocks, uninstall KubeBlocks and remove the KubeBlocks -// namespace that will destroy all resources created by KubeBlocks, avoid to leave -// some resources +// namespace that will destroy all resources created by KubeBlocks, avoid to leave resources behind func (o *destroyOptions) destroyCloud() error { var err error - // start to destroy cluster - printer.Warning(o.Out, `This action will uninstall KubeBlocks and delete the kubernetes cluster, - there may be residual resources, please confirm and manually clean up related - resources after this action. + printer.Warning(o.Out, `This action will destroy the kubernetes cluster, there may be residual resources, + please confirm and manually clean up related resources after this action. `) - fmt.Fprintf(o.Out, "Do you really want to destroy the kubernetes cluster %s?\n%s\n\n This is no undo. Only 'yes' will be accepted to confirm.\n\n", + fmt.Fprintf(o.Out, "Do you really want to destroy the kubernetes cluster %s?\n%s\n\n The operation cannot be rollbacked. Only 'yes' will be accepted to confirm.\n\n", o.prevCluster.ClusterName, o.prevCluster.String()) // confirm to destroy - entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() - if entered != yesStr { - fmt.Fprintf(o.Out, "\nPlayground destroy cancelled.\n") - return cmdutil.ErrExit + if !o.autoApprove { + entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() + if entered != yesStr { + fmt.Fprintf(o.Out, "\nPlayground destroy cancelled.\n") + return cmdutil.ErrExit + } } o.startTime = time.Now() // for cloud provider, we should delete all clusters created by KubeBlocks first, // uninstall KubeBlocks and remove the KubeBlocks namespace, then destroy the - // playground cluster, avoid to leave some resources. + // playground cluster, avoid to leave resources behind. // delete all clusters created by KubeBlocks, MUST BE VERY CAUTIOUS, use the right // kubeconfig and context, otherwise, it will delete the wrong cluster. if err = o.deleteClustersAndUninstallKB(); err != nil { @@ -187,6 +189,11 @@ func (o *destroyOptions) destroyCloud() error { func (o *destroyOptions) deleteClustersAndUninstallKB() error { var err error + if !o.purge { + klog.V(1).Infof("Skip to delete all clusters created by KubeBlocks and uninstall KubeBlocks") + return nil + } + if o.prevCluster.KubeConfig == "" { fmt.Fprintf(o.Out, "No kubeconfig found for kubernetes cluster %s in %s \n", o.prevCluster.ClusterName, o.stateFilePath) @@ -254,13 +261,13 @@ func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error { return nil } - spinner := printer.Spinner(o.Out, fmt.Sprintf("%-50s", "Delete clusters created by KubeBlocks")) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Delete clusters created by KubeBlocks")) + defer s.Fail() // get all clusters clusters, err := getClusters() if clusters == nil || len(clusters.Items) == 0 { - spinner(true) + s.Success() return nil } @@ -290,7 +297,7 @@ func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error { // check all clusters termination policy is WipeOut if checkWipeOut { - if err = wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) { + if err = wait.PollImmediate(5*time.Second, o.timeout, func() (bool, error) { return checkClusters(func(cluster *appsv1alpha1.Cluster) bool { if cluster.Spec.TerminationPolicy != appsv1alpha1.WipeOut { klog.V(1).Infof("Cluster %s termination policy is %s", cluster.Name, cluster.Spec.TerminationPolicy) @@ -308,7 +315,7 @@ func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error { } // check and wait all clusters are deleted - if err = wait.PollImmediate(5*time.Second, 5*time.Minute, func() (bool, error) { + if err = wait.PollImmediate(5*time.Second, o.timeout, func() (bool, error) { return checkClusters(func(cluster *appsv1alpha1.Cluster) bool { // always return false if any cluster is not deleted klog.V(1).Infof("Cluster %s is not deleted", cluster.Name) @@ -318,7 +325,7 @@ func (o *destroyOptions) deleteClusters(dynamic dynamic.Interface) error { return err } - spinner(true) + s.Success() return nil } @@ -329,6 +336,7 @@ func (o *destroyOptions) uninstallKubeBlocks(client kubernetes.Interface, dynami IOStreams: o.IOStreams, Client: client, Dynamic: dynamic, + Wait: true, }, AutoApprove: true, RemoveNamespace: true, @@ -346,17 +354,17 @@ func (o *destroyOptions) uninstallKubeBlocks(client kubernetes.Interface, dynami } func (o *destroyOptions) removeKubeConfig() error { - spinner := printer.Spinner(o.Out, "%-50s", "Remove kubeconfig from "+defaultKubeConfigPath) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Remove kubeconfig from "+defaultKubeConfigPath)) + defer s.Fail() if err := kubeConfigRemove(o.prevCluster.KubeConfig, defaultKubeConfigPath); err != nil { if os.IsNotExist(err) { - spinner(true) + s.Success() return nil } else { return err } } - spinner(true) + s.Success() clusterContext, err := kubeConfigCurrentContext(o.prevCluster.KubeConfig) if err != nil { @@ -369,7 +377,7 @@ func (o *destroyOptions) removeKubeConfig() error { return err } - // current context is deleted, notify user to set current context like kubectl + // current context is deleted, notify user to set current context with kubectl if currentContext == clusterContext { printer.Warning(o.Out, "this removed your active context, use \"kubectl config use-context\" to select a different one\n") } @@ -378,11 +386,11 @@ func (o *destroyOptions) removeKubeConfig() error { // remove state file func (o *destroyOptions) removeStateFile() error { - spinner := printer.Spinner(o.Out, "Remove state file %s", o.stateFilePath) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Remove state file %s", o.stateFilePath)) + defer s.Fail() if err := removeStateFile(o.stateFilePath); err != nil { return err } - spinner(true) + s.Success() return nil } diff --git a/internal/cli/cmd/playground/destroy_test.go b/internal/cli/cmd/playground/destroy_test.go index f7cdcad37..0cfb5c819 100644 --- a/internal/cli/cmd/playground/destroy_test.go +++ b/internal/cli/cmd/playground/destroy_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground diff --git a/internal/cli/cmd/playground/init.go b/internal/cli/cmd/playground/init.go index b050070c1..9bafc648b 100644 --- a/internal/cli/cmd/playground/init.go +++ b/internal/cli/cmd/playground/init.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -37,8 +40,8 @@ import ( cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" cmdcluster "github.com/apecloud/kubeblocks/internal/cli/cmd/cluster" "github.com/apecloud/kubeblocks/internal/cli/cmd/kubeblocks" - "github.com/apecloud/kubeblocks/internal/cli/create" "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/internal/cli/util/helm" @@ -47,12 +50,18 @@ import ( ) var ( + initLong = templates.LongDesc(`Bootstrap a kubernetes cluster and install KubeBlocks for playground. + +If no cloud provider is specified, a k3d cluster named kb-playground will be created on local host, +otherwise a kubernetes cluster will be created on the specified cloud. Then KubeBlocks will be installed +on the created kubernetes cluster, and an apecloud-mysql cluster named mycluster will be created.`) + initExample = templates.Examples(` - # create a k3d cluster on local host and install KubeBlocks + # create a k3d cluster on local host and install KubeBlocks kbcli playground init # create an AWS EKS cluster and install KubeBlocks, the region is required - kbcli playground init --cloud-provider aws --region cn-northwest-1 + kbcli playground init --cloud-provider aws --region us-west-1 # create an Alibaba cloud ACK cluster and install KubeBlocks, the region is required kbcli playground init --cloud-provider alicloud --region cn-hangzhou @@ -61,9 +70,29 @@ var ( kbcli playground init --cloud-provider tencentcloud --region ap-chengdu # create a Google cloud GKE cluster and install KubeBlocks, the region is required - kbcli playground init --cloud-provider gcp --region us-central1`) + kbcli playground init --cloud-provider gcp --region us-east1 + + # after init, run the following commands to experience KubeBlocks quickly + # list database cluster and check its status + kbcli cluster list + + # get cluster information + kbcli cluster describe mycluster + + # connect to database + kbcli cluster connect mycluster + + # view the Grafana + kbcli dashboard open kubeblocks-grafana + + # destroy playground + kbcli playground destroy`) supportedCloudProviders = []string{cp.Local, cp.AWS, cp.GCP, cp.AliCloud, cp.TencentCloud} + + spinnerMsg = func(format string, a ...any) spinner.Option { + return spinner.WithMessage(fmt.Sprintf("%-50s", fmt.Sprintf(format, a...))) + } ) type initOptions struct { @@ -74,6 +103,7 @@ type initOptions struct { clusterVersion string cloudProvider string region string + autoApprove bool baseOptions } @@ -86,6 +116,7 @@ func newInitCmd(streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ Use: "init", Short: "Bootstrap a kubernetes cluster and install KubeBlocks for playground.", + Long: initLong, Example: initExample, Run: func(cmd *cobra.Command, args []string) { util.CheckErr(o.validate()) @@ -93,11 +124,13 @@ func newInitCmd(streams genericclioptions.IOStreams) *cobra.Command { }, } - cmd.Flags().StringVar(&o.clusterDef, "cluster-definition", defaultClusterDef, "Cluster definition") - cmd.Flags().StringVar(&o.clusterVersion, "cluster-version", "", "Cluster definition") + cmd.Flags().StringVar(&o.clusterDef, "cluster-definition", defaultClusterDef, "Specify the cluster definition, run \"kbcli cd list\" to get the available cluster definitions") + cmd.Flags().StringVar(&o.clusterVersion, "cluster-version", "", "Specify the cluster version, run \"kbcli cv list\" to get the available cluster versions") cmd.Flags().StringVar(&o.kbVersion, "version", version.DefaultKubeBlocksVersion, "KubeBlocks version") cmd.Flags().StringVar(&o.cloudProvider, "cloud-provider", defaultCloudProvider, fmt.Sprintf("Cloud provider type, one of %v", supportedCloudProviders)) cmd.Flags().StringVar(&o.region, "region", "", "The region to create kubernetes cluster") + cmd.Flags().DurationVar(&o.Timeout, "timeout", 300*time.Second, "Time to wait for init playground, such as --timeout=10m") + cmd.Flags().BoolVar(&o.autoApprove, "auto-approve", false, "Skip interactive approval during the initialization of playground") util.CheckErr(cmd.RegisterFlagCompletionFunc( "cloud-provider", @@ -157,12 +190,12 @@ func (o *initOptions) local() error { } // create a local kubernetes cluster (k3d cluster) to deploy KubeBlocks - spinner := printer.Spinner(o.Out, "%-50s", "Create k3d cluster: "+clusterInfo.ClusterName) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Create k3d cluster: "+clusterInfo.ClusterName)) + defer s.Fail() if err = provider.CreateK8sCluster(clusterInfo); err != nil { return errors.Wrap(err, "failed to set up k3d cluster") } - spinner(true) + s.Success() clusterInfo, err = o.writeStateFile(provider) if err != nil { @@ -251,14 +284,16 @@ func (o *initOptions) cloud() error { return o.installKBAndCluster(clusterInfo) } -// confirmToContinue confirms to continue init or not if there is an existed kubernetes cluster +// confirmToContinue confirms to continue init process if there is an existed kubernetes cluster func (o *initOptions) confirmToContinue() error { clusterName := o.prevCluster.ClusterName - printer.Warning(o.Out, "Found an existed cluster %s, do you want to continue to initialize this cluster?\n Only 'yes' will be accepted to confirm.\n\n", clusterName) - entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() - if entered != yesStr { - fmt.Fprintf(o.Out, "\nPlayground init cancelled, please destroy the old cluster first.\n") - return cmdutil.ErrExit + if !o.autoApprove { + printer.Warning(o.Out, "Found an existed cluster %s, do you want to continue to initialize this cluster?\n Only 'yes' will be accepted to confirm.\n\n", clusterName) + entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() + if entered != yesStr { + fmt.Fprintf(o.Out, "\nPlayground init cancelled, please destroy the old cluster first.\n") + return cmdutil.ErrExit + } } fmt.Fprintf(o.Out, "Continue to initialize %s %s cluster %s... \n", o.cloudProvider, cp.K8sService(o.cloudProvider), clusterName) @@ -267,15 +302,17 @@ func (o *initOptions) confirmToContinue() error { func (o *initOptions) confirmInitNewKubeCluster() error { printer.Warning(o.Out, `This action will create a kubernetes cluster on the cloud that may - incur charges. Be sure to delete your infrastructure promptly to avoid - additional charges. We are not responsible for any charges you may incur. + incur charges. Be sure to delete your infrastructure properly to avoid additional charges. `) fmt.Fprintf(o.Out, ` -The whole process wll take about %s, please wait patiently, +The whole process will take about %s, please wait patiently, if it takes a long time, please check the network environment and try again. `, printer.BoldRed("20 minutes")) + if o.autoApprove { + return nil + } // confirm to run fmt.Fprintf(o.Out, "\nDo you want to perform this action?\n Only 'yes' will be accepted to approve.\n\n") entered, _ := prompt.NewPrompt("Enter a value:", nil, o.In).Run() @@ -286,10 +323,6 @@ if it takes a long time, please check the network environment and try again. return nil } -func printGuide() { - fmt.Fprintf(os.Stdout, guideStr, kbClusterName) -} - // writeStateFile writes cluster info to state file and return the new cluster info with kubeconfig func (o *initOptions) writeStateFile(provider cp.Interface) (*cp.K8sClusterInfo, error) { clusterInfo, err := provider.GetClusterInfo() @@ -308,8 +341,8 @@ func (o *initOptions) writeStateFile(provider cp.Interface) (*cp.K8sClusterInfo, // merge created kubernetes cluster kubeconfig to ~/.kube/config and set it as default func (o *initOptions) setKubeConfig(info *cp.K8sClusterInfo) error { - spinner := printer.Spinner(o.Out, "%-50s", "Merge kubeconfig to "+defaultKubeConfigPath) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Merge kubeconfig to "+defaultKubeConfigPath)) + defer s.Fail() // check if the default kubeconfig file exists, if not, create it if _, err := os.Stat(defaultKubeConfigPath); os.IsNotExist(err) { @@ -325,15 +358,15 @@ func (o *initOptions) setKubeConfig(info *cp.K8sClusterInfo) error { writeKubeConfigOptions{UpdateExisting: true, UpdateCurrentContext: true}); err != nil { return errors.Wrapf(err, "failed to write cluster %s kubeconfig", info.ClusterName) } - spinner(true) + s.Success() currentContext, err := kubeConfigCurrentContext(info.KubeConfig) - spinner = printer.Spinner(o.Out, "%-50s", "Switch current context to "+currentContext) - defer spinner(false) + s = spinner.New(o.Out, spinnerMsg("Switch current context to "+currentContext)) + defer s.Fail() if err != nil { return err } - spinner(true) + s.Success() return nil } @@ -367,12 +400,12 @@ func (o *initOptions) installKBAndCluster(info *cp.K8sClusterInfo) error { if o.clusterVersion != "" { clusterInfo += ", ClusterVersion: " + o.clusterVersion } - spinner := printer.Spinner(o.Out, "Create cluster %s (%s)", kbClusterName, clusterInfo) - defer spinner(false) + s := spinner.New(o.Out, spinnerMsg("Create cluster %s (%s)", kbClusterName, clusterInfo)) + defer s.Fail() if err = o.createCluster(); err != nil && !apierrors.IsAlreadyExists(err) { return errors.Wrapf(err, "failed to create cluster %s", kbClusterName) } - spinner(true) + s.Success() fmt.Fprintf(os.Stdout, "\nKubeBlocks playground init SUCCESSFULLY!\n\n") fmt.Fprintf(os.Stdout, "Kubernetes cluster \"%s\" has been created.\n", info.ClusterName) @@ -383,7 +416,7 @@ func (o *initOptions) installKBAndCluster(info *cp.K8sClusterInfo) error { fmt.Fprintf(o.Out, "Elapsed time: %s\n", time.Since(o.startTime).Truncate(time.Second)) } - printGuide() + fmt.Fprintf(o.Out, guideStr, kbClusterName) return nil } @@ -404,6 +437,8 @@ func (o *initOptions) installKubeBlocks(k8sClusterName string) error { IOStreams: o.IOStreams, Client: client, Dynamic: dynamic, + Wait: true, + Timeout: o.Timeout, }, Version: o.kbVersion, Monitor: true, @@ -417,13 +452,9 @@ func (o *initOptions) installKubeBlocks(k8sClusterName string) error { "snapshot-controller.enabled=true", "csi-hostpath-driver.enabled=true", - // enable aws loadbalancer controller addon automatically on playground - "aws-loadbalancer-controller.enabled=true", - fmt.Sprintf("aws-loadbalancer-controller.clusterName=%s", k8sClusterName), - // disable the persistent volume of prometheus, if not, the prometheus - // will dependent the hostpath csi driver ready to create persistent - // volume, but the order of addon installation is not guaranteed that + // will depend on the hostpath csi driver to create persistent + // volume, but the order of addon installation is not guaranteed which // will cause the prometheus PVC pending forever. "prometheus.server.persistentVolume.enabled=false", "prometheus.server.statefulSet.enabled=false", @@ -431,42 +462,63 @@ func (o *initOptions) installKubeBlocks(k8sClusterName string) error { "prometheus.alertmanager.statefulSet.enabled=false") } else if o.cloudProvider == cp.AWS { insOpts.ValueOpts.Values = append(insOpts.ValueOpts.Values, - // enable aws loadbalancer controller addon automatically on playground - "aws-loadbalancer-controller.enabled=true", - fmt.Sprintf("aws-loadbalancer-controller.clusterName=%s", k8sClusterName), + // enable aws-load-balancer-controller addon automatically on playground + "aws-load-balancer-controller.enabled=true", + fmt.Sprintf("aws-load-balancer-controller.clusterName=%s", k8sClusterName), ) } + if err = insOpts.PreCheck(); err != nil { + return err + } return insOpts.Install() } -// createCluster construct a cluster create options and run +// createCluster constructs a cluster create options and run func (o *initOptions) createCluster() error { - // construct a cluster create options and run - options, err := o.newCreateOptions() - if err != nil { - return err + c := cmdcluster.NewCreateOptions(util.NewFactory(), genericclioptions.NewTestIOStreamsDiscard()) + c.ClusterDefRef = o.clusterDef + c.ClusterVersionRef = o.clusterVersion + c.Namespace = defaultNamespace + c.Name = kbClusterName + c.UpdatableFlags = cmdcluster.UpdatableFlags{ + TerminationPolicy: "WipeOut", + Monitor: true, + PodAntiAffinity: "Preferred", + Tenancy: "SharedNode", + } + + // if we are running on local, create cluster with one replica + if o.cloudProvider == cp.Local { + c.Values = append(c.Values, "replicas=1") + } else { + // if we are running on cloud, create cluster with three replicas + c.Values = append(c.Values, "replicas=3") } - inputs := create.Inputs{ - BaseOptionsObj: &options.BaseOptions, - Options: options, - CueTemplateName: cmdcluster.CueTemplateName, - ResourceName: types.ResourceClusters, + if err := c.CreateOptions.Complete(); err != nil { + return err } - - return options.Run(inputs) + if err := c.Validate(); err != nil { + return err + } + if err := c.Complete(); err != nil { + return err + } + return c.Run() } -// checkExistedCluster check playground kubernetes cluster exists or not, playground -// only supports one kubernetes cluster exists at the same time +// checkExistedCluster checks playground kubernetes cluster exists or not, a kbcli client only +// support a single playground, they are bound to each other with a hidden context config file, +// the hidden file ensures that when destroy the playground it always goes with the fixed context, +// it makes the dangerous operation more safe and prevents from manipulating another context func (o *initOptions) checkExistedCluster() error { if o.prevCluster == nil { return nil } - warningMsg := fmt.Sprintf("playground only supports one kubernetes cluster at the same time,\n one cluster already existed, please destroy it first.\n%s\n", o.prevCluster.String()) - // if cloud provider is not same with the exited cluster cloud provider, informer + warningMsg := fmt.Sprintf("playground only supports one kubernetes cluster,\n if a cluster is already existed, please destroy it first.\n%s\n", o.prevCluster.String()) + // if cloud provider is not same with the existed cluster cloud provider, suggest // user to destroy the previous cluster first if o.prevCluster.CloudProvider != o.cloudProvider { printer.Warning(o.Out, warningMsg) @@ -478,7 +530,7 @@ func (o *initOptions) checkExistedCluster() error { } // previous kubernetes cluster is a cloud provider cluster, check if the region - // is same with the new cluster region, if not, informer user to destroy the previous + // is same with the new cluster region, if not, suggest user to destroy the previous // cluster first if o.prevCluster.Region != o.region { printer.Warning(o.Out, warningMsg) @@ -486,39 +538,3 @@ func (o *initOptions) checkExistedCluster() error { } return nil } - -func (o *initOptions) newCreateOptions() (*cmdcluster.CreateOptions, error) { - dynamicClient, err := util.NewFactory().DynamicClient() - if err != nil { - return nil, err - } - options := &cmdcluster.CreateOptions{ - BaseOptions: create.BaseOptions{ - IOStreams: genericclioptions.NewTestIOStreamsDiscard(), - Namespace: defaultNamespace, - Name: kbClusterName, - Dynamic: dynamicClient, - }, - UpdatableFlags: cmdcluster.UpdatableFlags{ - TerminationPolicy: "WipeOut", - Monitor: true, - PodAntiAffinity: "Preferred", - Tenancy: "SharedNode", - }, - ClusterDefRef: o.clusterDef, - ClusterVersionRef: o.clusterVersion, - } - - // if we are running on cloud, create cluster with three replicas - if o.cloudProvider != cp.Local { - options.Values = append(options.Values, "replicas=3") - } - - if err = options.Validate(); err != nil { - return nil, err - } - if err = options.Complete(); err != nil { - return nil, err - } - return options, nil -} diff --git a/internal/cli/cmd/playground/init_test.go b/internal/cli/cmd/playground/init_test.go index f6ae12944..38945db93 100644 --- a/internal/cli/cmd/playground/init_test.go +++ b/internal/cli/cmd/playground/init_test.go @@ -1,22 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground import ( + "os" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -24,15 +29,20 @@ import ( cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" clitesting "github.com/apecloud/kubeblocks/internal/cli/testing" - "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/cli/util/helm" ) var _ = Describe("playground", func() { + const ( + testKubeConfigPath = "./testdata/kubeconfig" + ) + var streams genericclioptions.IOStreams BeforeEach(func() { streams, _, _, _ = genericclioptions.NewTestIOStreams() + Expect(os.Setenv(types.CliHomeEnv, "./testdata")).Should(Succeed()) }) It("init at local host", func() { @@ -44,7 +54,7 @@ var _ = Describe("playground", func() { clusterVersion: clitesting.ClusterVersionName, IOStreams: streams, cloudProvider: defaultCloudProvider, - helmCfg: helm.NewConfig("", util.ConfigPath("config_kb_test"), "", false), + helmCfg: helm.NewConfig("", testKubeConfigPath, "", false), } Expect(o.validate()).Should(Succeed()) Expect(o.run()).Should(HaveOccurred()) @@ -61,9 +71,4 @@ var _ = Describe("playground", func() { } Expect(o.validate()).Should(HaveOccurred()) }) - - It("guide", func() { - cmd := newGuideCmd() - Expect(cmd).ShouldNot(BeNil()) - }) }) diff --git a/internal/cli/cmd/playground/kubeconfig.go b/internal/cli/cmd/playground/kubeconfig.go index 91d67a1c4..8b69a07fa 100644 --- a/internal/cli/cmd/playground/kubeconfig.go +++ b/internal/cli/cmd/playground/kubeconfig.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -27,7 +30,7 @@ import ( "k8s.io/klog/v2" ) -// writeKubeConfigOptions provide a set of options for writing a KubeConfig file +// writeKubeConfigOptions provides a set of options for writing a KubeConfig file type writeKubeConfigOptions struct { UpdateExisting bool UpdateCurrentContext bool @@ -90,12 +93,12 @@ func kubeConfigWrite(kubeConfigStr string, output string, options writeKubeConfi return kubeConfigMerge(kubeConfig, existingKubeConfig, output, options) } -// kubeConfigGetDefaultPath returns the path of the default kubeconfig, but errors +// kubeConfigGetDefaultPath returns the path of the default kubeconfig, print errors // if the KUBECONFIG env var specifies more than one file func kubeConfigGetDefaultPath() (string, error) { defaultKubeConfigLoadingRules := clientcmd.NewDefaultClientConfigLoadingRules() if len(defaultKubeConfigLoadingRules.GetLoadingPrecedence()) > 1 { - return "", fmt.Errorf("multiple kubeconfigs specified via KUBECONFIG env var: Please reduce to one entry, unset KUBECONFIG or explicitly choose an output") + return "", fmt.Errorf("multiple kubeconfigs specified via KUBECONFIG env var: Please reduce to one entry, unset KUBECONFIG or explicitly choose one") } return defaultKubeConfigLoadingRules.GetDefaultFilename(), nil } diff --git a/internal/cli/cmd/playground/kubeconfig_test.go b/internal/cli/cmd/playground/kubeconfig_test.go index 0fd8ff4f3..b64a333ae 100644 --- a/internal/cli/cmd/playground/kubeconfig_test.go +++ b/internal/cli/cmd/playground/kubeconfig_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground diff --git a/internal/cli/cmd/playground/palyground.go b/internal/cli/cmd/playground/palyground.go index d44077b9c..29d1906b8 100644 --- a/internal/cli/cmd/playground/palyground.go +++ b/internal/cli/cmd/playground/palyground.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -24,15 +27,14 @@ import ( // NewPlaygroundCmd creates the playground command func NewPlaygroundCmd(streams genericclioptions.IOStreams) *cobra.Command { cmd := &cobra.Command{ - Use: "playground [init | destroy | guide]", - Short: "Bootstrap a playground KubeBlocks in local host or cloud.", + Use: "playground [init | destroy]", + Short: "Bootstrap or destroy a playground KubeBlocks in local host or cloud.", } // add subcommands cmd.AddCommand( newInitCmd(streams), newDestroyCmd(streams), - newGuideCmd(), ) return cmd diff --git a/internal/cli/cmd/playground/playground_test.go b/internal/cli/cmd/playground/playground_test.go index 21e3ce01c..021e57abe 100644 --- a/internal/cli/cmd/playground/playground_test.go +++ b/internal/cli/cmd/playground/playground_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground diff --git a/internal/cli/cmd/playground/suite_test.go b/internal/cli/cmd/playground/suite_test.go index 4486a9b60..8c85c87bc 100644 --- a/internal/cli/cmd/playground/suite_test.go +++ b/internal/cli/cmd/playground/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -35,7 +38,6 @@ func TestPlayground(t *testing.T) { var _ = BeforeSuite(func() { // set fake image info cp.K3sImage = "fake-k3s-image" - cp.K3dToolsImage = "fake-k3s-tools-image" cp.K3dProxyImage = "fake-k3d-proxy-image" // set default cluster name to test diff --git a/internal/cli/cmd/playground/testdata/playground/kb-playground.state b/internal/cli/cmd/playground/testdata/playground/kb-playground.state new file mode 100644 index 000000000..f4b3c4a27 --- /dev/null +++ b/internal/cli/cmd/playground/testdata/playground/kb-playground.state @@ -0,0 +1 @@ +{"cluster_name":"kb-playground-test","cloud_provider":"local"} diff --git a/internal/cli/cmd/playground/types.go b/internal/cli/cmd/playground/types.go index 90913fe24..4ec7c12ae 100644 --- a/internal/cli/cmd/playground/types.go +++ b/internal/cli/cmd/playground/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground diff --git a/internal/cli/cmd/playground/util.go b/internal/cli/cmd/playground/util.go index d60993e87..a48bd19bc 100644 --- a/internal/cli/cmd/playground/util.go +++ b/internal/cli/cmd/playground/util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground @@ -30,7 +33,7 @@ import ( "k8s.io/client-go/kubernetes" cp "github.com/apecloud/kubeblocks/internal/cli/cloudprovider" - "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/spinner" "github.com/apecloud/kubeblocks/internal/cli/util" "github.com/apecloud/kubeblocks/version" ) @@ -117,8 +120,8 @@ func readClusterInfoFromFile(path string) (*cp.K8sClusterInfo, error) { } func writeAndUseKubeConfig(kubeConfig string, kubeConfigPath string, out io.Writer) error { - spinner := printer.Spinner(out, fmt.Sprintf("%-50s", "Write kubeconfig to "+kubeConfigPath)) - defer spinner(false) + s := spinner.New(out, spinnerMsg("Write kubeconfig to "+kubeConfigPath)) + defer s.Fail() if err := kubeConfigWrite(kubeConfig, kubeConfigPath, writeKubeConfigOptions{ UpdateExisting: true, UpdateCurrentContext: true, @@ -131,7 +134,7 @@ func writeAndUseKubeConfig(kubeConfig string, kubeConfigPath string, out io.Writ return err } - spinner(true) + s.Success() return nil } diff --git a/internal/cli/cmd/playground/util_test.go b/internal/cli/cmd/playground/util_test.go index d3d59b9a2..fdbb49c07 100644 --- a/internal/cli/cmd/playground/util_test.go +++ b/internal/cli/cmd/playground/util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package playground diff --git a/internal/cli/cmd/plugin/describe.go b/internal/cli/cmd/plugin/describe.go new file mode 100644 index 000000000..0df572b3e --- /dev/null +++ b/internal/cli/cmd/plugin/describe.go @@ -0,0 +1,81 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "io" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" +) + +var pluginDescribeExample = templates.Examples(` + # Describe a plugin + kbcli plugin describe [PLUGIN] + + # Describe a plugin with index + kbcli plugin describe [INDEX/PLUGIN] + `) + +func NewPluginDescribeCmd(streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "describe", + Short: "Describe a plugin", + Example: pluginDescribeExample, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(printPluginInfo(streams.Out, args[0])) + }, + } + return cmd +} + +func printPluginInfo(out io.Writer, name string) error { + indexName, pluginName := CanonicalPluginName(name) + plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName) + if err != nil { + return err + } + + fmt.Fprintf(out, "NAME: %s\n", plugin.Name) + fmt.Fprintf(out, "INDEX: %s\n", indexName) + if platform, ok, err := GetMatchingPlatform(plugin.Spec.Platforms); err == nil && ok { + if platform.URI != "" { + fmt.Fprintf(out, "URI: %s\n", platform.URI) + fmt.Fprintf(out, "SHA256: %s\n", platform.Sha256) + } + } + if plugin.Spec.Version != "" { + fmt.Fprintf(out, "VERSION: %s\n", plugin.Spec.Version) + } + if plugin.Spec.Homepage != "" { + fmt.Fprintf(out, "HOMEPAGE: %s\n", plugin.Spec.Homepage) + } + if plugin.Spec.Description != "" { + fmt.Fprintf(out, "DESCRIPTION: \n%s\n", plugin.Spec.Description) + } + if plugin.Spec.Caveats != "" { + fmt.Fprintf(out, "CAVEATS:\n%s\n", indent(plugin.Spec.Caveats)) + } + return nil +} diff --git a/internal/cli/cmd/plugin/download/download.go b/internal/cli/cmd/plugin/download/download.go new file mode 100755 index 000000000..cf45356b6 --- /dev/null +++ b/internal/cli/cmd/plugin/download/download.go @@ -0,0 +1,247 @@ +// Copyright 2019 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. + +package download + +import ( + "archive/tar" + "archive/zip" + "bytes" + "compress/gzip" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "github.com/pkg/errors" + "k8s.io/klog/v2" +) + +// download gets a file from the internet in memory and writes it content +// to a Verifier. +func download(url string, verifier Verifier, fetcher Fetcher) (io.ReaderAt, int64, error) { + body, err := fetcher.Get(url) + if err != nil { + return nil, 0, errors.Wrapf(err, "failed to obtain plugin archive") + } + defer body.Close() + + klog.V(3).Infof("Reading archive file into memory") + data, err := io.ReadAll(io.TeeReader(body, verifier)) + if err != nil { + return nil, 0, errors.Wrap(err, "could not read archive") + } + klog.V(2).Infof("Read %d bytes from archive into memory", len(data)) + + return bytes.NewReader(data), int64(len(data)), verifier.Verify() +} + +// extractZIP extracts a zip file into the target directory. +func extractZIP(targetDir string, read io.ReaderAt, size int64) error { + klog.V(4).Infof("Extracting zip archive to %q", targetDir) + zipReader, err := zip.NewReader(read, size) + if err != nil { + return err + } + + for _, f := range zipReader.File { + if err := suspiciousPath(f.Name); err != nil { + return err + } + + path := filepath.Join(targetDir, filepath.FromSlash(f.Name)) + if f.FileInfo().IsDir() { + if err := os.MkdirAll(path, f.Mode()); err != nil { + return errors.Wrap(err, "can't create directory tree") + } + continue + } + + dir := filepath.Dir(path) + klog.V(4).Infof("zip: ensuring parent dirs exist for regular file, dir=%s", dir) + if err := os.MkdirAll(dir, 0o755); err != nil { + return errors.Wrap(err, "failed to create directory for zip entry") + } + src, err := f.Open() + if err != nil { + return errors.Wrap(err, "could not open inflating zip file") + } + + dst, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, f.Mode()) + if err != nil { + src.Close() + return errors.Wrap(err, "can't create file in zip destination dir") + } + closeAll := func() { + src.Close() + dst.Close() + } + + if _, err := io.Copy(dst, src); err != nil { + closeAll() + return errors.Wrap(err, "can't copy content to zip destination file") + } + closeAll() + } + + return nil +} + +// extractTARGZ extracts a gzipped tar file into the target directory. +func extractTARGZ(targetDir string, at io.ReaderAt, size int64) error { + klog.V(4).Infof("tar: extracting to %q", targetDir) + in := io.NewSectionReader(at, 0, size) + + gzr, err := gzip.NewReader(in) + if err != nil { + return errors.Wrap(err, "failed to create gzip reader") + } + defer gzr.Close() + + tr := tar.NewReader(gzr) + for { + hdr, err := tr.Next() + if err == io.EOF { + break + } + if err != nil { + return errors.Wrap(err, "tar extraction error") + } + klog.V(4).Infof("tar: processing %q (type=%d, mode=%s)", hdr.Name, hdr.Typeflag, os.FileMode(hdr.Mode)) + // see https://golang.org/cl/78355 for handling pax_global_header + if hdr.Name == "pax_global_header" { + klog.V(4).Infof("tar: skipping pax_global_header file") + continue + } + + if err := suspiciousPath(hdr.Name); err != nil { + return err + } + + path := filepath.Join(targetDir, filepath.FromSlash(hdr.Name)) + switch hdr.Typeflag { + case tar.TypeDir: + if err := os.MkdirAll(path, os.FileMode(hdr.Mode)); err != nil { + return errors.Wrap(err, "failed to create directory from tar") + } + case tar.TypeReg: + dir := filepath.Dir(path) + klog.V(4).Infof("tar: ensuring parent dirs exist for regular file, dir=%s", dir) + if err := os.MkdirAll(dir, 0o755); err != nil { + return errors.Wrap(err, "failed to create directory for tar") + } + f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY, os.FileMode(hdr.Mode)) + if err != nil { + return errors.Wrapf(err, "failed to create file %q", path) + } + + if _, err := io.Copy(f, tr); err != nil { + f.Close() + return errors.Wrapf(err, "failed to copy %q from tar into file", hdr.Name) + } + f.Close() + default: + return errors.Errorf("unable to handle file type %d for %q in tar", hdr.Typeflag, hdr.Name) + } + klog.V(4).Infof("tar: processed %q", hdr.Name) + } + klog.V(4).Infof("tar extraction to %s complete", targetDir) + return nil +} + +func suspiciousPath(path string) error { + if strings.Contains(path, "..") { + return errors.Errorf("refusing to unpack archive with suspicious entry %q", path) + } + + if strings.HasPrefix(path, `/`) || strings.HasPrefix(path, `\`) { + return errors.Errorf("refusing to unpack archive with absolute entry %q", path) + } + + return nil +} + +func detectMIMEType(at io.ReaderAt) (string, error) { + buf := make([]byte, 512) + n, err := at.ReadAt(buf, 0) + if err != nil && err != io.EOF { + return "", errors.Wrap(err, "failed to read first 512 bytes") + } + if n < 512 { + klog.V(5).Infof("Only read %d of 512 bytes to determine the file type", n) + } + + // Cut off mime extra info beginning with ';' i.e: + // "text/plain; charset=utf-8" should result in "text/plain". + return strings.Split(http.DetectContentType(buf[:n]), ";")[0], nil +} + +type extractor func(targetDir string, read io.ReaderAt, size int64) error + +var defaultExtractors = map[string]extractor{ + "application/zip": extractZIP, + "application/x-gzip": extractTARGZ, +} + +func extractArchive(dst string, at io.ReaderAt, size int64) error { + t, err := detectMIMEType(at) + if err != nil { + return errors.Wrap(err, "failed to determine content type") + } + klog.V(4).Infof("detected %q file type", t) + exf, ok := defaultExtractors[t] + if !ok { + return errors.Errorf("mime type %q for archive file is not a supported archive format", t) + } + return errors.Wrap(exf(dst, at, size), "failed to extract file") +} + +// Downloader is responsible for fetching, verifying and extracting a binary. +type Downloader struct { + verifier Verifier + fetcher Fetcher +} + +// NewDownloader builds a new Downloader. +func NewDownloader(v Verifier, f Fetcher) Downloader { + return Downloader{ + verifier: v, + fetcher: f, + } +} + +// Get pulls the uri and verifies it. On success, the download gets extracted +// into dst. +func (d Downloader) Get(uri, dst string) error { + body, size, err := download(uri, d.verifier, d.fetcher) + if err != nil { + return err + } + return extractArchive(dst, body, size) +} + +// DownloadAndExtract downloads the specified archive uri (or uses the provided overrideFile, if a non-empty value) +// while validating its checksum with the provided sha256sum, and extracts its contents to extractDir that must be. +// created. +func DownloadAndExtract(extractDir, uri, sha256sum, overrideFile string) error { + var fetcher Fetcher = HTTPFetcher{} + if overrideFile != "" { + fetcher = NewFileFetcher(overrideFile) + } + + verifier := NewSha256Verifier(sha256sum) + err := NewDownloader(verifier, fetcher).Get(uri, extractDir) + return errors.Wrap(err, "failed to unpack the plugin archive") +} diff --git a/internal/cli/cmd/plugin/download/fetch.go b/internal/cli/cmd/plugin/download/fetch.go new file mode 100755 index 000000000..10b20dbbe --- /dev/null +++ b/internal/cli/cmd/plugin/download/fetch.go @@ -0,0 +1,57 @@ +// Copyright 2019 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. + +package download + +import ( + "io" + "net/http" + "os" + + "github.com/pkg/errors" + "k8s.io/klog/v2" +) + +type Fetcher interface { + // Get gets the file and returns an stream to read the file. + Get(uri string) (io.ReadCloser, error) +} + +var _ Fetcher = HTTPFetcher{} + +// HTTPFetcher is used to get a file from a http:// or https:// schema path. +type HTTPFetcher struct{} + +// Get gets the file and returns a stream to read the file. +func (HTTPFetcher) Get(uri string) (io.ReadCloser, error) { + klog.V(2).Infof("Fetching %q", uri) + resp, err := http.Get(uri) + if err != nil { + return nil, errors.Wrapf(err, "failed to download %q", uri) + } + return resp.Body, nil +} + +var _ Fetcher = fileFetcher{} + +type fileFetcher struct{ f string } + +func (f fileFetcher) Get(_ string) (io.ReadCloser, error) { + klog.V(2).Infof("Reading %q", f.f) + file, err := os.Open(f.f) + return file, errors.Wrapf(err, "failed to open archive file %q for reading", f.f) +} + +// NewFileFetcher returns a local file reader. +func NewFileFetcher(path string) Fetcher { return fileFetcher{f: path} } diff --git a/internal/cli/cmd/plugin/download/verifier.go b/internal/cli/cmd/plugin/download/verifier.go new file mode 100755 index 000000000..2c2828b0e --- /dev/null +++ b/internal/cli/cmd/plugin/download/verifier.go @@ -0,0 +1,55 @@ +// Copyright 2019 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. + +package download + +import ( + "bytes" + "crypto/sha256" + "encoding/hex" + "hash" + "io" + + "github.com/pkg/errors" + "k8s.io/klog/v2" +) + +type Verifier interface { + io.Writer + Verify() error +} + +var _ Verifier = sha256Verifier{} + +type sha256Verifier struct { + hash.Hash + wantedHash []byte +} + +// NewSha256Verifier creates a Verifier that tests against the given hash. +func NewSha256Verifier(hashed string) Verifier { + raw, _ := hex.DecodeString(hashed) + return sha256Verifier{ + Hash: sha256.New(), + wantedHash: raw, + } +} + +func (v sha256Verifier) Verify() error { + klog.V(1).Infof("Compare sha256 (%s) signed version", hex.EncodeToString(v.wantedHash)) + if bytes.Equal(v.wantedHash, v.Sum(nil)) { + return nil + } + return errors.Errorf("checksum does not match, want: %x, got %x", v.wantedHash, v.Sum(nil)) +} diff --git a/internal/cli/cmd/plugin/index.go b/internal/cli/cmd/plugin/index.go new file mode 100644 index 000000000..8e8def42d --- /dev/null +++ b/internal/cli/cmd/plugin/index.go @@ -0,0 +1,258 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "io" + "os" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + pluginListIndexExample = templates.Examples(` + # List all configured plugin indexes + kbcli plugin index list + `) + + pluginAddIndexExample = templates.Examples(` + # Add a new plugin index + kbcli plugin index add myIndex + `) + + pluginDeleteIndexExample = templates.Examples(` + # Delete a plugin index + kbcli plugin index delete myIndex + `) +) + +func NewPluginIndexCmd(streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "index", + Short: "Manage custom plugin indexes", + Long: "Manage which repositories are used to discover plugins and install plugins from", + } + + cmd.AddCommand(NewPluginIndexListCmd(streams)) + cmd.AddCommand(NewPluginIndexAddCmd(streams)) + cmd.AddCommand(NewPluginIndexDeleteCmd(streams)) + cmd.AddCommand(NewPluginIndexUpdateCmd(streams)) + return cmd +} + +type PluginIndexOptions struct { + IndexName string + URL string + + genericclioptions.IOStreams +} + +func (o *PluginIndexOptions) ListIndex() error { + indexes, err := ListIndexes(paths) + if err != nil { + return errors.Wrap(err, "failed to list indexes") + } + + p := NewPluginIndexPrinter(o.IOStreams.Out) + for _, index := range indexes { + addPluginIndexRow(index.Name, index.URL, p) + } + p.Print() + + return nil +} + +func (o *PluginIndexOptions) AddIndex() error { + err := AddIndex(paths, o.IndexName, o.URL) + if err != nil { + return err + } + return nil +} + +func (o *PluginIndexOptions) DeleteIndex() error { + err := DeleteIndex(paths, o.IndexName) + if err != nil { + return err + } + return nil +} + +func (o *PluginIndexOptions) UpdateIndex() error { + indexes, err := ListIndexes(paths) + if err != nil { + return errors.Wrap(err, "failed to list indexes") + } + + for _, idx := range indexes { + indexPath := paths.IndexPath(idx.Name) + klog.V(1).Infof("Updating the local copy of plugin index (%s)", indexPath) + if err := util.EnsureUpdated(idx.URL, indexPath); err != nil { + klog.Warningf("failed to update index %q: %v", idx.Name, err) + continue + } + + fmt.Fprintf(o.Out, "Updated the local copy of plugin index %q\n", idx.Name) + } + + return nil +} + +func NewPluginIndexListCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginIndexOptions{ + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "list", + Short: "List configured indexes", + Example: pluginListIndexExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.ListIndex()) + }, + } + + return cmd +} + +func NewPluginIndexAddCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginIndexOptions{ + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "add", + Short: "Add a new index", + Example: pluginAddIndexExample, + Args: cobra.ExactArgs(2), + Run: func(cmd *cobra.Command, args []string) { + o.IndexName = args[0] + o.URL = args[1] + cmdutil.CheckErr(o.AddIndex()) + }, + } + + return cmd +} + +func NewPluginIndexDeleteCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginIndexOptions{ + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "delete", + Short: "Remove a configured index", + Example: pluginDeleteIndexExample, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + o.IndexName = args[0] + cmdutil.CheckErr(o.DeleteIndex()) + }, + } + + return cmd +} + +func NewPluginIndexUpdateCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginIndexOptions{ + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "update", + Short: "update all configured indexes", + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.UpdateIndex()) + }, + } + + return cmd +} + +func NewPluginIndexPrinter(out io.Writer) *printer.TablePrinter { + t := printer.NewTablePrinter(out) + t.SetHeader("INDEX", "URL") + return t +} + +func addPluginIndexRow(index, url string, p *printer.TablePrinter) { + p.AddRow(index, url) +} + +// ListIndexes returns a slice of Index objects. The path argument is used as +// the base path of the index. +func ListIndexes(paths *Paths) ([]Index, error) { + entries, err := os.ReadDir(paths.IndexBase()) + if err != nil { + return nil, err + } + + var indexes []Index + for _, e := range entries { + if !e.IsDir() { + continue + } + indexName := e.Name() + remote, err := util.GitGetRemoteURL(paths.IndexPath(indexName)) + if err != nil { + return nil, errors.Wrapf(err, "failed to list the remote URL for index %s", indexName) + } + + indexes = append(indexes, Index{ + Name: indexName, + URL: remote, + }) + } + return indexes, nil +} + +// AddIndex initializes a new index to install plugins from. +func AddIndex(paths *Paths, name, url string) error { + if name == "" { + return errors.New("index name must be specified") + } + dir := paths.IndexPath(name) + if _, err := os.Stat(dir); os.IsNotExist(err) { + return util.EnsureCloned(url, dir) + } else if err != nil { + return err + } + return fmt.Errorf("index %q already exists", name) +} + +// DeleteIndex removes specified index name. If index does not exist, returns an error that can be tested by os.IsNotExist. +func DeleteIndex(paths *Paths, name string) error { + dir := paths.IndexPath(name) + if _, err := os.Stat(dir); err != nil { + return err + } + + return os.RemoveAll(dir) +} diff --git a/internal/cli/cmd/plugin/install.go b/internal/cli/cmd/plugin/install.go new file mode 100755 index 000000000..2a7098cef --- /dev/null +++ b/internal/cli/cmd/plugin/install.go @@ -0,0 +1,209 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/cmd/plugin/download" +) + +var ( + pluginInstallExample = templates.Examples(` + # install a kbcli or kubectl plugin by name + kbcli plugin install [PLUGIN] + + # install a kbcli or kubectl plugin by name and index + kbcli plugin install [INDEX/PLUGIN] + `) +) + +type PluginInstallOption struct { + plugins []pluginEntry + + genericclioptions.IOStreams +} + +type pluginEntry struct { + index string + plugin Plugin +} + +func NewPluginInstallCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginInstallOption{ + IOStreams: streams, + } + cmd := &cobra.Command{ + Use: "install", + Short: "Install kbcli or kubectl plugins", + Example: pluginInstallExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(args)) + cmdutil.CheckErr(o.Install()) + }, + } + return cmd +} + +func (o *PluginInstallOption) Complete(names []string) error { + for _, name := range names { + indexName, pluginName := CanonicalPluginName(name) + + // check whether the plugin exists + if _, err := os.Stat(paths.PluginInstallReceiptPath(pluginName)); err == nil { + fmt.Fprintf(o.Out, "plugin %q is already installed\n", name) + continue + } + + plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName) + if err != nil { + if os.IsNotExist(err) { + return errors.Errorf("plugin %q does not exist in the %s plugin index", name, indexName) + } + return errors.Wrapf(err, "failed to load plugin %q from the %s plugin index", name, indexName) + } + o.plugins = append(o.plugins, pluginEntry{ + index: indexName, + plugin: plugin, + }) + } + return nil +} + +func (o *PluginInstallOption) Install() error { + var failed []string + var returnErr error + for _, entry := range o.plugins { + plugin := entry.plugin + fmt.Fprintf(o.Out, "Installing plugin: %s\n", plugin.Name) + err := Install(paths, plugin, entry.index, InstallOpts{}) + if err == ErrIsAlreadyInstalled { + continue + } + if err != nil { + klog.Warningf("failed to install plugin %q: %v", plugin.Name, err) + if returnErr == nil { + returnErr = err + } + failed = append(failed, plugin.Name) + continue + } + fmt.Fprintf(o.Out, "Installed plugin: %s\n", plugin.Name) + output := fmt.Sprintf("Use this plugin:\n\tkubectl %s\n", plugin.Name) + if plugin.Spec.Homepage != "" { + output += fmt.Sprintf("Documentation:\n\t%s\n", plugin.Spec.Homepage) + } + if plugin.Spec.Caveats != "" { + output += fmt.Sprintf("Caveats:\n%s\n", indent(plugin.Spec.Caveats)) + } + fmt.Fprintln(o.Out, indent(output)) + } + if len(failed) > 0 { + return errors.Wrapf(returnErr, "failed to install some plugins: %+v", failed) + } + return nil +} + +// Install downloads and installs a plugin. The operation tries +// to keep the plugin dir in a healthy state if it fails during the process. +func Install(p *Paths, plugin Plugin, indexName string, opts InstallOpts) error { + klog.V(2).Infof("Looking for installed versions") + _, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(plugin.Name)) + if err == nil { + return ErrIsAlreadyInstalled + } else if !os.IsNotExist(err) { + return errors.Wrap(err, "failed to look up plugin receipt") + } + + // Find available installation candidate + candidate, ok, err := GetMatchingPlatform(plugin.Spec.Platforms) + if err != nil { + return errors.Wrap(err, "failed trying to find a matching platform in plugin spec") + } + if !ok { + return errors.Errorf("plugin %q does not offer installation for this platform", plugin.Name) + } + + // The actual install should be the last action so that a failure during receipt + // saving does not result in an installed plugin without receipt. + klog.V(3).Infof("Install plugin %s at version=%s", plugin.Name, plugin.Spec.Version) + if err := install(installOperation{ + pluginName: plugin.Name, + platform: candidate, + + binDir: p.BinPath(), + installDir: p.PluginVersionInstallPath(plugin.Name, plugin.Spec.Version), + }, opts); err != nil { + return errors.Wrap(err, "install failed") + } + + klog.V(3).Infof("Storing install receipt for plugin %s", plugin.Name) + err = StoreReceipt(NewReceipt(plugin, indexName, metav1.Now()), p.PluginInstallReceiptPath(plugin.Name)) + return errors.Wrap(err, "installation receipt could not be stored, uninstall may fail") +} + +func install(op installOperation, opts InstallOpts) error { + // Download and extract + klog.V(3).Infof("Creating download staging directory") + downloadStagingDir, err := os.MkdirTemp("", "kbcli-downloads") + if err != nil { + return errors.Wrapf(err, "could not create staging dir %q", downloadStagingDir) + } + klog.V(3).Infof("Successfully created download staging directory %q", downloadStagingDir) + defer func() { + klog.V(3).Infof("Deleting the download staging directory %s", downloadStagingDir) + if err := os.RemoveAll(downloadStagingDir); err != nil { + klog.Warningf("failed to clean up download staging directory: %s", err) + } + }() + if err := download.DownloadAndExtract(downloadStagingDir, op.platform.URI, op.platform.Sha256, opts.ArchiveFileOverride); err != nil { + return errors.Wrap(err, "failed to unpack into staging dir") + } + + applyDefaults(&op.platform) + if err := moveToInstallDir(downloadStagingDir, op.installDir, op.platform.Files); err != nil { + return errors.Wrap(err, "failed while moving files to the installation directory") + } + + subPathAbs, err := filepath.Abs(op.installDir) + if err != nil { + return errors.Wrapf(err, "failed to get the absolute fullPath of %q", op.installDir) + } + fullPath := filepath.Join(op.installDir, filepath.FromSlash(op.platform.Bin)) + pathAbs, err := filepath.Abs(fullPath) + if err != nil { + return errors.Wrapf(err, "failed to get the absolute fullPath of %q", fullPath) + } + if _, ok := IsSubPath(subPathAbs, pathAbs); !ok { + return errors.Wrapf(err, "the fullPath %q does not extend the sub-fullPath %q", fullPath, op.installDir) + } + err = createOrUpdateLink(op.binDir, fullPath, op.pluginName) + return errors.Wrap(err, "failed to link installed plugin") +} diff --git a/internal/cli/cmd/plugin/pathutil.go b/internal/cli/cmd/plugin/pathutil.go new file mode 100644 index 000000000..b2e170fb5 --- /dev/null +++ b/internal/cli/cmd/plugin/pathutil.go @@ -0,0 +1,346 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "io" + "os" + "path/filepath" + "strings" + "syscall" + + "github.com/pkg/errors" + "k8s.io/klog/v2" + + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type move struct { + from, to string +} + +func findMoveTargets(fromDir, toDir string, fo FileOperation) ([]move, error) { + if fo.To != filepath.Clean(fo.To) { + return nil, errors.Errorf("the provided path is not clean, %q should be %q", fo.To, filepath.Clean(fo.To)) + } + fromDir, err := filepath.Abs(fromDir) + if err != nil { + return nil, errors.Wrap(err, "could not get the relative path for the move src") + } + + klog.V(4).Infof("Trying to move single file directly from=%q to=%q with file operation=%#v", fromDir, toDir, fo) + if m, ok, err := getDirectMove(fromDir, toDir, fo); err != nil { + return nil, errors.Wrap(err, "failed to detect single move operation") + } else if ok { + klog.V(3).Infof("Detected single move from file operation=%#v", fo) + return []move{m}, nil + } + + klog.V(4).Infoln("Wasn't a single file, proceeding with Glob move") + newDir, err := filepath.Abs(filepath.Join(filepath.FromSlash(toDir), filepath.FromSlash(fo.To))) + if err != nil { + return nil, errors.Wrap(err, "could not get the relative path for the move dst") + } + + gl, err := filepath.Glob(filepath.Join(filepath.FromSlash(fromDir), filepath.FromSlash(fo.From))) + if err != nil { + return nil, errors.Wrap(err, "could not get files using a glob string") + } + if len(gl) == 0 { + return nil, errors.Errorf("no files in the plugin archive matched the glob pattern=%s", fo.From) + } + + moves := make([]move, 0, len(gl)) + for _, v := range gl { + newPath := filepath.Join(newDir, filepath.Base(filepath.FromSlash(v))) + // Check secure path + m := move{from: v, to: newPath} + if !isMoveAllowed(fromDir, toDir, m) { + return nil, errors.Errorf("can't move, move target %v is not a subpath from=%q, to=%q", m, fromDir, toDir) + } + moves = append(moves, m) + } + return moves, nil +} + +func getDirectMove(fromDir, toDir string, fo FileOperation) (move, bool, error) { + var m move + fromDir, err := filepath.Abs(fromDir) + if err != nil { + return m, false, errors.Wrap(err, "could not get the relative path for the move src") + } + + toDir, err = filepath.Abs(toDir) + if err != nil { + return m, false, errors.Wrap(err, "could not get the relative path for the move dst") + } + + // Check file (not a Glob) + fromFilePath := filepath.Clean(filepath.Join(fromDir, fo.From)) + _, err = os.Stat(fromFilePath) + if err != nil { + return m, false, nil + } + + // If target is empty use old file name. + if filepath.Clean(fo.To) == "." { + fo.To = filepath.Base(fromFilePath) + } + + // Build new file name + toFilePath, err := filepath.Abs(filepath.Join(filepath.FromSlash(toDir), filepath.FromSlash(fo.To))) + if err != nil { + return m, false, errors.Wrap(err, "could not get the relative path for the move dst") + } + + // Check sane path + m = move{from: fromFilePath, to: toFilePath} + if !isMoveAllowed(fromDir, toDir, m) { + return move{}, false, errors.Errorf("can't move, move target %v is out of bounds from=%q, to=%q", m, fromDir, toDir) + } + + return m, true, nil +} + +func isMoveAllowed(fromBase, toBase string, m move) bool { + _, okFrom := IsSubPath(fromBase, m.from) + _, okTo := IsSubPath(toBase, m.to) + return okFrom && okTo +} + +func moveFiles(fromDir, toDir string, fo FileOperation) error { + klog.V(4).Infof("Finding move targets from %q to %q with file operation=%#v", fromDir, toDir, fo) + moves, err := findMoveTargets(fromDir, toDir, fo) + if err != nil { + return errors.Wrap(err, "could not find move targets") + } + + for _, m := range moves { + klog.V(2).Infof("Move file from %q to %q", m.from, m.to) + if err := os.MkdirAll(filepath.Dir(m.to), 0o755); err != nil { + return errors.Wrapf(err, "failed to create move path %q", filepath.Dir(m.to)) + } + + if err = renameOrCopy(m.from, m.to); err != nil { + return errors.Wrapf(err, "could not rename/copy file from %q to %q", m.from, m.to) + } + } + klog.V(4).Infoln("Move operations completed") + return nil +} + +func moveAllFiles(fromDir, toDir string, fos []FileOperation) error { + for _, fo := range fos { + if err := moveFiles(fromDir, toDir, fo); err != nil { + return errors.Wrap(err, "failed moving files") + } + } + return nil +} + +// moveToInstallDir moves plugins from srcDir to dstDir (created in this method) with given FileOperation. +func moveToInstallDir(srcDir, installDir string, fos []FileOperation) error { + installationDir := filepath.Dir(installDir) + klog.V(4).Infof("Creating directory %q", installationDir) + if err := os.MkdirAll(installationDir, 0o755); err != nil { + return errors.Wrapf(err, "error creating directory at %q", installationDir) + } + + tmp, err := os.MkdirTemp("", "kbcli-temp-move") + klog.V(4).Infof("Creating temp plugin move operations dir %q", tmp) + if err != nil { + return errors.Wrap(err, "failed to find a temporary director") + } + defer os.RemoveAll(tmp) + + if err = moveAllFiles(srcDir, tmp, fos); err != nil { + return errors.Wrap(err, "failed to move files") + } + + klog.V(2).Infof("Move directory %q to %q", tmp, installDir) + if err = renameOrCopy(tmp, installDir); err != nil { + defer func() { + klog.V(3).Info("Cleaning up installation directory due to error during copying files") + os.Remove(installDir) + }() + return errors.Wrapf(err, "could not rename/copy directory %q to %q", tmp, installDir) + } + return nil +} + +// renameOrCopy tries to rename a dir or file. If rename is not supported, a manual copy will be performed. +// Existing files at "to" will be deleted. +func renameOrCopy(from, to string) error { + // Try atomic rename (does not work cross partition). + fi, err := os.Stat(to) + if err != nil && !os.IsNotExist(err) { + return errors.Wrapf(err, "error checking move target dir %q", to) + } + if fi != nil && fi.IsDir() { + klog.V(4).Infof("There's already a directory at move target %q. deleting.", to) + if err := os.RemoveAll(to); err != nil { + return errors.Wrapf(err, "error cleaning up dir %q", to) + } + klog.V(4).Infof("Move target directory %q cleaned up", to) + } + + err = os.Rename(from, to) + // Fallback for invalid cross-device link (errno:18). + if isCrossDeviceRenameErr(err) { + klog.V(2).Infof("Cross-device link error while copying, fallback to manual copy") + return errors.Wrap(copyTree(from, to), "failed to copy directory tree as a fallback") + } + return err +} + +// copyTree copies files or directories, recursively. +func copyTree(from, to string) (err error) { + return filepath.Walk(from, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + newPath, _ := ReplaceBase(path, from, to) + if info.IsDir() { + klog.V(4).Infof("Creating new dir %q", newPath) + err = os.MkdirAll(newPath, info.Mode()) + } else { + klog.V(4).Infof("Copying file %q", newPath) + err = copyFile(path, newPath, info.Mode()) + } + return err + }) +} + +func copyFile(source, dst string, mode os.FileMode) (err error) { + sf, err := os.Open(source) + if err != nil { + return err + } + defer sf.Close() + + df, err := os.Create(dst) + if err != nil { + return err + } + defer df.Close() + + _, err = io.Copy(df, sf) + if err != nil { + return err + } + return os.Chmod(dst, mode) +} + +// isCrossDeviceRenameErr determines if an os.Rename error is due to cross-fs/drive/volume copying. +func isCrossDeviceRenameErr(err error) bool { + le, ok := err.(*os.LinkError) + if !ok { + return false + } + errno, ok := le.Err.(syscall.Errno) + if !ok { + return false + } + return (util.IsWindows() && errno == 17) || // syscall.ERROR_NOT_SAME_DEVICE + (!util.IsWindows() && errno == 18) // syscall.EXDEV +} + +// IsSubPath checks if the extending path is an extension of the basePath, it will return the extending path +// elements. Both paths have to be absolute or have the same root directory. The remaining path elements +func IsSubPath(basePath, subPath string) (string, bool) { + extendingPath, err := filepath.Rel(basePath, subPath) + if err != nil { + return "", false + } + if strings.HasPrefix(extendingPath, "..") { + return "", false + } + return extendingPath, true +} + +// ReplaceBase returns a replacement path with replacement as a base of the path instead of the old base. a/b/c, a, d -> d/b/c +func ReplaceBase(path, old, replacement string) (string, error) { + extendingPath, ok := IsSubPath(old, path) + if !ok { + return "", errors.Errorf("can't replace %q in %q, it is not a subpath", old, path) + } + return filepath.Join(replacement, extendingPath), nil +} + +// CanonicalPluginName resolves a plugin's index and name from input string. +// If an index is not specified, the default index name is assumed. +func CanonicalPluginName(in string) (string, string) { + if strings.Count(in, "/") == 0 { + return DefaultIndexName, in + } + p := strings.SplitN(in, "/", 2) + return p[0], p[1] +} + +func createOrUpdateLink(binDir, binary, plugin string) error { + dst := filepath.Join(binDir, pluginNameToBin(plugin, util.IsWindows())) + + if err := removeLink(dst); err != nil { + return errors.Wrap(err, "failed to remove old symlink") + } + if _, err := os.Stat(binary); os.IsNotExist(err) { + return errors.Wrapf(err, "can't create symbolic link, source binary (%q) cannot be found in extracted archive", binary) + } + + // Create new + klog.V(2).Infof("Creating symlink to %q at %q", binary, dst) + if err := os.Symlink(binary, dst); err != nil { + return errors.Wrapf(err, "failed to create a symlink from %q to %q", binary, dst) + } + klog.V(2).Infof("Created symlink at %q", dst) + + return nil +} + +// removeLink removes a symlink reference if exists. +func removeLink(path string) error { + fi, err := os.Lstat(path) + if os.IsNotExist(err) { + klog.V(3).Infof("No file found at %q", path) + return nil + } else if err != nil { + return errors.Wrapf(err, "failed to read the symlink in %q", path) + } + + if fi.Mode()&os.ModeSymlink == 0 { + return errors.Errorf("file %q is not a symlink (mode=%s)", path, fi.Mode()) + } + if err := os.Remove(path); err != nil { + return errors.Wrapf(err, "failed to remove the symlink in %q", path) + } + klog.V(3).Infof("Removed symlink from %q", path) + return nil +} + +// pluginNameToBin creates the name of the symlink file for the plugin name. +// It converts dashes to underscores. +func pluginNameToBin(name string, isWindows bool) string { + name = strings.ReplaceAll(name, "-", "_") + name = "kbcli-" + name + if isWindows { + name += ".exe" + } + return name +} diff --git a/internal/cli/cmd/plugin/platform.go b/internal/cli/cmd/plugin/platform.go new file mode 100755 index 000000000..b26ff608b --- /dev/null +++ b/internal/cli/cmd/plugin/platform.go @@ -0,0 +1,86 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "os" + "runtime" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/labels" + "k8s.io/klog/v2" +) + +// GetMatchingPlatform finds the platform spec in the specified plugin that +// matches the os/arch of the current machine (can be overridden via KREW_OS +// and/or KREW_ARCH). +func GetMatchingPlatform(platforms []Platform) (Platform, bool, error) { + return matchPlatform(platforms, OSArch()) +} + +// matchPlatform returns the first matching platform to given os/arch. +func matchPlatform(platforms []Platform, env OSArchPair) (Platform, bool, error) { + envLabels := labels.Set{ + "os": env.OS, + "arch": env.Arch, + } + klog.V(2).Infof("Matching platform for labels(%v)", envLabels) + + for i, platform := range platforms { + sel, err := metav1.LabelSelectorAsSelector(platform.Selector) + if err != nil { + return Platform{}, false, errors.Wrap(err, "failed to compile label selector") + } + if sel.Matches(envLabels) { + klog.V(2).Infof("Found matching platform with index (%d)", i) + return platform, true, nil + } + } + return Platform{}, false, nil +} + +// OSArchPair is wrapper around operating system and architecture +type OSArchPair struct { + OS, Arch string +} + +// String converts environment into a string +func (p OSArchPair) String() string { + return fmt.Sprintf("%s/%s", p.OS, p.Arch) +} + +// OSArch returns the OS/arch combination to be used on the current system. It +// can be overridden by setting KREW_OS and/or KREW_ARCH environment variables. +func OSArch() OSArchPair { + return OSArchPair{ + OS: getEnvOrDefault("KBLCI_OS", runtime.GOOS), + Arch: getEnvOrDefault("KBCLI_ARCH", runtime.GOARCH), + } +} + +func getEnvOrDefault(env, absent string) string { + v := os.Getenv(env) + if v != "" { + return v + } + return absent +} diff --git a/internal/cli/cmd/plugin/plugin.go b/internal/cli/cmd/plugin/plugin.go new file mode 100644 index 000000000..2e822cbd0 --- /dev/null +++ b/internal/cli/cmd/plugin/plugin.go @@ -0,0 +1,316 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + pluginLong = templates.LongDesc(` + Provides utilities for interacting with plugins. + + Plugins provide extended functionality that is not part of the major command-line distribution. + `) + + pluginListExample = templates.Examples(` + # List all available plugins file on a user's PATH. + kbcli plugin list + `) + + ValidPluginFilenamePrefixes = []string{"kbcli", "kubectl"} + paths = GetKbcliPluginPath() +) + +func NewPluginCmd(streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "plugin", + Short: "Provides utilities for interacting with plugins.", + Long: pluginLong, + PersistentPreRun: func(cmd *cobra.Command, args []string) { + InitPlugin() + }, + } + + cmd.AddCommand( + NewPluginListCmd(streams), + NewPluginIndexCmd(streams), + NewPluginInstallCmd(streams), + NewPluginUninstallCmd(streams), + NewPluginSearchCmd(streams), + NewPluginDescribeCmd(streams), + NewPluginUpgradeCmd(streams), + ) + return cmd +} + +type PluginListOptions struct { + Verifier PathVerifier + + PluginPaths []string + + genericclioptions.IOStreams +} + +func NewPluginListCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &PluginListOptions{ + IOStreams: streams, + } + cmd := &cobra.Command{ + Use: "list", + DisableFlagsInUseLine: true, + Short: "List all visible plugin executables on a user's PATH", + Example: pluginListExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(cmd)) + cmdutil.CheckErr(o.Run()) + }, + } + return cmd +} + +func (o *PluginListOptions) Complete(cmd *cobra.Command) error { + o.Verifier = &CommandOverrideVerifier{ + root: cmd.Root(), + seenPlugins: map[string]string{}, + } + + o.PluginPaths = filepath.SplitList(os.Getenv("PATH")) + return nil +} + +func (o *PluginListOptions) Run() error { + plugins, pluginErrors := o.ListPlugins() + + if len(plugins) == 0 { + pluginErrors = append(pluginErrors, fmt.Errorf("error: unable to find any kbcli or kubectl plugins in your PATH")) + } + + pluginWarnings := 0 + p := NewPluginPrinter(o.IOStreams.Out) + errMsg := "" + for _, pluginPath := range plugins { + name := filepath.Base(pluginPath) + path := filepath.Dir(pluginPath) + if errs := o.Verifier.Verify(pluginPath); len(errs) != 0 { + for _, err := range errs { + errMsg += fmt.Sprintf("%s\n", err) + pluginWarnings++ + } + } + addPluginRow(name, path, p) + } + p.Print() + klog.V(1).Info(errMsg) + + if pluginWarnings > 0 { + if pluginWarnings == 1 { + pluginErrors = append(pluginErrors, fmt.Errorf("error: one plugin warining was found")) + } else { + pluginErrors = append(pluginErrors, fmt.Errorf("error: %d plugin warnings were found", pluginWarnings)) + } + } + if len(pluginErrors) > 0 { + errs := bytes.NewBuffer(nil) + for _, e := range pluginErrors { + fmt.Fprintln(errs, e) + } + return fmt.Errorf("%s", errs.String()) + } + + return nil +} + +func (o *PluginListOptions) ListPlugins() ([]string, []error) { + var plugins []string + var errors []error + + for _, dir := range uniquePathsList(o.PluginPaths) { + if len(strings.TrimSpace(dir)) == 0 { + continue + } + + files, err := os.ReadDir(dir) + if err != nil { + if _, ok := err.(*os.PathError); ok { + klog.V(1).Info("Unable to read directory %q from your PATH: %v. Skipping...\n", dir, err) + continue + } + + errors = append(errors, fmt.Errorf("error: unable to read directory %q in your PATH: %v", dir, err)) + continue + } + + for _, f := range files { + if f.IsDir() { + continue + } + if !hasValidPrefix(f.Name(), ValidPluginFilenamePrefixes) { + continue + } + + plugins = append(plugins, filepath.Join(dir, f.Name())) + } + } + + return plugins, errors +} + +// PathVerifier receives a path and validates it. +type PathVerifier interface { + Verify(path string) []error +} + +type CommandOverrideVerifier struct { + root *cobra.Command + seenPlugins map[string]string +} + +// Verify implements PathVerifier and determines if a given path +// is valid depending on whether it overwrites an existing +// kbcli command path, or a previously seen plugin. +func (v *CommandOverrideVerifier) Verify(path string) []error { + if v.root == nil { + return []error{fmt.Errorf("unable to verify path with nil root")} + } + + // extract the plugin binary name + binName := filepath.Base(path) + + cmdPath := strings.Split(binName, "-") + if len(cmdPath) > 1 { + // the first argument is always "kbcli" or "kubectl" for a plugin binary + cmdPath = cmdPath[1:] + } + + var errors []error + if isExec, err := isExecutable(path); err == nil && !isExec { + errors = append(errors, fmt.Errorf("warning: %q identified as a kbcli or kubectl plugin, but it is not executable", path)) + } else if err != nil { + errors = append(errors, fmt.Errorf("error: unable to indentify %s as an executable file: %v", path, err)) + } + + if existingPath, ok := v.seenPlugins[binName]; ok { + errors = append(errors, fmt.Errorf("warning: %s is overshadowed by a similarly named plugin: %s", path, existingPath)) + } else { + v.seenPlugins[binName] = path + } + + if cmd, _, err := v.root.Find(cmdPath); err == nil { + errors = append(errors, fmt.Errorf("warning: %q overwrites existing kbcli command: %q", path, cmd.CommandPath())) + } + + return errors +} + +func isExecutable(fullPath string) (bool, error) { + info, err := os.Stat(fullPath) + if err != nil { + return false, err + } + + if util.IsWindows() { + fileExt := strings.ToLower(filepath.Ext(fullPath)) + + switch fileExt { + case ".bat", ".cmd", ".com", ".exe", ".ps1": + return true, nil + } + return false, nil + } + + if m := info.Mode(); !m.IsDir() && m&0111 != 0 { + return true, nil + } + + return false, nil +} + +func uniquePathsList(paths []string) []string { + var newPaths []string + seen := map[string]bool{} + + for _, path := range paths { + if !seen[path] { + newPaths = append(newPaths, path) + seen[path] = true + } + } + return newPaths +} + +func hasValidPrefix(filepath string, validPrefixes []string) bool { + for _, prefix := range validPrefixes { + if strings.HasPrefix(filepath, prefix+"-") { + return true + } + } + return false +} + +func NewPluginPrinter(out io.Writer) *printer.TablePrinter { + t := printer.NewTablePrinter(out) + t.SetHeader("NAME", "PATH") + return t +} + +func addPluginRow(name, path string, p *printer.TablePrinter) { + p.AddRow(name, path) +} + +func InitPlugin() { + // Ensure that the base directories exist + if err := EnsureDirs(paths.BasePath(), + paths.BinPath(), + paths.InstallPath(), + paths.IndexBase(), + paths.InstallReceiptsPath()); err != nil { + klog.Fatal(err) + } + + // check if index exists, if indexes don't exist, download default index + indexes, err := ListIndexes(paths) + if err != nil { + klog.Fatal(err) + } + if len(indexes) == 0 { + klog.V(1).Info("no index found, downloading default index") + if err := AddIndex(paths, DefaultIndexName, DefaultIndexURI); err != nil { + klog.Fatal("failed to download default index", err) + } + if err := AddIndex(paths, KrewIndexName, KrewIndexURI); err != nil { + klog.Fatal("failed to download krew index", err) + } + } +} diff --git a/internal/cli/cmd/plugin/plugin_test.go b/internal/cli/cmd/plugin/plugin_test.go new file mode 100644 index 000000000..b93448e28 --- /dev/null +++ b/internal/cli/cmd/plugin/plugin_test.go @@ -0,0 +1,242 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "os" + "path/filepath" + "reflect" + "strings" + "testing" + + "k8s.io/cli-runtime/pkg/genericclioptions" +) + +func TestPluginPathsAreUnaltered(t *testing.T) { + tempDir1, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins1") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + tempDir2, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins2") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // cleanup + defer func() { + if err := os.RemoveAll(tempDir1); err != nil { + panic(fmt.Errorf("unexpected cleanup error: %v", err)) + } + if err := os.RemoveAll(tempDir2); err != nil { + panic(fmt.Errorf("unexpected cleanup error: %v", err)) + } + }() + + ioStreams, _, _, errOut := genericclioptions.NewTestIOStreams() + verifier := newFakePluginPathVerifier() + pluginPaths := []string{tempDir1, tempDir2} + o := &PluginListOptions{ + Verifier: verifier, + IOStreams: ioStreams, + + PluginPaths: pluginPaths, + } + + // write at least one valid plugin file + if _, err := os.CreateTemp(tempDir1, "kbcli-"); err != nil { + t.Fatalf("unexpected error %v", err) + } + if _, err := os.CreateTemp(tempDir2, "kubectl-"); err != nil { + t.Fatalf("unexpected error %v", err) + } + + if err := o.Run(); err != nil { + t.Fatalf("unexpected error %v - %v", err, errOut.String()) + } + + // ensure original paths remain unaltered + if len(verifier.seenUnsorted) != len(pluginPaths) { + t.Fatalf("saw unexpected plugin paths. Expecting %v, got %v", pluginPaths, verifier.seenUnsorted) + } + for actual := range verifier.seenUnsorted { + if !strings.HasPrefix(verifier.seenUnsorted[actual], pluginPaths[actual]) { + t.Fatalf("expected PATH slice to be unaltered. Expecting %v, but got %v", pluginPaths[actual], verifier.seenUnsorted[actual]) + } + } +} + +func TestPluginPathsAreValid(t *testing.T) { + tempDir, err := os.MkdirTemp(os.TempDir(), "test-cmd-plugins") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + // cleanup + defer func() { + if err := os.RemoveAll(tempDir); err != nil { + panic(fmt.Errorf("unexpected cleanup error: %v", err)) + } + }() + + tc := []struct { + name string + pluginPaths []string + pluginFile func() (*os.File, error) + verifier *fakePluginPathVerifier + expectVerifyErrors []error + expectErr string + expectErrOut string + expectOut string + }{ + { + name: "ensure no plugins found if no files begin with kubectl- prefix", + pluginPaths: []string{tempDir}, + verifier: newFakePluginPathVerifier(), + pluginFile: func() (*os.File, error) { + return os.CreateTemp(tempDir, "notkbcli-") + }, + expectErr: "error: unable to find any kbcli or kubectl plugins in your PATH\n", + expectOut: "NAME", + }, + { + name: "ensure de-duplicated plugin-paths slice", + pluginPaths: []string{tempDir, tempDir}, + verifier: newFakePluginPathVerifier(), + pluginFile: func() (*os.File, error) { + return os.CreateTemp(tempDir, "kbcli-") + }, + expectOut: "NAME", + }, + { + name: "ensure no errors when empty string or blank path are specified", + pluginPaths: []string{tempDir, "", " "}, + verifier: newFakePluginPathVerifier(), + pluginFile: func() (*os.File, error) { + return os.CreateTemp(tempDir, "kbcli-") + }, + expectOut: "NAME", + }, + } + + for _, test := range tc { + t.Run(test.name, func(t *testing.T) { + ioStreams, _, out, errOut := genericclioptions.NewTestIOStreams() + o := &PluginListOptions{ + Verifier: test.verifier, + IOStreams: ioStreams, + + PluginPaths: test.pluginPaths, + } + + // create files + if test.pluginFile != nil { + if _, err := test.pluginFile(); err != nil { + t.Fatalf("unexpected error creating plugin file: %v", err) + } + } + + for _, expected := range test.expectVerifyErrors { + for _, actual := range test.verifier.errors { + if expected != actual { + t.Fatalf("unexpected error: expected %v, but got %v", expected, actual) + } + } + } + + err := o.Run() + switch { + case err == nil && len(test.expectErr) > 0: + t.Fatalf("unexpected non-error: expected %v, but got nothing", test.expectErr) + case err != nil && len(test.expectErr) == 0: + t.Fatalf("unexpected error: expected nothing, but got %v", err.Error()) + case err != nil && err.Error() != test.expectErr: + t.Fatalf("unexpected error: expected %v, but got %v", test.expectErr, err.Error()) + } + + if len(test.expectErrOut) == 0 && errOut.Len() > 0 { + t.Fatalf("unexpected error output: expected nothing, but got %v", errOut.String()) + } else if len(test.expectErrOut) > 0 && !strings.Contains(errOut.String(), test.expectErrOut) { + t.Fatalf("unexpected error output: expected to contain %v, but got %v", test.expectErrOut, errOut.String()) + } + + if len(test.expectOut) > 0 && !strings.Contains(out.String(), test.expectOut) { + t.Fatalf("unexpected output: expected to contain %v, but got %v", test.expectOut, out.String()) + } + }) + } +} + +func TestListPlugins(t *testing.T) { + pluginPath, _ := filepath.Abs("./testdata") + expectPlugins := []string{ + filepath.Join(pluginPath, "kbcli-foo"), + filepath.Join(pluginPath, "kbcli-version"), + filepath.Join(pluginPath, "kubectl-foo"), + filepath.Join(pluginPath, "kubectl-version"), + } + + verifier := newFakePluginPathVerifier() + ioStreams, _, _, _ := genericclioptions.NewTestIOStreams() + pluginPaths := []string{pluginPath} + + o := &PluginListOptions{ + Verifier: verifier, + IOStreams: ioStreams, + + PluginPaths: pluginPaths, + } + + plugins, errs := o.ListPlugins() + if len(errs) > 0 { + t.Fatalf("unexpected errors: %v", errs) + } + + if !reflect.DeepEqual(expectPlugins, plugins) { + t.Fatalf("saw unexpected plugins. Expecting %v, got %v", expectPlugins, plugins) + } +} + +type duplicatePathError struct { + path string +} + +func (d *duplicatePathError) Error() string { + return fmt.Sprintf("path %q already visited", d.path) +} + +type fakePluginPathVerifier struct { + errors []error + seen map[string]bool + seenUnsorted []string +} + +func (f *fakePluginPathVerifier) Verify(path string) []error { + if f.seen[path] { + err := &duplicatePathError{path} + f.errors = append(f.errors, err) + return []error{err} + } + f.seen[path] = true + f.seenUnsorted = append(f.seenUnsorted, path) + return nil +} + +func newFakePluginPathVerifier() *fakePluginPathVerifier { + return &fakePluginPathVerifier{seen: make(map[string]bool)} +} diff --git a/internal/cli/cmd/plugin/search.go b/internal/cli/cmd/plugin/search.go new file mode 100644 index 000000000..eb23c7522 --- /dev/null +++ b/internal/cli/cmd/plugin/search.go @@ -0,0 +1,97 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "io" + "os" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/printer" +) + +var ( + pluginSearchExample = templates.Examples(` + # search a kbcli or kubectl plugin by name + kbcli plugin search myplugin + `) +) + +func NewPluginSearchCmd(streams genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "search", + Short: "Search kbcli or kubectl plugins", + Example: pluginSearchExample, + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(searchPlugin(streams, args[0])) + }, + } + + return cmd +} + +func searchPlugin(streams genericclioptions.IOStreams, name string) error { + indexes, err := ListIndexes(paths) + if err != nil { + return errors.Wrap(err, "failed to list indexes") + } + + var plugins []pluginEntry + for _, index := range indexes { + plugin, err := LoadPluginByName(paths.IndexPluginsPath(index.Name), name) + if err != nil && !os.IsNotExist(err) { + klog.V(1).Info("failed to load plugin %q from the index", name) + } else { + plugins = append(plugins, pluginEntry{ + index: index.Name, + plugin: plugin, + }) + } + } + + p := NewPluginSearchPrinter(streams.Out) + for _, plugin := range plugins { + _, err := os.Stat(paths.PluginInstallReceiptPath(name)) + addPluginSearchRow(plugin.index, plugin.plugin.Name, !os.IsNotExist(err), p) + } + p.Print() + return nil +} + +func NewPluginSearchPrinter(out io.Writer) *printer.TablePrinter { + t := printer.NewTablePrinter(out) + t.SetHeader("INDEX", "NAME", "INSTALLED") + return t +} + +func addPluginSearchRow(index, plugin string, installed bool, p *printer.TablePrinter) { + if installed { + p.AddRow(index, plugin, "yes") + } else { + p.AddRow(index, plugin, "no") + } +} diff --git a/internal/cli/cmd/plugin/testdata/kbcli-foo b/internal/cli/cmd/plugin/testdata/kbcli-foo new file mode 100644 index 000000000..41b6b7b74 --- /dev/null +++ b/internal/cli/cmd/plugin/testdata/kbcli-foo @@ -0,0 +1,10 @@ +#!/bin/bash + +# optional argument handling +if [[ "$1" == "version" ]] +then + echo "1.0.0" + exit 0 +fi + +echo "I am a plugin named kbcli-foo" diff --git a/internal/cli/cmd/plugin/testdata/kbcli-version b/internal/cli/cmd/plugin/testdata/kbcli-version new file mode 100644 index 000000000..b745356fe --- /dev/null +++ b/internal/cli/cmd/plugin/testdata/kbcli-version @@ -0,0 +1,4 @@ +!/bin/bash + +# This plugin is a no-op and is used to test plugins +# that overshadow existing kbcli commands \ No newline at end of file diff --git a/internal/cli/cmd/plugin/testdata/kubectl-foo b/internal/cli/cmd/plugin/testdata/kubectl-foo new file mode 100644 index 000000000..71dd6e54d --- /dev/null +++ b/internal/cli/cmd/plugin/testdata/kubectl-foo @@ -0,0 +1,10 @@ +#!/bin/bash + +# optional argument handling +if [[ "$1" == "version" ]] +then + echo "1.0.0" + exit 0 +fi + +echo "I am a plugin named kubectl-foo" diff --git a/internal/cli/cmd/plugin/testdata/kubectl-version b/internal/cli/cmd/plugin/testdata/kubectl-version new file mode 100644 index 000000000..e12a64322 --- /dev/null +++ b/internal/cli/cmd/plugin/testdata/kubectl-version @@ -0,0 +1,4 @@ +!/bin/bash + +# This plugin is a no-op and is used to test plugins +# that overshadow existing kbcli commands diff --git a/internal/cli/cmd/plugin/types.go b/internal/cli/cmd/plugin/types.go new file mode 100644 index 000000000..fcd270fc2 --- /dev/null +++ b/internal/cli/cmd/plugin/types.go @@ -0,0 +1,186 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" + "sigs.k8s.io/yaml" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +const ( + DefaultIndexURI = "https://github.com/apecloud/block-index.git" + DefaultIndexName = "default" + KrewIndexURI = "https://github.com/kubernetes-sigs/krew-index.git" + KrewIndexName = "krew" + ManifestExtension = ".yaml" + PluginKind = "Plugin" +) + +var SupportAPIVersion = []string{ + "krew.googlecontainertools.github.com/v1alpha2", + "kbcli.googlecontainertools.github.com/v1alpha2", +} + +type Paths struct { + base string + tmp string +} + +func (p *Paths) BasePath() string { + return p.base +} + +func (p *Paths) IndexBase() string { + return filepath.Join(p.base, "index") +} + +func (p *Paths) IndexPath(name string) string { + return filepath.Join(p.IndexBase(), name) +} + +func (p *Paths) IndexPluginsPath(name string) string { + return filepath.Join(p.IndexPath(name), "plugins") +} + +func (p *Paths) InstallReceiptsPath() string { + return filepath.Join(p.base, "receipts") +} + +func (p *Paths) BinPath() string { + return filepath.Join(p.base, "bin") +} + +func (p *Paths) InstallPath() string { + return filepath.Join(p.base, "store") +} + +func (p *Paths) PluginInstallPath(plugin string) string { + return filepath.Join(p.InstallPath(), plugin) +} + +func (p *Paths) PluginVersionInstallPath(plugin, version string) string { + return filepath.Join(p.InstallPath(), plugin, version) +} + +func (p *Paths) PluginInstallReceiptPath(plugin string) string { + return filepath.Join(p.InstallReceiptsPath(), plugin+".yaml") +} + +type Index struct { + Name string + URL string +} + +type Plugin struct { + metav1.TypeMeta `json:",inline" yaml:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty" yaml:"metadata"` + + Spec PluginSpec `json:"spec"` +} + +// PluginSpec is the plugin specification. +type PluginSpec struct { + Version string `json:"version,omitempty"` + ShortDescription string `json:"shortDescription,omitempty"` + Description string `json:"description,omitempty"` + Caveats string `json:"caveats,omitempty"` + Homepage string `json:"homepage,omitempty"` + + Platforms []Platform `json:"platforms,omitempty"` +} + +// Platform describes how to perform an installation on a specific platform +// and how to match the target platform (os, arch). +type Platform struct { + URI string `json:"uri,omitempty"` + Sha256 string `json:"sha256,omitempty"` + + Selector *metav1.LabelSelector `json:"selector,omitempty"` + Files []FileOperation `json:"files"` + + // Bin specifies the path to the plugin executable. + // The path is relative to the root of the installation folder. + // The binary will be linked after all FileOperations are executed. + Bin string `json:"bin"` +} + +// FileOperation specifies a file copying operation from plugin archive to the +// installation directory. +type FileOperation struct { + From string `json:"from,omitempty"` + To string `json:"to,omitempty"` +} + +// Receipt describes a plugin receipt file. +type Receipt struct { + Plugin `json:",inline" yaml:",inline"` + + Status ReceiptStatus `json:"status"` +} + +// ReceiptStatus contains information about the installed plugin. +type ReceiptStatus struct { + Source SourceIndex `json:"source"` +} + +// SourceIndex contains information about the index a plugin was installed from. +type SourceIndex struct { + // Name is the configured name of an index a plugin was installed from. + Name string `json:"name"` +} + +type InstallOpts struct { + ArchiveFileOverride string +} + +type installOperation struct { + pluginName string + platform Platform + + binDir string + installDir string +} + +// NewReceipt returns a new receipt with the given plugin and index name. +func NewReceipt(plugin Plugin, indexName string, timestamp metav1.Time) Receipt { + plugin.CreationTimestamp = timestamp + return Receipt{ + Plugin: plugin, + Status: ReceiptStatus{ + Source: SourceIndex{ + Name: indexName, + }, + }, + } +} +func StoreReceipt(receipt Receipt, dest string) error { + yamlBytes, err := yaml.Marshal(receipt) + if err != nil { + return errors.Wrapf(err, "convert to yaml") + } + + err = os.WriteFile(dest, yamlBytes, 0o644) + return errors.Wrapf(err, "write plugin receipt %q", dest) +} diff --git a/internal/cli/cmd/plugin/uninstall.go b/internal/cli/cmd/plugin/uninstall.go new file mode 100644 index 000000000..3efd47235 --- /dev/null +++ b/internal/cli/cmd/plugin/uninstall.go @@ -0,0 +1,91 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "os" + "path/filepath" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" + + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +var ( + pluginUninstallExample = templates.Examples(` + # uninstall a kbcli or kubectl plugin by name + kbcli plugin uninstall [PLUGIN] + `) +) + +func NewPluginUninstallCmd(_ genericclioptions.IOStreams) *cobra.Command { + cmd := &cobra.Command{ + Use: "uninstall", + Short: "Uninstall kbcli or kubectl plugins", + Example: pluginUninstallExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(uninstallPlugins(args)) + }, + } + + return cmd +} + +func uninstallPlugins(names []string) error { + for _, name := range names { + klog.V(4).Infof("Going to uninstall plugin %s\n", name) + if err := uninstall(paths, name); err != nil { + return errors.Wrapf(err, "failed to uninstall plugin %s", name) + } + } + return nil +} + +func uninstall(p *Paths, name string) error { + if _, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(name)); err != nil { + if os.IsNotExist(err) { + return ErrIsNotInstalled + } + return errors.Wrapf(err, "failed to look up install receipt for plugin %q", name) + } + + klog.V(1).Infof("Deleting plugin %s", name) + + symlinkPath := filepath.Join(p.BinPath(), pluginNameToBin(name, util.IsWindows())) + klog.V(3).Infof("Unlink %q", symlinkPath) + if err := removeLink(symlinkPath); err != nil { + return errors.Wrap(err, "could not uninstall symlink of plugin") + } + + pluginInstallPath := p.PluginInstallPath(name) + klog.V(3).Infof("Deleting path %q", pluginInstallPath) + if err := os.RemoveAll(pluginInstallPath); err != nil { + return errors.Wrapf(err, "could not remove plugin directory %q", pluginInstallPath) + } + pluginReceiptPath := p.PluginInstallReceiptPath(name) + klog.V(3).Infof("Deleting plugin receipt %q", pluginReceiptPath) + err := os.Remove(pluginReceiptPath) + return errors.Wrapf(err, "could not remove plugin receipt %q", pluginReceiptPath) +} diff --git a/internal/cli/cmd/plugin/upgrade.go b/internal/cli/cmd/plugin/upgrade.go new file mode 100644 index 000000000..da6084b71 --- /dev/null +++ b/internal/cli/cmd/plugin/upgrade.go @@ -0,0 +1,196 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "fmt" + "os" + "strings" + + "github.com/pkg/errors" + "github.com/spf13/cobra" + k8sver "k8s.io/apimachinery/pkg/util/version" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/klog/v2" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/util/templates" +) + +var ( + pluginUpgradeExample = templates.Examples(` + # upgrade installed plugins with specified name + kbcli plugin upgrade myplugin + + # upgrade installed plugin to a newer version + kbcli plugin upgrade --all + `) +) + +type UpgradeOptions struct { + // common user flags + all bool + + pluginNames []string + genericclioptions.IOStreams +} + +func NewPluginUpgradeCmd(streams genericclioptions.IOStreams) *cobra.Command { + o := &UpgradeOptions{ + IOStreams: streams, + } + + cmd := &cobra.Command{ + Use: "upgrade", + Short: "Upgrade kbcli or kubectl plugins", + Example: pluginUpgradeExample, + Run: func(cmd *cobra.Command, args []string) { + cmdutil.CheckErr(o.Complete(args)) + cmdutil.CheckErr(o.Run()) + }, + } + + cmd.Flags().BoolVar(&o.all, "all", o.all, "Upgrade all installed plugins") + + return cmd +} + +func (o *UpgradeOptions) Complete(args []string) error { + if o.all { + installed, err := GetInstalledPluginReceipts(paths.InstallReceiptsPath()) + if err != nil { + return err + } + for _, receipt := range installed { + o.pluginNames = append(o.pluginNames, receipt.Status.Source.Name+"/"+receipt.Name) + } + } else { + if len(args) == 0 { + return errors.New("no plugin name specified") + } + for _, arg := range args { + receipt, err := ReadReceiptFromFile(paths.PluginInstallReceiptPath(arg)) + if err != nil { + return err + } + o.pluginNames = append(o.pluginNames, receipt.Status.Source.Name+"/"+receipt.Name) + } + } + + return nil +} + +func (o *UpgradeOptions) Run() error { + for _, name := range o.pluginNames { + indexName, pluginName := CanonicalPluginName(name) + + plugin, err := LoadPluginByName(paths.IndexPluginsPath(indexName), pluginName) + if err != nil { + return err + } + + fmt.Fprintf(o.Out, "Upgrading plugin: %s\n", name) + if err := Upgrade(paths, plugin, indexName); err != nil { + if err == ErrIsAlreadyUpgraded { + fmt.Fprintf(o.Out, "Plugin %q is already upgraded\n", name) + continue + } + return err + } + } + return nil +} + +// Upgrade reinstalls and deletes the old plugin. The operation tries +// to keep dir in a healthy state if it fails during the process. +func Upgrade(p *Paths, plugin Plugin, indexName string) error { + installReceipt, err := ReadReceiptFromFile(p.PluginInstallReceiptPath(plugin.Name)) + if err != nil { + return errors.Wrapf(err, "failed to load install receipt for plugin %q", plugin.Name) + } + + curVersion := installReceipt.Spec.Version + curv, err := parseVersion(curVersion) + if err != nil { + return errors.Wrapf(err, "failed to parse installed plugin version (%q) as a semver value", curVersion) + } + + // Find available installation candidate + candidate, ok, err := GetMatchingPlatform(plugin.Spec.Platforms) + if err != nil { + return errors.Wrap(err, "failed trying to find a matching platform in plugin spec") + } + if !ok { + return errors.Errorf("plugin %q does not offer installation for this platform (%s)", + plugin.Name, OSArch()) + } + + newVersion := plugin.Spec.Version + newv, err := parseVersion(newVersion) + if err != nil { + return errors.Wrapf(err, "failed to parse candidate version spec (%q)", newVersion) + } + klog.V(2).Infof("Comparing versions: current=%s target=%s", curv, newv) + + // See if it's a newer version + if !curv.LessThan(newv) { + klog.V(3).Infof("Plugin does not need upgrade (%s ≥ %s)", curv, newv) + return ErrIsAlreadyUpgraded + } + klog.V(1).Infof("Plugin needs upgrade (%s < %s)", curv, newv) + + // Re-Install + klog.V(1).Infof("Installing new version %s", newVersion) + if err := install(installOperation{ + pluginName: plugin.Name, + platform: candidate, + + installDir: p.PluginVersionInstallPath(plugin.Name, newVersion), + binDir: p.BinPath(), + }, InstallOpts{}); err != nil { + return errors.Wrap(err, "failed to install new version") + } + + klog.V(2).Infof("Upgrading install receipt for plugin %s", plugin.Name) + if err = StoreReceipt(NewReceipt(plugin, indexName, installReceipt.CreationTimestamp), p.PluginInstallReceiptPath(plugin.Name)); err != nil { + return errors.Wrap(err, "installation receipt could not be stored, uninstall may fail") + } + + // Clean old installations + klog.V(2).Infof("Starting old version cleanup") + return cleanupInstallation(p, plugin, curVersion) +} + +// cleanupInstallation removes a plugin directly +func cleanupInstallation(p *Paths, plugin Plugin, oldVersion string) error { + klog.V(1).Infof("Remove old plugin installation under %q", p.PluginVersionInstallPath(plugin.Name, oldVersion)) + return os.RemoveAll(p.PluginVersionInstallPath(plugin.Name, oldVersion)) +} + +func parseVersion(s string) (*k8sver.Version, error) { + var vv *k8sver.Version + if !strings.HasPrefix(s, "v") { + return vv, errors.Errorf("version string %q does not start with 'v'", s) + } + vv, err := k8sver.ParseSemantic(s) + if err != nil { + return vv, err + } + return vv, nil +} diff --git a/internal/cli/cmd/plugin/utils.go b/internal/cli/cmd/plugin/utils.go new file mode 100644 index 000000000..4319a1f29 --- /dev/null +++ b/internal/cli/cmd/plugin/utils.go @@ -0,0 +1,242 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plugin + +import ( + "io" + "os" + "path/filepath" + "regexp" + "strings" + "unicode" + + "github.com/pkg/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/util/homedir" + "k8s.io/klog/v2" + "sigs.k8s.io/yaml" +) + +var ( + ErrIsAlreadyInstalled = errors.New("can't install, the newest version is already installed") + ErrIsNotInstalled = errors.New("plugin is not installed") + ErrIsAlreadyUpgraded = errors.New("can't upgrade, the newest version is already installed") +) + +func GetKbcliPluginPath() *Paths { + base := filepath.Join(homedir.HomeDir(), ".kbcli", "plugins") + return NewPaths(base) +} + +func EnsureDirs(paths ...string) error { + for _, p := range paths { + if err := os.MkdirAll(p, os.ModePerm); err != nil { + return err + } + } + return nil +} + +func NewPaths(base string) *Paths { + return &Paths{base: base, tmp: os.TempDir()} +} + +func LoadPluginByName(pluginsDir, pluginName string) (Plugin, error) { + klog.V(4).Infof("Reading plugin %q from %s", pluginName, pluginsDir) + return ReadPluginFromFile(filepath.Join(pluginsDir, pluginName+ManifestExtension)) +} + +func ReadPluginFromFile(path string) (Plugin, error) { + var plugin Plugin + err := readFromFile(path, &plugin) + if err != nil { + return plugin, err + } + return plugin, errors.Wrap(ValidatePlugin(plugin.Name, plugin), "plugin manifest validation error") +} + +func ReadReceiptFromFile(path string) (Receipt, error) { + var receipt Receipt + err := readFromFile(path, &receipt) + if err != nil { + return receipt, err + } + return receipt, nil +} + +func readFromFile(path string, as interface{}) error { + f, err := os.Open(path) + if err != nil { + return err + } + err = decodeFile(f, &as) + return errors.Wrapf(err, "failed to parse yaml file %q", path) +} + +func decodeFile(r io.ReadCloser, as interface{}) error { + defer r.Close() + b, err := io.ReadAll(r) + if err != nil { + return err + } + return yaml.Unmarshal(b, &as) +} + +func indent(s string) string { + out := "\\\n" + s = strings.TrimRightFunc(s, unicode.IsSpace) + out += regexp.MustCompile("(?m)^").ReplaceAllString(s, " | ") + out += "\n/" + return out +} + +func applyDefaults(platform *Platform) { + if platform.Files == nil { + platform.Files = []FileOperation{{From: "*", To: "."}} + klog.V(4).Infof("file operation not specified, assuming %v", platform.Files) + } +} + +// GetInstalledPluginReceipts returns a list of receipts. +func GetInstalledPluginReceipts(receiptsDir string) ([]Receipt, error) { + files, err := filepath.Glob(filepath.Join(receiptsDir, "*"+ManifestExtension)) + if err != nil { + return nil, errors.Wrapf(err, "failed to glob receipts directory (%s) for manifests", receiptsDir) + } + out := make([]Receipt, 0, len(files)) + for _, f := range files { + r, err := ReadReceiptFromFile(f) + if err != nil { + return nil, errors.Wrapf(err, "failed to parse plugin install receipt %s", f) + } + out = append(out, r) + klog.V(4).Infof("parsed receipt for %s: version=%s", r.GetObjectMeta().GetName(), r.Spec.Version) + + } + return out, nil +} + +func isSupportAPIVersion(apiVersion string) bool { + for _, v := range SupportAPIVersion { + if apiVersion == v { + return true + } + } + return false +} + +func ValidatePlugin(name string, p Plugin) error { + if !isSupportAPIVersion(p.APIVersion) { + return errors.Errorf("plugin manifest has apiVersion=%q, not supported in this version of krew (try updating plugin index or install a newer version of krew)", p.APIVersion) + } + if p.Kind != PluginKind { + return errors.Errorf("plugin manifest has kind=%q, but only %q is supported", p.Kind, PluginKind) + } + if p.Name != name { + return errors.Errorf("plugin manifest has name=%q, but expected %q", p.Name, name) + } + if p.Spec.ShortDescription == "" { + return errors.New("should have a short description") + } + if len(p.Spec.Platforms) == 0 { + return errors.New("should have a platform") + } + if p.Spec.Version == "" { + return errors.New("should have a version") + } + if _, err := parseVersion(p.Spec.Version); err != nil { + return errors.Wrap(err, "failed to parse version") + } + for _, pl := range p.Spec.Platforms { + if err := validatePlatform(pl); err != nil { + return errors.Wrapf(err, "platform (%+v) is badly constructed", pl) + } + } + return nil +} + +func validatePlatform(p Platform) error { + if p.URI == "" { + return errors.New("`uri` is unset") + } + if p.Sha256 == "" { + return errors.New("`sha256` sum is unset") + } + if p.Bin == "" { + return errors.New("`bin` is unset") + } + if err := validateFiles(p.Files); err != nil { + return errors.Wrap(err, "`files` is invalid") + } + if err := validateSelector(p.Selector); err != nil { + return errors.Wrap(err, "invalid platform selector") + } + return nil +} + +func validateFiles(fops []FileOperation) error { + if fops == nil { + return nil + } + if len(fops) == 0 { + return errors.New("`files` is empty, set it") + } + for _, op := range fops { + if op.From == "" { + return errors.New("`from` field is unset") + } else if op.To == "" { + return errors.New("`to` field is unset") + } + } + return nil +} + +// validateSelector checks if the platform selector uses supported keys and is not empty or nil. +func validateSelector(sel *metav1.LabelSelector) error { + if sel == nil { + return errors.New("nil selector is not supported") + } + if sel.MatchLabels == nil && len(sel.MatchExpressions) == 0 { + return errors.New("empty selector is not supported") + } + + // check for unsupported keys + keys := []string{} + for k := range sel.MatchLabels { + keys = append(keys, k) + } + for _, expr := range sel.MatchExpressions { + keys = append(keys, expr.Key) + } + for _, key := range keys { + if key != "os" && key != "arch" { + return errors.Errorf("key %q not supported", key) + } + } + + if sel.MatchLabels != nil && len(sel.MatchLabels) == 0 { + return errors.New("`matchLabels` specified but empty") + } + if sel.MatchExpressions != nil && len(sel.MatchExpressions) == 0 { + return errors.New("`matchExpressions` specified but empty") + } + + return nil +} diff --git a/internal/cli/cmd/version/suite_test.go b/internal/cli/cmd/version/suite_test.go index 0e19b0b64..43f4ae546 100644 --- a/internal/cli/cmd/version/suite_test.go +++ b/internal/cli/cmd/version/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package version diff --git a/internal/cli/cmd/version/version.go b/internal/cli/cmd/version/version.go index 695e8ec75..697b0a295 100644 --- a/internal/cli/cmd/version/version.go +++ b/internal/cli/cmd/version/version.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package version @@ -52,14 +55,14 @@ func (o *versionOptions) Run(f cmdutil.Factory) { klog.V(1).Infof("failed to get clientset: %v", err) } - versionInfo, _ := util.GetVersionInfo(client) - if v := versionInfo[util.KubernetesApp]; len(v) > 0 { - fmt.Printf("Kubernetes: %s\n", v) + v, _ := util.GetVersionInfo(client) + if v.Kubernetes != "" { + fmt.Printf("Kubernetes: %s\n", v.Kubernetes) } - if v := versionInfo[util.KubeBlocksApp]; len(v) > 0 { - fmt.Printf("KubeBlocks: %s\n", v) + if v.KubeBlocks != "" { + fmt.Printf("KubeBlocks: %s\n", v.KubeBlocks) } - fmt.Printf("kbcli: %s\n", versionInfo[util.KBCLIApp]) + fmt.Printf("kbcli: %s\n", v.Cli) if o.verbose { fmt.Printf(" BuildDate: %s\n", version.BuildDate) fmt.Printf(" GitCommit: %s\n", version.GitCommit) diff --git a/internal/cli/cmd/version/version_test.go b/internal/cli/cmd/version/version_test.go index 5bd9c9ac6..f3c1d9c25 100644 --- a/internal/cli/cmd/version/version_test.go +++ b/internal/cli/cmd/version/version_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package version diff --git a/internal/cli/create/create.go b/internal/cli/create/create.go old mode 100644 new mode 100755 index 480492770..3642e074b --- a/internal/cli/create/create.go +++ b/internal/cli/create/create.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package create @@ -27,19 +30,20 @@ import ( "cuelang.org/go/cue/cuecontext" cuejson "cuelang.org/go/encoding/json" "github.com/leaanthony/debme" - "github.com/spf13/cobra" - "k8s.io/apimachinery/pkg/api/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" - k8sapitypes "k8s.io/apimachinery/pkg/types" "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/scheme" - "github.com/apecloud/kubeblocks/internal/cli/types" - "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/edit" + "github.com/apecloud/kubeblocks/internal/cli/printer" ) var ( @@ -47,254 +51,234 @@ var ( cueTemplate embed.FS ) -type Inputs struct { - // Use cobra command use - Use string - - // Short is the short description shown in the 'help' output. - Short string +type CreateDependency func(dryRun []string) error - // Example is examples of how to use the command. - Example string +type DryRunStrategy int - // BaseOptionsObj - BaseOptionsObj *BaseOptions +const ( + DryRunNone DryRunStrategy = iota + DryRunClient + DryRunServer +) - // Options a command options object which extends BaseOptions - Options interface{} +// CreateOptions the options of creation command should inherit baseOptions +type CreateOptions struct { + Factory cmdutil.Factory + Namespace string - // CueTemplateName cue template file name + // Name Resource name of the command line operation + Name string + Args []string + Dynamic dynamic.Interface + Client kubernetes.Interface + Format printer.Format + ToPrinter func(*meta.RESTMapping, bool) (printers.ResourcePrinterFunc, error) + DryRun string + EditBeforeCreate bool + + // CueTemplateName cue template file name to render the resource CueTemplateName string - // ResourceName k8s resource name - ResourceName string - - // Group of API, default is apps - Group string - - // Group of Version, default is v1alpha1 - Version string - - // Factory - Factory cmdutil.Factory - - // ValidateFunc optional, custom validate func - Validate func() error + // Options a command options object which extends CreateOptions that will be used + // to render the cue template + Options interface{} - // Complete optional, do custom complete options - Complete func() error + // GVR is the GroupVersionResource of the resource to be created + GVR schema.GroupVersionResource - BuildFlags func(*cobra.Command) + // CustomOutPut will be executed after creating successfully. + CustomOutPut func(options *CreateOptions) // PreCreate optional, make changes on yaml before create PreCreate func(*unstructured.Unstructured) error - // CustomOutPut will be executed after creating successfully. - CustomOutPut func(options *BaseOptions) + // CleanUpFn will be executed after creating failed. + CleanUpFn func() error - // ResourceNameGVRForCompletion resource name for completion. - ResourceNameGVRForCompletion schema.GroupVersionResource -} - -// BaseOptions the options of creation command should inherit baseOptions -type BaseOptions struct { - // Namespace k8s namespace - Namespace string `json:"namespace"` - - // Name Resource name of the command line operation - Name string `json:"name"` - - Dynamic dynamic.Interface `json:"-"` - - Client kubernetes.Interface `json:"-"` + // CreateDependencies will be executed before creating. + CreateDependencies CreateDependency // Quiet minimize unnecessary output Quiet bool - ClientSet kubernetes.Interface - genericclioptions.IOStreams } -// BuildCommand build create command -func BuildCommand(inputs Inputs) *cobra.Command { - cmd := &cobra.Command{ - Use: inputs.Use, - Short: inputs.Short, - Example: inputs.Example, - ValidArgsFunction: util.ResourceNameCompletionFunc(inputs.Factory, inputs.ResourceNameGVRForCompletion), - Run: func(cmd *cobra.Command, args []string) { - util.CheckErr(inputs.BaseOptionsObj.Complete(inputs, args)) - util.CheckErr(inputs.BaseOptionsObj.Validate(inputs)) - util.CheckErr(inputs.BaseOptionsObj.Run(inputs)) - }, - } - if inputs.BuildFlags != nil { - inputs.BuildFlags(cmd) - } - return cmd -} - -func (o *BaseOptions) Complete(inputs Inputs, args []string) error { +func (o *CreateOptions) Complete() error { var err error - if o.Namespace, _, err = inputs.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { + if o.Namespace, _, err = o.Factory.ToRawKubeConfigLoader().Namespace(); err != nil { return err } - if len(args) > 0 { - o.Name = args[0] + // now we use the first argument as the resource name + if len(o.Args) > 0 { + o.Name = o.Args[0] } - if o.Dynamic, err = inputs.Factory.DynamicClient(); err != nil { + if o.Dynamic, err = o.Factory.DynamicClient(); err != nil { return err } - if o.Client, err = inputs.Factory.KubernetesClientSet(); err != nil { + if o.Client, err = o.Factory.KubernetesClientSet(); err != nil { return err } - if o.ClientSet, err = inputs.Factory.KubernetesClientSet(); err != nil { - return err - } - - // do custom options complete - if inputs.Complete != nil { - if err = inputs.Complete(); err != nil { - return err + o.ToPrinter = func(mapping *meta.RESTMapping, withNamespace bool) (printers.ResourcePrinterFunc, error) { + var p printers.ResourcePrinter + switch o.Format { + case printer.JSON: + p = &printers.JSONPrinter{} + case printer.YAML: + p = &printers.YAMLPrinter{} + default: + return nil, genericclioptions.NoCompatiblePrinterError{AllowedFormats: []string{"JSON", "YAML"}} } - } - return nil -} -func (o *BaseOptions) Validate(inputs Inputs) error { - // do options validate - if inputs.Validate != nil { - if err := inputs.Validate(); err != nil { - return err + p, err = printers.NewTypeSetter(scheme.Scheme).WrapToPrinter(p, nil) + if err != nil { + return nil, err } + return p.PrintObj, nil } + return nil } // Run execute command. the options of parameter contain the command flags and args. -func (o *BaseOptions) Run(inputs Inputs) error { - var ( - cueValue cue.Value - err error - unstructuredObj *unstructured.Unstructured - optionsByte []byte - ) - - if optionsByte, err = json.Marshal(inputs.Options); err != nil { +func (o *CreateOptions) Run() error { + resObj, err := o.buildResourceObj() + if err != nil { return err } - if cueValue, err = newCueValue(inputs.CueTemplateName); err != nil { - return err + if o.PreCreate != nil { + if err = o.PreCreate(resObj); err != nil { + return err + } } - if cueValue, err = fillOptions(cueValue, optionsByte); err != nil { - return err + if o.EditBeforeCreate { + customEdit := edit.NewCustomEditOptions(o.Factory, o.IOStreams, "create") + if err := customEdit.Run(resObj); err != nil { + return err + } } - if unstructuredObj, err = convertContentToUnstructured(cueValue); err != nil { + dryRunStrategy, err := o.GetDryRunStrategy() + if err != nil { return err } - if inputs.PreCreate != nil { - if err = inputs.PreCreate(unstructuredObj); err != nil { + if dryRunStrategy != DryRunClient { + createOptions := metav1.CreateOptions{} + + if dryRunStrategy == DryRunServer { + createOptions.DryRun = []string{metav1.DryRunAll} + } + + // create dependencies + if o.CreateDependencies != nil { + if err = o.CreateDependencies(createOptions.DryRun); err != nil { + return err + } + } + + // create kubernetes resource + resObj, err = o.Dynamic.Resource(o.GVR).Namespace(o.Namespace).Create(context.TODO(), resObj, createOptions) + if err != nil { + if apierrors.IsAlreadyExists(err) { + return err + } + + // for other errors, clean up dependencies + if cleanErr := o.CleanUp(); cleanErr != nil { + fmt.Fprintf(o.ErrOut, "Failed to clean up denpendencies: %v\n", cleanErr) + } return err } - } - group := inputs.Group - if len(group) == 0 { - group = types.AppsAPIGroup - } - version := inputs.Version - if len(version) == 0 { - version = types.AppsAPIVersion + if dryRunStrategy != DryRunServer { + o.Name = resObj.GetName() + if o.Quiet { + return nil + } + if o.CustomOutPut != nil { + o.CustomOutPut(o) + } else { + fmt.Fprintf(o.Out, "%s %s created\n", resObj.GetKind(), resObj.GetName()) + } + return nil + } } - // create k8s resource - gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: inputs.ResourceName} - if unstructuredObj, err = o.Dynamic.Resource(gvr).Namespace(o.Namespace).Create(context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil { + printer, err := o.ToPrinter(nil, false) + if err != nil { return err } - o.Name = unstructuredObj.GetName() - if o.Quiet { + return printer.PrintObj(resObj, o.Out) +} + +func (o *CreateOptions) CleanUp() error { + if o.CreateDependencies == nil { return nil } - if inputs.CustomOutPut != nil { - inputs.CustomOutPut(o) - } else { - fmt.Fprintf(o.Out, "%s %s created\n", unstructuredObj.GetKind(), unstructuredObj.GetName()) + + if o.CleanUpFn != nil { + return o.CleanUpFn() } return nil } -// RunAsApply execute command. the options of parameter contain the command flags and args. -// if the resource exists, run as "kubectl apply". -func (o *BaseOptions) RunAsApply(inputs Inputs) error { +func (o *CreateOptions) buildResourceObj() (*unstructured.Unstructured, error) { var ( - cueValue cue.Value - err error - unstructuredObj *unstructured.Unstructured - optionsByte []byte + cueValue cue.Value + err error + optionsByte []byte ) - if optionsByte, err = json.Marshal(inputs.Options); err != nil { - return err + if optionsByte, err = json.Marshal(o.Options); err != nil { + return nil, err } - if cueValue, err = newCueValue(inputs.CueTemplateName); err != nil { - return err + // append namespace and name to options and marshal to json + m := make(map[string]interface{}) + if err = json.Unmarshal(optionsByte, &m); err != nil { + return nil, err } - - if cueValue, err = fillOptions(cueValue, optionsByte); err != nil { - return err + m["namespace"] = o.Namespace + m["name"] = o.Name + if optionsByte, err = json.Marshal(m); err != nil { + return nil, err } - if unstructuredObj, err = convertContentToUnstructured(cueValue); err != nil { - return err + if cueValue, err = newCueValue(o.CueTemplateName); err != nil { + return nil, err } - group := inputs.Group - if len(group) == 0 { - group = types.AppsAPIGroup + if cueValue, err = fillOptions(cueValue, optionsByte); err != nil { + return nil, err } + return convertContentToUnstructured(cueValue) +} - version := inputs.Version - if len(version) == 0 { - version = types.AppsAPIVersion +func (o *CreateOptions) GetDryRunStrategy() (DryRunStrategy, error) { + if o.DryRun == "" { + return DryRunNone, nil + } + switch o.DryRun { + case "client": + return DryRunClient, nil + case "server": + return DryRunServer, nil + case "unchanged": + return DryRunClient, nil + case "none": + return DryRunNone, nil + default: + return DryRunNone, fmt.Errorf(`invalid dry-run value (%v). Must be "none", "server", or "client"`, o.DryRun) } - // create k8s resource - gvr := schema.GroupVersionResource{Group: group, Version: version, Resource: inputs.ResourceName} - objectName, _, err := unstructured.NestedString(unstructuredObj.Object, "metadata", "name") - if err != nil { - return err - } - objectByte, err := json.Marshal(unstructuredObj) - if err != nil { - return err - } - if _, err := o.Dynamic.Resource(gvr).Namespace(o.Namespace).Patch( - context.TODO(), objectName, k8sapitypes.MergePatchType, - objectByte, metav1.PatchOptions{}); err != nil { - - // create object if not found - if errors.IsNotFound(err) { - if _, err = o.Dynamic.Resource(gvr).Namespace(o.Namespace).Create( - context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil { - return err - } - } else { - return err - } - } - return nil } -// NewCueValue convert cue template to cue Value which holds any value like Boolean,Struct,String and more cue type. +// NewCueValue converts cue template to cue Value which holds any value like Boolean,Struct,String and more cue type. func newCueValue(cueTemplateName string) (cue.Value, error) { tmplFs, _ := debme.FS(cueTemplate, "template") if tmlBytes, err := tmplFs.ReadFile(cueTemplateName); err != nil { @@ -304,7 +288,7 @@ func newCueValue(cueTemplateName string) (cue.Value, error) { } } -// fillOptions fill options object in cue template file +// fillOptions fills options object in cue template file func fillOptions(cueValue cue.Value, optionsByte []byte) (cue.Value, error) { var ( expr ast.Expr @@ -318,7 +302,7 @@ func fillOptions(cueValue cue.Value, optionsByte []byte) (cue.Value, error) { return cueValue, nil } -// convertContentToUnstructured get content object in cue template file and convert it to Unstructured +// convertContentToUnstructured gets content object in cue template file and convert it to Unstructured func convertContentToUnstructured(cueValue cue.Value) (*unstructured.Unstructured, error) { var ( contentByte []byte diff --git a/internal/cli/create/create_test.go b/internal/cli/create/create_test.go old mode 100644 new mode 100755 index 7d59a0d96..81c0d0b66 --- a/internal/cli/create/create_test.go +++ b/internal/cli/create/create_test.go @@ -1,48 +1,69 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package create import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - "github.com/spf13/cobra" "k8s.io/cli-runtime/pkg/genericclioptions" clientfake "k8s.io/client-go/rest/fake" cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + "github.com/apecloud/kubeblocks/internal/cli/printer" "github.com/apecloud/kubeblocks/internal/cli/testing" "github.com/apecloud/kubeblocks/internal/cli/types" ) var _ = Describe("Create", func() { + const ( + clusterName = "test" + cueFileName = "create_template_test.cue" + ) + var ( - tf *cmdtesting.TestFactory - streams genericclioptions.IOStreams - baseOptions BaseOptions + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + options CreateOptions ) BeforeEach(func() { streams, _, _, _ = genericclioptions.NewTestIOStreams() tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) tf.Client = &clientfake.RESTClient{} - baseOptions = BaseOptions{ - Name: "test", - IOStreams: streams, + clusterOptions := map[string]interface{}{ + "clusterDefRef": "test-def", + "clusterVersionRef": "test-clusterversion-ref", + "components": []string{}, + "terminationPolicy": "Halt", + } + options = CreateOptions{ + Factory: tf, + Name: clusterName, + Namespace: testing.Namespace, + IOStreams: streams, + GVR: types.ClusterGVR(), + CueTemplateName: cueFileName, + Options: clusterOptions, } }) @@ -51,77 +72,74 @@ var _ = Describe("Create", func() { }) Context("Create Objects", func() { - It("test Create run", func() { - clusterOptions := map[string]interface{}{ - "name": "test", - "namespace": testing.Namespace, - "clusterDefRef": "test-def", - "clusterVersionRef": "test-clusterversion-ref", - "components": []string{}, - "terminationPolicy": "Halt", - } + It("Complete", func() { + options.Args = []string{} + Expect(options.Complete()).Should(Succeed()) + }) - inputs := Inputs{ - CueTemplateName: "create_template_test.cue", - ResourceName: types.ResourceClusters, - BaseOptionsObj: &baseOptions, - Options: clusterOptions, - Factory: tf, - Validate: func() error { - return nil - }, - Complete: func() error { - return nil + It("test create with dry-run", func() { + options.Format = printer.YAML + testCases := []struct { + clusterName string + isUseDryRun bool + mode string + dryRunStrategy DryRunStrategy + success bool + }{ + { // test do not use dry-run strategy + "test1", + false, + "", + DryRunNone, + true, }, - BuildFlags: func(cmd *cobra.Command) { - cmd.Flags().StringVar(&baseOptions.Namespace, "clusterDefRef", "", "cluster definition") + { // test no parameter strategy + "test2", + true, + "unchanged", + DryRunClient, + true, }, - } - cmd := BuildCommand(inputs) - Expect(cmd).ShouldNot(BeNil()) - Expect(cmd.Flags().Lookup("clusterDefRef")).ShouldNot(BeNil()) - - Expect(baseOptions.Complete(inputs, []string{})).Should(Succeed()) - Expect(baseOptions.Validate(inputs)).Should(Succeed()) - Expect(baseOptions.Run(inputs)).Should(Succeed()) - }) - - It("test Create runAsApply", func() { - clusterOptions := map[string]interface{}{ - "name": "test-apply", - "namespace": testing.Namespace, - "clusterDefRef": "test-def", - "clusterVersionRef": "test-clusterversion-ref", - "components": []string{}, - "terminationPolicy": "Halt", - } - - inputs := Inputs{ - CueTemplateName: "create_template_test.cue", - ResourceName: types.ResourceClusters, - BaseOptionsObj: &baseOptions, - Options: clusterOptions, - Factory: tf, - Validate: func() error { - return nil + { // test client strategy + "test3", + true, + "client", + DryRunClient, + true, }, - Complete: func() error { - return nil + { // test server strategy + "test4", + true, + "server", + DryRunServer, + true, }, - BuildFlags: func(cmd *cobra.Command) { - cmd.Flags().StringVar(&baseOptions.Namespace, "clusterDefRef", "", "cluster definition") + { // test error parameter + "test5", + true, + "ape", + DryRunServer, + false, }, } - cmd := BuildCommand(inputs) - Expect(cmd).ShouldNot(BeNil()) - Expect(cmd.Flags().Lookup("clusterDefRef")).ShouldNot(BeNil()) - - Expect(baseOptions.Complete(inputs, []string{})).Should(Succeed()) - Expect(baseOptions.Validate(inputs)).Should(Succeed()) - // create - Expect(baseOptions.RunAsApply(inputs)).Should(Succeed()) - // apply if exists - Expect(baseOptions.RunAsApply(inputs)).Should(Succeed()) + + for _, t := range testCases { + By(fmt.Sprintf("when isDryRun %v, dryRunStrategy %v, mode %s", + t.isUseDryRun, t.dryRunStrategy, t.mode)) + options.Name = t.clusterName + if t.isUseDryRun { + options.DryRun = t.mode + } + Expect(options.Complete()).Should(Succeed()) + + s, _ := options.GetDryRunStrategy() + if t.success { + Expect(s == t.dryRunStrategy).Should(BeTrue()) + Expect(options.Run()).Should(Succeed()) + } else { + Expect(s).ShouldNot(Equal(t.dryRunStrategy)) + } + } }) }) }) diff --git a/internal/cli/create/suite_test.go b/internal/cli/create/suite_test.go index 6567bd8b3..b7857ebc9 100644 --- a/internal/cli/create/suite_test.go +++ b/internal/cli/create/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package create diff --git a/internal/cli/create/template/backup_template.cue b/internal/cli/create/template/backup_template.cue index 2967978d7..85d37eabf 100644 --- a/internal/cli/create/template/backup_template.cue +++ b/internal/cli/create/template/backup_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // required, command line input options for parameters and flags options: { @@ -18,7 +21,6 @@ options: { namespace: string backupType: string backupPolicy: string - ttl: string } // required, k8s api resource content @@ -35,6 +37,5 @@ content: { spec: { backupType: options.backupType backupPolicyName: options.backupPolicy - ttl: options.ttl } } diff --git a/internal/cli/create/template/backuppolicy_template.cue b/internal/cli/create/template/backuppolicy_template.cue deleted file mode 100644 index 0da18ea4b..000000000 --- a/internal/cli/create/template/backuppolicy_template.cue +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright ApeCloud, Inc. -// -// 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. - -// required, command line input options for parameters and flags -options: { - name: string - namespace: string - clusterName: string - ttl: string - connectionSecret: string - policyTemplate: string - role: string -} - -// required, k8s api resource content -content: { - apiVersion: "dataprotection.kubeblocks.io/v1alpha1" - kind: "BackupPolicy" - metadata: { - name: options.name - namespace: options.namespace - } - spec: { - backupPolicyTemplateName: options.policyTemplate - target: { - labelsSelector: { - matchLabels: { - "app.kubernetes.io/instance": options.clusterName - if options.role != _|_ { - "kubeblocks.io/role": options.role - } - } - } - secret: { - name: options.connectionSecret - } - } - remoteVolume: { - name: "backup-remote-volume" - persistentVolumeClaim: { - claimName: "backup-s3-pvc" - } - } - ttl: options.ttl - } -} diff --git a/internal/cli/create/template/cluster_operations_template.cue b/internal/cli/create/template/cluster_operations_template.cue index eb02b11e9..3c1f9c3d3 100644 --- a/internal/cli/create/template/cluster_operations_template.cue +++ b/internal/cli/create/template/cluster_operations_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // required, command line input options for parameters and flags options: { @@ -24,6 +27,7 @@ options: { componentNames: [...string] cpu: string memory: string + class: string replicas: int storage: string vctNames: [...string] @@ -88,6 +92,9 @@ content: { if options.type == "VerticalScaling" { verticalScaling: [ for _, cName in options.componentNames { componentName: cName + if options.class != "" { + class: options.class + } requests: { if options.memory != "" { memory: options.memory diff --git a/internal/cli/create/template/cluster_template.cue b/internal/cli/create/template/cluster_template.cue index 6ccc528e0..75b5b7a92 100644 --- a/internal/cli/create/template/cluster_template.cue +++ b/internal/cli/create/template/cluster_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // required, command line input options for parameters and flags options: { diff --git a/internal/cli/create/template/create_template_test.cue b/internal/cli/create/template/create_template_test.cue index 4309a6db7..e487417b9 100644 --- a/internal/cli/create/template/create_template_test.cue +++ b/internal/cli/create/template/create_template_test.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // required, options for command line input for args and flags. options: { diff --git a/internal/cli/create/template/dns_chaos_template.cue b/internal/cli/create/template/dns_chaos_template.cue new file mode 100644 index 000000000..96a63787d --- /dev/null +++ b/internal/cli/create/template/dns_chaos_template.cue @@ -0,0 +1,47 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + action: string + selector: {} + mode: string + value: string + duration: string + + patterns: [...] +} + +// required, k8s api resource content +content: { + kind: "DNSChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "dns-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + action: options.action + mode: options.mode + value: options.value + duration: options.duration + + patterns: options.patterns + } +} diff --git a/internal/cli/create/template/http_chaos_template.cue b/internal/cli/create/template/http_chaos_template.cue new file mode 100644 index 000000000..87700b44f --- /dev/null +++ b/internal/cli/create/template/http_chaos_template.cue @@ -0,0 +1,76 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + selector: {} + mode: string + value: string + duration: string + + target: string + port: int32 + path: string + method: string + code?: int32 + + abort?: bool + delay?: string + replace?: {} + patch?: {} +} + +// required, k8s api resource content +content: { + kind: "HTTPChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "http-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + duration: options.duration + + target: options.target + port: options.port + path: options.path + method: options.method + if options.code != _|_ { + code: options.code + } + + if options.abort != _|_ { + abort: options.abort + } + + if options.delay != _|_ { + delay: options.delay + } + + if len(options.replace) != 0 { + replace: options.replace + } + + if len(options.patch["body"]) != 0 { + patch: options.patch + } + } +} diff --git a/internal/cli/create/template/io_chaos_template.cue b/internal/cli/create/template/io_chaos_template.cue new file mode 100644 index 000000000..0d6f4a917 --- /dev/null +++ b/internal/cli/create/template/io_chaos_template.cue @@ -0,0 +1,69 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + action: string + selector: {} + mode: string + value: string + duration: string + + delay: string + errno: int + attr?: {} + mistake?: {} + + volumePath: string + path: string + percent: int + methods: [...] + containerNames: [...] +} + +// required, k8s api resource content +content: { + kind: "IOChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "io-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + action: options.action + duration: options.duration + + delay: options.delay + errno: options.errno + if len(options.attr) != 0 { + attr: options.attr + } + if len(options.mistake) != 0 { + mistake: options.mistake + } + + volumePath: options.volumePath + path: options.path + percent: options.percent + methods: options.methods + containerNames: options.containerNames + } +} diff --git a/internal/cli/create/template/migration_template.cue b/internal/cli/create/template/migration_template.cue new file mode 100644 index 000000000..83ffc3bc8 --- /dev/null +++ b/internal/cli/create/template/migration_template.cue @@ -0,0 +1,92 @@ +//Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +//This file is part of KubeBlocks project +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + name: string + namespace: string + teplate: string + taskType: string + source: string + sourceEndpointModel: {} + sink: string + sinkEndpointModel: {} + migrationObject: [...] + migrationObjectModel: {} + steps: [...] + stepsModel: [...] + tolerations: [...] + tolerationModel: {} + resources: [...] + resourceModel: {} + serverId: int +} + +// required, k8s api resource content +content: { + apiVersion: "datamigration.apecloud.io/v1alpha1" + kind: "MigrationTask" + metadata: { + name: options.name + namespace: options.namespace + } + spec: { + taskType: options.taskType + template: options.template + sourceEndpoint: options.sourceEndpointModel + sinkEndpoint: options.sinkEndpointModel + initialization: { + steps: options.stepsModel + config: { + preCheck: { + resource: { + limits: options.resourceModel["precheck"] + } + tolerations: options.tolerationModel["precheck"] + } + initStruct: { + resource: { + limits: options.resourceModel["init-struct"] + } + tolerations: options.tolerationModel["init-struct"] + } + initData: { + resource: { + limits: options.resourceModel["init-data"] + } + tolerations: options.tolerationModel["init-data"] + } + } + } + cdc: { + config: { + resource: { + limits: options.resourceModel["cdc"] + } + tolerations: options.tolerationModel["cdc"] + param: { + "extractor.server_id": options.serverId + } + } + } + migrationObj: options.migrationObjectModel + globalTolerations: options.tolerationModel["global"] + globalResources: { + limits: options.resourceModel["global"] + } + } +} diff --git a/internal/cli/create/template/network_chaos_template.cue b/internal/cli/create/template/network_chaos_template.cue new file mode 100644 index 000000000..494a193e5 --- /dev/null +++ b/internal/cli/create/template/network_chaos_template.cue @@ -0,0 +1,83 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + action: string + selector: {} + mode: string + value: string + duration: string + + direction: string + externalTargets?: [...] + target?: {} + + loss?: {} + delay?: {} + duplicate?: {} + corrupt?: {} + bandwidth?: {} +} + +// required, k8s api resource content +content: { + kind: "NetworkChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "network-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + action: options.action + duration: options.duration + + direction: options.direction + + if options.externalTargets != _|_ { + externalTargets: options.externalTargets + } + + if options.target["mode"] != _|_ || len(options.target["selector"]) != 0 { + target: options.target + } + + if options.loss["loss"] != _|_ { + loss: options.loss + } + + if options.delay["latency"] != _|_ { + delay: options.delay + } + + if options.corrupt["corrupt"] != _|_ { + corrupt: options.corrupt + } + + if options.duplicate["duplicate"] != _|_ { + duplicate: options.duplicate + } + + if options.bandwidth["rate"] != _|_ { + bandwidth: options.bandwidth + } + } +} diff --git a/internal/cli/create/template/node_chaos_template.cue b/internal/cli/create/template/node_chaos_template.cue new file mode 100644 index 000000000..f07862072 --- /dev/null +++ b/internal/cli/create/template/node_chaos_template.cue @@ -0,0 +1,65 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + kind: string + namespace: string + action: string + secretName: string + region: string + instance: string + volumeID: string + deviceName?: string + duration: string + + project: string +} + +// required, k8s api resource content +content: { + kind: options.kind + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "node-chaos-" + namespace: options.namespace + } + spec: { + action: options.action + secretName: options.secretName + duration: options.duration + + if options.kind == "AWSChaos" { + awsRegion: options.region + ec2Instance: options.instance + if options.deviceName != _|_ { + volumeID: options.volumeID + deviceName: options.deviceName + } + } + + if options.kind == "GCPChaos" { + project: options.project + zone: options.region + instance: options.instance + if options.deviceName != _|_ { + deviceNames: [options.deviceName] + } + + } + } +} diff --git a/internal/cli/create/template/pod_chaos_template.cue b/internal/cli/create/template/pod_chaos_template.cue new file mode 100644 index 000000000..654479624 --- /dev/null +++ b/internal/cli/create/template/pod_chaos_template.cue @@ -0,0 +1,49 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + action: string + selector: {} + mode: string + value: string + duration: string + + gracePeriod: int64 + containerNames: [...] +} + +// required, k8s api resource content +content: { + kind: "PodChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "pod-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + action: options.action + duration: options.duration + + gracePeriod: options.gracePeriod + containerNames: options.containerNames + } +} diff --git a/internal/cli/create/template/stress_chaos_template.cue b/internal/cli/create/template/stress_chaos_template.cue new file mode 100644 index 000000000..94a74885a --- /dev/null +++ b/internal/cli/create/template/stress_chaos_template.cue @@ -0,0 +1,46 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + selector: {} + mode: string + value: string + duration: string + + stressors: {} + containerNames: [...] +} + +// required, k8s api resource content +content: { + kind: "StressChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "stress-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + duration: options.duration + stressors: options.stressors + containerNames: options.containerNames + } +} diff --git a/internal/cli/create/template/time_chaos_template.cue b/internal/cli/create/template/time_chaos_template.cue new file mode 100644 index 000000000..68512adf2 --- /dev/null +++ b/internal/cli/create/template/time_chaos_template.cue @@ -0,0 +1,49 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +// required, command line input options for parameters and flags +options: { + namespace: string + selector: {} + mode: string + value: string + duration: string + + timeOffset: string + clockIds: [...] + containerNames: [...] +} + +// required, k8s api resource content +content: { + kind: "TimeChaos" + apiVersion: "chaos-mesh.org/v1alpha1" + metadata: { + generateName: "time-chaos-" + namespace: options.namespace + } + spec: { + selector: options.selector + mode: options.mode + value: options.value + duration: options.duration + + timeOffset: options.timeOffset + clockIds: options.clockIds + containerNames: options.containerNames + } +} diff --git a/internal/cli/create/template/volumesnapshotclass_template.cue b/internal/cli/create/template/volumesnapshotclass_template.cue index 1730dca9f..81c446be8 100644 --- a/internal/cli/create/template/volumesnapshotclass_template.cue +++ b/internal/cli/create/template/volumesnapshotclass_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // required, command line input options for parameters and flags options: { diff --git a/internal/cli/delete/delete.go b/internal/cli/delete/delete.go index 2816e6eb0..93c1a248d 100644 --- a/internal/cli/delete/delete.go +++ b/internal/cli/delete/delete.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package delete @@ -35,7 +38,8 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/util/prompt" ) -type DeleteHook func(object runtime.Object) error +type DeleteHook func(options *DeleteOptions, object runtime.Object) error + type DeleteOptions struct { Factory cmdutil.Factory Namespace string @@ -167,7 +171,6 @@ func (o *DeleteOptions) deleteResult(r *resource.Result) error { if err != nil { return err } - var runtimeObj runtime.Object deleteInfos = append(deleteInfos, info) found++ @@ -178,10 +181,10 @@ func (o *DeleteOptions) deleteResult(r *resource.Result) error { if err = o.preDeleteResource(info); err != nil { return err } - if runtimeObj, err = o.deleteResource(info, options); err != nil { + if _, err = o.deleteResource(info, options); err != nil { return err } - if err = o.postDeleteResource(runtimeObj); err != nil { + if err = o.postDeleteResource(info.Object); err != nil { return err } fmt.Fprintf(o.Out, "%s %s deleted\n", info.Mapping.GroupVersionKind.Kind, info.Name) @@ -209,20 +212,21 @@ func (o *DeleteOptions) deleteResource(info *resource.Info, deleteOptions *metav } func (o *DeleteOptions) preDeleteResource(info *resource.Info) error { - if o.PreDeleteHook != nil { - if info.Object == nil { - if err := info.Get(); err != nil { - return err - } + if o.PreDeleteHook == nil { + return nil + } + + if info.Object == nil { + if err := info.Get(); err != nil { + return err } - return o.PreDeleteHook(info.Object) } - return nil + return o.PreDeleteHook(o, info.Object) } func (o *DeleteOptions) postDeleteResource(object runtime.Object) error { if o.PostDeleteHook != nil { - return o.PostDeleteHook(object) + return o.PostDeleteHook(o, object) } return nil } diff --git a/internal/cli/delete/delete_test.go b/internal/cli/delete/delete_test.go index 9959e7ab2..30001727e 100644 --- a/internal/cli/delete/delete_test.go +++ b/internal/cli/delete/delete_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package delete @@ -185,7 +188,7 @@ var _ = Describe("Delete", func() { By("set pre-delete hook") // block cluster deletion - fakePreDeleteHook := func(object runtime.Object) error { + fakePreDeleteHook := func(o *DeleteOptions, object runtime.Object) error { if object.GetObjectKind().GroupVersionKind().Kind == appsv1alpha1.ClusterKind { return fmt.Errorf("fake pre-delete hook error") } else { diff --git a/internal/cli/delete/suite_test.go b/internal/cli/delete/suite_test.go index e0268bd23..690969207 100644 --- a/internal/cli/delete/suite_test.go +++ b/internal/cli/delete/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package delete diff --git a/internal/cli/edit/custom_edit.go b/internal/cli/edit/custom_edit.go new file mode 100644 index 000000000..f0b450684 --- /dev/null +++ b/internal/cli/edit/custom_edit.go @@ -0,0 +1,205 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package edit + +import ( + "bufio" + "bytes" + "fmt" + "io" + "os" + "path/filepath" + "strings" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/yaml" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/editor" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" + "github.com/apecloud/kubeblocks/internal/cli/util/prompt" +) + +// CustomEditOptions is used to edit the resource manifest when creating or updating the resource, +// instead of using -o yaml to output the yaml file before editing the manifest. +type CustomEditOptions struct { + Factory cmdutil.Factory + PrintFlags *genericclioptions.PrintFlags + Method string + TestEnv bool + + genericclioptions.IOStreams +} + +func NewCustomEditOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, method string) *CustomEditOptions { + return &CustomEditOptions{ + Factory: f, + PrintFlags: genericclioptions.NewPrintFlags("").WithDefaultOutput("yaml"), + IOStreams: streams, + Method: method, + TestEnv: false, + } +} + +func (o *CustomEditOptions) Run(originalObj runtime.Object) error { + buf := &bytes.Buffer{} + var ( + original []byte + edited []byte + tmpFile string + w io.Writer = buf + ) + editPrinter, err := o.PrintFlags.ToPrinter() + if err != nil { + return fmt.Errorf("failed to create printer: %v", err) + } + if err := editPrinter.PrintObj(originalObj, w); err != nil { + return fmt.Errorf("failed to print object: %v", err) + } + original = buf.Bytes() + + if !o.TestEnv { + edited, tmpFile, err = editObject(original) + if err != nil { + return fmt.Errorf("failed to lanch editor: %v", err) + } + } else { + edited = original + } + + dynamicClient, err := o.Factory.DynamicClient() + if err != nil { + return fmt.Errorf("failed to get dynamic client: %v", err) + } + // apply validation + fieldValidationVerifier := resource.NewQueryParamVerifier(dynamicClient, o.Factory.OpenAPIGetter(), resource.QueryParamFieldValidation) + schemaValidator, err := o.Factory.Validator(metav1.FieldValidationStrict, fieldValidationVerifier) + if err != nil { + return fmt.Errorf("failed to get validator: %v", err) + } + err = schemaValidator.ValidateBytes(cmdutil.StripComments(edited)) + if err != nil { + return fmt.Errorf("the edited file failed validation: %v", err) + } + + // Compare content without comments + if bytes.Equal(cmdutil.StripComments(original), cmdutil.StripComments(edited)) { + os.Remove(tmpFile) + _, err = fmt.Fprintln(o.ErrOut, "Edit cancelled, no changes made.") + if err != nil { + return fmt.Errorf("error writing to stderr: %v", err) + } + return nil + } + + // Returns an error if comments are included. + lines, err := hasComment(bytes.NewBuffer(edited)) + if err != nil { + return fmt.Errorf("error checking for comment: %v", err) + } + if !lines { + os.Remove(tmpFile) + _, err = fmt.Fprintln(o.ErrOut, "Edit cancelled, saved file was empty.") + if err != nil { + return fmt.Errorf("error writing to stderr: %v", err) + } + } + + decoder := yaml.NewYAMLOrJSONDecoder(bytes.NewReader(edited), len(edited)) + if err := decoder.Decode(originalObj); err != nil { + return fmt.Errorf("failed to decode edited object: %v", err) + } + + if o.Method == "patched" { + diff, err := util.GetUnifiedDiffString(string(original), string(edited)) + if err != nil { + return fmt.Errorf("failed to get diff: %v", err) + } + util.DisplayDiffWithColor(o.IOStreams.Out, diff) + } else if o.Method == "create" { + err := editPrinter.PrintObj(originalObj, o.IOStreams.Out) + if err != nil { + return fmt.Errorf("failed to print object: %v", err) + } + } + return confirmToContinue(o.IOStreams) +} + +func editObject(original []byte) ([]byte, string, error) { + err := addHeader(bytes.NewBuffer(original)) + if err != nil { + return nil, "", err + } + + edit := editor.NewDefaultEditor([]string{ + "KUBE_EDITOR", + "EDITOR", + }) + // launch the editor + edited, tmpFile, err := edit.LaunchTempFile(fmt.Sprintf("%s-edit-", filepath.Base(os.Args[0])), ".yaml", bytes.NewBuffer(original)) + if err != nil { + return nil, "", err + } + + return edited, tmpFile, nil +} + +// HasComment returns true if any line in the provided stream is non empty - has non-whitespace +// characters, or the first non-whitespace character is a '#' indicating a comment. Returns +// any errors encountered reading the stream. +func hasComment(r io.Reader) (bool, error) { + s := bufio.NewScanner(r) + for s.Scan() { + if line := strings.TrimSpace(s.Text()); len(line) > 0 && line[0] != '#' { + return true, nil + } + } + if err := s.Err(); err != nil && err != io.EOF { + return false, err + } + return false, nil +} + +// AddHeader adds a header to the provided writer +func addHeader(w io.Writer) error { + _, err := fmt.Fprint(w, `# Please edit the object below. Lines beginning with a '#' will be ignored, +# and an empty file will abort the edit. If an error occurs while saving this file will be +# reopened with the relevant failures. +# +`) + return err +} + +func confirmToContinue(stream genericclioptions.IOStreams) error { + printer.Warning(stream.Out, "Above resource will be created or changed, do you want to continue to create or change this resource?\n Only 'yes' will be accepted to confirm.\n\n") + entered, _ := prompt.NewPrompt("Enter a value:", nil, stream.In).Run() + if entered != "yes" { + _, err := fmt.Fprintf(stream.Out, "\nCancel resource creation.\n") + if err != nil { + return err + } + return cmdutil.ErrExit + } + return nil +} diff --git a/internal/cli/edit/custom_edit_test.go b/internal/cli/edit/custom_edit_test.go new file mode 100644 index 000000000..e973aa15d --- /dev/null +++ b/internal/cli/edit/custom_edit_test.go @@ -0,0 +1,85 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package edit + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/cli-runtime/pkg/genericclioptions" + clientfake "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" +) + +var _ = Describe("Custom edit", func() { + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + tf.Client = &clientfake.RESTClient{} + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("test edit the cluster resource before updating", func() { + options := NewCustomEditOptions(tf, streams, "patched") + options.TestEnv = true + resObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "test", + }, + "spec": map[string]interface{}{ + "clusterDefinitionRef": "apecloud-mysql", + }, + }, + } + Expect(options.Run(resObj)).Should(Succeed()) + }) + + It("test edit the cluster resource before creating", func() { + options := NewCustomEditOptions(tf, streams, "create") + options.TestEnv = true + resObj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "v1", + "kind": "Namespace", + "metadata": map[string]interface{}{ + "name": "test", + }, + "spec": map[string]interface{}{ + "clusterDefinitionRef": "apecloud-mysql", + }, + }, + } + Expect(options.Run(resObj)).Should(Succeed()) + }) +}) diff --git a/internal/cli/edit/edit.go b/internal/cli/edit/edit.go new file mode 100644 index 000000000..e2be5ae22 --- /dev/null +++ b/internal/cli/edit/edit.go @@ -0,0 +1,75 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package edit + +import ( + "fmt" + + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + "k8s.io/kubectl/pkg/cmd/util/editor" + + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type EditOptions struct { + editor.EditOptions + Factory cmdutil.Factory + // Name is the resource name + Name string + GVR schema.GroupVersionResource +} + +func NewEditOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, + gvr schema.GroupVersionResource) *EditOptions { + return &EditOptions{ + Factory: f, + GVR: gvr, + EditOptions: *editor.NewEditOptions(editor.NormalEditMode, streams), + } +} + +func (o *EditOptions) AddFlags(cmd *cobra.Command) { + // bind flag structs + o.RecordFlags.AddFlags(cmd) + o.PrintFlags.AddFlags(cmd) + + usage := "to use to edit the resource" + cmdutil.AddFilenameOptionFlags(cmd, &o.FilenameOptions, usage) + cmdutil.AddValidateFlags(cmd) + cmd.Flags().BoolVarP(&o.OutputPatch, "output-patch", "", o.OutputPatch, "Output the patch if the resource is edited.") + cmd.Flags().BoolVar(&o.WindowsLineEndings, "windows-line-endings", o.WindowsLineEndings, + "Defaults to the line ending native to your platform.") + cmdutil.AddFieldManagerFlagVar(cmd, &o.FieldManager, "kubectl-edit") + cmdutil.AddApplyAnnotationVarFlags(cmd, &o.ApplyAnnotation) + cmdutil.AddSubresourceFlags(cmd, &o.Subresource, "If specified, edit will operate on the subresource of the requested object.", editor.SupportedSubresources...) +} + +func (o *EditOptions) Complete(cmd *cobra.Command, args []string) error { + if len(args) == 0 { + return fmt.Errorf("missing the name") + } + if len(args) > 0 { + o.Name = args[0] + } + return o.EditOptions.Complete(o.Factory, []string{util.GVRToString(o.GVR), o.Name}, cmd) +} diff --git a/internal/cli/edit/edit_test.go b/internal/cli/edit/edit_test.go new file mode 100644 index 000000000..62b1593a7 --- /dev/null +++ b/internal/cli/edit/edit_test.go @@ -0,0 +1,78 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package edit + +import ( + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/spf13/cobra" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest/fake" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/types" +) + +var _ = Describe("List", func() { + var ( + streams genericclioptions.IOStreams + tf *cmdtesting.TestFactory + ) + + mockClient := func() *corev1.PodList { + pods, _, _ := cmdtesting.TestData() + tf = cmdtesting.NewTestFactory().WithNamespace("test") + defer tf.Cleanup() + + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &fake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Resp: &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, pods)}, + } + return pods + } + + AfterEach(func() { + tf.Cleanup() + }) + + It("test edit", func() { + pods := mockClient() + o := NewEditOptions(tf, streams, schema.GroupVersionResource{Group: "", Resource: "pods", Version: types.K8sCoreAPIVersion}) + cmd := &cobra.Command{ + Use: "edit-test", + Short: "edit test.", + Run: func(cmd *cobra.Command, args []string) { + + }, + } + o.AddFlags(cmd) + podName := pods.Items[0].Name + Expect(o.Complete(cmd, []string{})).Should(MatchError("missing the name")) + Expect(o.Complete(cmd, []string{podName})).ShouldNot(HaveOccurred()) + }) +}) diff --git a/internal/cli/edit/suite_test.go b/internal/cli/edit/suite_test.go new file mode 100644 index 000000000..0d5f0022f --- /dev/null +++ b/internal/cli/edit/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package edit + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestList(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Edit Suite") +} diff --git a/internal/cli/engine/engine_test.go b/internal/cli/engine/engine_test.go deleted file mode 100644 index 81f2254e3..000000000 --- a/internal/cli/engine/engine_test.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 engine - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("Engine", func() { - It("new mysql engine", func() { - typeName := stateMysql - engine, _ := New(typeName) - Expect(engine).ShouldNot(BeNil()) - - url := engine.ConnectCommand() - Expect(len(url)).Should(Equal(3)) - - url = engine.ConnectCommand() - Expect(len(url)).Should(Equal(3)) - - Expect(engine.Container()).Should(Equal("mysql")) - }) - - It("new unknown engine", func() { - typeName := "unknown-type" - engine, err := New(typeName) - Expect(engine).Should(BeNil()) - Expect(err).Should(HaveOccurred()) - }) -}) diff --git a/internal/cli/engine/mysql_test.go b/internal/cli/engine/mysql_test.go deleted file mode 100644 index 8f246fad7..000000000 --- a/internal/cli/engine/mysql_test.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 engine - -import ( - "fmt" - - . "github.com/onsi/ginkgo/v2" -) - -var _ = Describe("Mysql Engine", func() { - It("connection example", func() { - mysql := newMySQL() - - info := &ConnectionInfo{ - User: "user", - Host: "host", - Password: "*****", - Database: "test-db", - Port: "1234", - } - for k := range mysql.examples { - fmt.Printf("%s Connection Example\n", k.String()) - fmt.Println(mysql.ConnectExample(info, k.String())) - } - - fmt.Println(mysql.ConnectExample(info, "")) - }) -}) diff --git a/internal/cli/engine/redis.go b/internal/cli/engine/redis.go deleted file mode 100644 index dc831857d..000000000 --- a/internal/cli/engine/redis.go +++ /dev/null @@ -1,46 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 engine - -type redis struct { - info EngineInfo - examples map[ClientType]buildConnectExample -} - -func newRedis() *redis { - return &redis{ - info: EngineInfo{}, - examples: map[ClientType]buildConnectExample{}, - } -} - -func (r redis) ConnectCommand() []string { - // TODO implement me - panic("implement me") -} - -func (r redis) Container() string { - // TODO implement me - panic("implement me") -} - -func (r redis) ConnectExample(info *ConnectionInfo, client string) string { - // TODO implement me - panic("implement me") -} - -var _ Interface = &redis{} diff --git a/internal/cli/engine/suite_test.go b/internal/cli/engine/suite_test.go deleted file mode 100644 index fa075bc86..000000000 --- a/internal/cli/engine/suite_test.go +++ /dev/null @@ -1,29 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 engine_test - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -func TestEngine(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Engine Suite") -} diff --git a/internal/cli/exec/exec.go b/internal/cli/exec/exec.go index b59b9ca41..24d813d09 100644 --- a/internal/cli/exec/exec.go +++ b/internal/cli/exec/exec.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package exec @@ -62,7 +65,7 @@ func NewExecOptions(f cmdutil.Factory, streams genericclioptions.IOStreams) *Exe } } -// Complete receive exec parameters +// Complete receives exec parameters func (o *ExecOptions) Complete() error { var err error o.Config, err = o.Factory.ToRESTConfig() @@ -87,7 +90,7 @@ func (o *ExecOptions) Complete() error { func (o *ExecOptions) validate() error { var err error - // pod is not get, try to get it by pod name + // pod is not set, try to get it by pod name if o.Pod == nil && len(o.PodName) > 0 { if o.Pod, err = o.Client.CoreV1().Pods(o.Namespace).Get(context.TODO(), o.PodName, metav1.GetOptions{}); err != nil { return err diff --git a/internal/cli/exec/exec_test.go b/internal/cli/exec/exec_test.go index cada2f46f..015556e53 100644 --- a/internal/cli/exec/exec_test.go +++ b/internal/cli/exec/exec_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package exec diff --git a/internal/cli/exec/suite_test.go b/internal/cli/exec/suite_test.go index da8c4d9ff..7533619ca 100644 --- a/internal/cli/exec/suite_test.go +++ b/internal/cli/exec/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package exec diff --git a/internal/cli/list/list.go b/internal/cli/list/list.go index b054c67e0..4af82f4b6 100644 --- a/internal/cli/list/list.go +++ b/internal/cli/list/list.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package list @@ -60,8 +63,8 @@ type ListOptions struct { // print the result or not, if true, use default printer to print, otherwise, // only return the result to caller. - Print bool - + Print bool + SortBy string genericclioptions.IOStreams } @@ -72,15 +75,17 @@ func NewListOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, IOStreams: streams, GVR: gvr, Print: true, + SortBy: ".metadata.name", } } func (o *ListOptions) AddFlags(cmd *cobra.Command, isClusterScope ...bool) { if len(isClusterScope) == 0 || !isClusterScope[0] { - cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespace", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") + cmd.Flags().BoolVarP(&o.AllNamespaces, "all-namespaces", "A", o.AllNamespaces, "If present, list the requested object(s) across all namespaces. Namespace in current context is ignored even if specified with --namespace.") } cmd.Flags().StringVarP(&o.LabelSelector, "selector", "l", o.LabelSelector, "Selector (label query) to filter on, supports '=', '==', and '!='.(e.g. -l key1=value1,key2=value2). Matching objects must satisfy all of the specified label constraints.") cmd.Flags().BoolVar(&o.ShowLabels, "show-labels", false, "When printing, show all labels as the last column (default hide labels column)") + //Todo: --sortBy supports custom field sorting, now `list` is to sort using the `.metadata.name` field in default printer.AddOutputFlag(cmd, &o.Format) } @@ -127,6 +132,7 @@ func (o *ListOptions) Complete() error { } if o.Format.IsHumanReadable() { + p = &cmdget.SortingPrinter{Delegate: p, SortField: o.SortBy} p = &cmdget.TablePrinter{Delegate: p} } return p.PrintObj, nil @@ -175,6 +181,9 @@ func (o *ListOptions) transformRequests(req *rest.Request) { fmt.Sprintf("application/json;as=Table;v=%s;g=%s", metav1beta1.SchemeGroupVersion.Version, metav1beta1.GroupName), "application/json", }, ",")) + if len(o.SortBy) > 0 { + req.Param("includeObject", "Object") + } } func (o *ListOptions) printResult(r *resource.Result) error { diff --git a/internal/cli/list/list_test.go b/internal/cli/list/list_test.go index f4e3b8b84..9b54e73ac 100644 --- a/internal/cli/list/list_test.go +++ b/internal/cli/list/list_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package list @@ -100,7 +103,7 @@ bar test foo test bar ` - _ = cmd.Flags().Set("all-namespace", "true") + _ = cmd.Flags().Set("all-namespaces", "true") cmd.Run(cmd, []string{}) Expect(buf.String()).To(Equal(expected)) }) diff --git a/internal/cli/list/suite_test.go b/internal/cli/list/suite_test.go index d4f3fa80a..958d5b313 100644 --- a/internal/cli/list/suite_test.go +++ b/internal/cli/list/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package list diff --git a/internal/cli/patch/patch.go b/internal/cli/patch/patch.go index a021fb1e6..eeadffced 100644 --- a/internal/cli/patch/patch.go +++ b/internal/cli/patch/patch.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package patch @@ -26,6 +29,7 @@ import ( "github.com/spf13/cobra" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" @@ -38,6 +42,7 @@ import ( "k8s.io/client-go/kubernetes/scheme" cmdutil "k8s.io/kubectl/pkg/cmd/util" + "github.com/apecloud/kubeblocks/internal/cli/edit" "github.com/apecloud/kubeblocks/internal/cli/util" ) @@ -51,7 +56,7 @@ type Options struct { GVR schema.GroupVersionResource OutputOperation OutputOperation - // follow fields are similar to kubectl patch + // following fields are similar to kubectl patch PrintFlags *genericclioptions.PrintFlags ToPrinter func(string) (printers.ResourcePrinter, error) Patch string @@ -66,6 +71,8 @@ type Options struct { unstructuredClientForMapping func(mapping *meta.RESTMapping) (resource.RESTClient, error) fieldManager string + EditBeforeUpdate bool + genericclioptions.IOStreams } @@ -82,6 +89,7 @@ func NewOptions(f cmdutil.Factory, streams genericclioptions.IOStreams, gvr sche func (o *Options) AddFlags(cmd *cobra.Command) { o.PrintFlags.AddFlags(cmd) cmdutil.AddDryRunFlag(cmd) + cmd.Flags().BoolVar(&o.EditBeforeUpdate, "edit", o.EditBeforeUpdate, "Edit the API resource") } func (o *Options) complete(cmd *cobra.Command) error { @@ -167,13 +175,38 @@ func (o *Options) Run(cmd *cobra.Command) error { NewHelper(client, mapping). DryRun(o.dryRunStrategy == cmdutil.DryRunServer). WithFieldManager(o.fieldManager). - WithSubresource(o.Subresource) + WithSubresource(o.Subresource). + WithFieldValidation(metav1.FieldValidationStrict) patchedObj, err := helper.Patch(namespace, name, patchType, patchBytes, nil) if err != nil { if apierrors.IsUnsupportedMediaType(err) { return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind)) } - return err + return fmt.Errorf("unable to update %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err) + } + + if o.EditBeforeUpdate { + customEdit := edit.NewCustomEditOptions(o.Factory, o.IOStreams, "patched") + if err = customEdit.Run(patchedObj); err != nil { + return fmt.Errorf("unable to edit %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err) + } + patchedObj = &unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": patchedObj.(*unstructured.Unstructured).Object["metadata"], + "spec": patchedObj.(*unstructured.Unstructured).Object["spec"], + }, + } + patchBytes, err = patchedObj.(*unstructured.Unstructured).MarshalJSON() + if err != nil { + return fmt.Errorf("unable to marshal %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err) + } + patchedObj, err = helper.Patch(namespace, name, patchType, patchBytes, nil) + if err != nil { + if apierrors.IsUnsupportedMediaType(err) { + return errors.Wrap(err, fmt.Sprintf("%s is not supported by %s", patchType, mapping.GroupVersionKind)) + } + return fmt.Errorf("unable to update %s %s/%s: %v", info.Mapping.GroupVersionKind.Kind, info.Namespace, info.Name, err) + } } didPatch := !reflect.DeepEqual(info.Object, patchedObj) diff --git a/internal/cli/patch/patch_test.go b/internal/cli/patch/patch_test.go index d422904fe..987f04546 100644 --- a/internal/cli/patch/patch_test.go +++ b/internal/cli/patch/patch_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package patch diff --git a/internal/cli/patch/suite_test.go b/internal/cli/patch/suite_test.go index e0ed40d36..1cd0d5bfe 100644 --- a/internal/cli/patch/suite_test.go +++ b/internal/cli/patch/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package patch diff --git a/internal/cli/printer/describe.go b/internal/cli/printer/describe.go index 2c176a83b..268d57e14 100644 --- a/internal/cli/printer/describe.go +++ b/internal/cli/printer/describe.go @@ -1,25 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer import ( + "encoding/json" "fmt" "io" + "reflect" + "gopkg.in/yaml.v2" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -71,19 +77,19 @@ func PrintComponentConfigMeta(tplInfos []types.ConfigTemplateInfo, clusterName, } tbl := NewTablePrinter(out) PrintTitle("ConfigSpecs Meta") - enableReconfiguring := func(tpl appsv1alpha1.ComponentConfigSpec, key string) string { - if len(tpl.ConfigConstraintRef) > 0 && cfgcore.CheckConfigTemplateReconfigureKey(tpl, key) { + enableReconfiguring := func(tpl appsv1alpha1.ComponentConfigSpec, configFileKey string) string { + if len(tpl.ConfigConstraintRef) > 0 && cfgcore.IsSupportConfigFileReconfigure(tpl, configFileKey) { return "true" } return "false" } tbl.SetHeader("CONFIG-SPEC-NAME", "FILE", "ENABLED", "TEMPLATE", "CONSTRAINT", "RENDERED", "COMPONENT", "CLUSTER") for _, info := range tplInfos { - for key := range info.CMObj.Data { + for configFileKey := range info.CMObj.Data { tbl.AddRow( BoldYellow(info.Name), - key, - BoldYellow(enableReconfiguring(info.TPL, key)), + configFileKey, + BoldYellow(enableReconfiguring(info.TPL, configFileKey)), info.TPL.TemplateRef, info.TPL.ConfigConstraintRef, info.CMObj.Name, @@ -93,3 +99,49 @@ func PrintComponentConfigMeta(tplInfos []types.ConfigTemplateInfo, clusterName, } tbl.Print() } + +// PrintHelmValues prints the helm values file of the release in specified format, supports JSON、YAML and Table +func PrintHelmValues(configs map[string]interface{}, format Format, out io.Writer) { + inTable := func() { + p := NewTablePrinter(out) + p.SetHeader("KEY", "VALUE") + p.SortBy(1) + for key, value := range configs { + addRows(key, value, p, true) // to table + } + p.Print() + } + if format.IsHumanReadable() { + inTable() + return + } + + var data []byte + if format == YAML { + data, _ = yaml.Marshal(configs) + } else { + data, _ = json.MarshalIndent(configs, "", " ") + data = append(data, '\n') + } + fmt.Fprint(out, string(data)) +} + +// addRows parses the interface value and add it to the Table +func addRows(key string, value interface{}, p *TablePrinter, ori bool) { + if value == nil { + p.AddRow(key, value) + return + } + if reflect.TypeOf(value).Kind() == reflect.Map && ori { + if len(value.(map[string]interface{})) == 0 { + data, _ := json.Marshal(value) + p.AddRow(key, string(data)) + } + for k, v := range value.(map[string]interface{}) { + addRows(key+"."+k, v, p, false) + } + } else { + data, _ := json.Marshal(value) + p.AddRow(key, string(data)) + } +} diff --git a/internal/cli/printer/describe_test.go b/internal/cli/printer/describe_test.go index 97089930f..63ab64f4d 100644 --- a/internal/cli/printer/describe_test.go +++ b/internal/cli/printer/describe_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer @@ -88,3 +91,33 @@ func TestPrintConditions(t *testing.T) { t.Fatalf(`Expect conditions output: "%s %s %s %s"`, conditionType, reason, metav1.ConditionFalse, message) } } + +func TestPrintHelmValues(t *testing.T) { + mockHelmConfig := map[string]interface{}{ + "updateStrategy": map[string]interface{}{ + "rollingUpdate": map[string]interface{}{ + "maxSurge": 1, + "maxUnavailable": "40%", + }, + "type": "RollingUpdate", + }, + "podDisruptionBudget": map[string]interface{}{ + "minAvailable": 1, + }, + "loggerSettings": map[string]interface{}{ + "developmentMode": false, + "encoder": "console", + "timeEncoding": "iso8601", + }, + "priorityClassName": nil, + "nameOverride": "", + "fullnameOverride±": "", + "dnsPolicy": "ClusterFirst", + "replicaCount": 1, + "hostNetwork": false, + "keepAddons": false, + } + out := &bytes.Buffer{} + + PrintHelmValues(mockHelmConfig, JSON, out) +} diff --git a/internal/cli/printer/format.go b/internal/cli/printer/format.go old mode 100644 new mode 100755 index b63a3960e..ea2f2f043 --- a/internal/cli/printer/format.go +++ b/internal/cli/printer/format.go @@ -1,26 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer import ( "fmt" + "os" "strings" "github.com/spf13/cobra" + "k8s.io/klog/v2" "k8s.io/kubectl/pkg/cmd/util" ) @@ -88,6 +93,10 @@ func AddOutputFlag(cmd *cobra.Command, varRef *Format) { })) } +func AddOutputFlagForCreate(cmd *cobra.Command, varRef *Format) { + cmd.Flags().VarP(newOutputValue(YAML, varRef), "output", "o", "prints the output in the specified format. Allowed values: JSON and YAML") +} + type outputValue Format func newOutputValue(defaultValue Format, p *Format) *outputValue { @@ -111,3 +120,18 @@ func (o *outputValue) Set(s string) error { *o = outputValue(outfmt) return nil } + +// FatalWithRedColor when an error occurs, sets the red color to print it. +func FatalWithRedColor(msg string, code int) { + if klog.V(99).Enabled() { + klog.FatalDepth(2, msg) + } + if len(msg) > 0 { + // add newline if needed + if !strings.HasSuffix(msg, "\n") { + msg += "\n" + } + fmt.Fprint(os.Stderr, BoldRed(msg)) + } + os.Exit(code) +} diff --git a/internal/cli/printer/format_test.go b/internal/cli/printer/format_test.go index 2a3d16511..ef133ea14 100644 --- a/internal/cli/printer/format_test.go +++ b/internal/cli/printer/format_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer diff --git a/internal/cli/printer/helper.go b/internal/cli/printer/helper.go index 02c4cf64d..b54bb5a46 100644 --- a/internal/cli/printer/helper.go +++ b/internal/cli/printer/helper.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer diff --git a/internal/cli/printer/printer.go b/internal/cli/printer/printer.go index cf8b01736..00467a169 100644 --- a/internal/cli/printer/printer.go +++ b/internal/cli/printer/printer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer @@ -108,10 +111,31 @@ func (t *TablePrinter) AddRow(row ...interface{}) { } func (t *TablePrinter) Print() { + if t == nil || t.Tbl == nil { + return + } t.Tbl.Render() } -// PrintPairStringToLine print pair string for a line , the format is as follows "*:\t". +// SortBy sorts the table alphabetically by the column you specify, it will be sorted by the first table column in default. +// The columnNumber index starts from 1 +func (t *TablePrinter) SortBy(columnNumber ...int) { + if len(columnNumber) == 0 { + t.Tbl.SortBy([]table.SortBy{ + { + Number: 1, + }, + }) + return + } + res := make([]table.SortBy, len(columnNumber)) + for i := range columnNumber { + res[i].Number = columnNumber[i] + } + t.Tbl.SortBy(res) +} + +// PrintPairStringToLine prints pair string for a line , the format is as follows "*:\t". // spaceCount is the space character count which is placed in the offset of field string. // the default values of tabCount is 2. func PrintPairStringToLine(name, value string, spaceCount ...int) { @@ -154,7 +178,7 @@ func PrintLine(line string) { fmt.Println(line) } -// PrintBlankLine print a blank line +// PrintBlankLine prints a blank line func PrintBlankLine(out io.Writer) { if out == nil { out = os.Stdout diff --git a/internal/cli/printer/printer_test.go b/internal/cli/printer/printer_test.go index 27e7d0410..a7d845a3a 100644 --- a/internal/cli/printer/printer_test.go +++ b/internal/cli/printer/printer_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package printer @@ -109,3 +112,25 @@ func checkOutPut(t *testing.T, captureFunc func() (string, error), expect string } assert.Equal(t, expect, capturedOutput) } + +func TestSort(t *testing.T) { + printer := NewTablePrinter(os.Stdout) + headerRow := make([]interface{}, len(header)) + for i, h := range header { + headerRow[i] = h + } + printer.SetHeader(headerRow...) + printer.SortBy(1) + for _, r := range [][]string{ + {"cedar51", "default", "apecloud-mysql", "ac-mysql-8.0.30", "Delete", "Feb 20,2023 16:39 UTC+0800"}, + {"brier63", "default", "apecloud-mysql", "ac-mysql-8.0.30", "Delete", "Feb 20,2023 16:39 UTC+0800"}, + {"alpha19", "default", "apecloud-mysql", "ac-mysql-8.0.30", "Delete", "Feb 20,2023 16:39 UTC+0800"}, + } { + row := make([]interface{}, len(r)) + for i, rr := range r { + row[i] = rr + } + printer.AddRow(row...) + } + printer.Print() +} diff --git a/internal/cli/printer/spinner.go b/internal/cli/printer/spinner.go deleted file mode 100644 index e7f23951d..000000000 --- a/internal/cli/printer/spinner.go +++ /dev/null @@ -1,60 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 printer - -import ( - "fmt" - "io" - "runtime" - "sync" - "time" - - "github.com/briandowns/spinner" - - "github.com/apecloud/kubeblocks/internal/cli/types" -) - -func Spinner(w io.Writer, fmtstr string, a ...any) func(result bool) { - msg := fmt.Sprintf(fmtstr, a...) - var once sync.Once - var s *spinner.Spinner - - if runtime.GOOS == types.GoosWindows { - fmt.Fprintf(w, "%s\n", msg) - return func(result bool) {} - } else { - s = spinner.New(spinner.CharSets[11], 100*time.Millisecond) - s.Writer = w - s.HideCursor = true - _ = s.Color("cyan") - s.Suffix = fmt.Sprintf(" %s", msg) - s.Start() - } - - return func(result bool) { - once.Do(func() { - if s != nil { - s.Stop() - } - if result { - fmt.Fprintf(w, "%s %s\n", msg, BoldGreen("OK")) - } else { - fmt.Fprintf(w, "%s %s\n", msg, BoldRed("FAIL")) - } - }) - } -} diff --git a/internal/cli/printer/spinner_test.go b/internal/cli/printer/spinner_test.go deleted file mode 100644 index 0de16208a..000000000 --- a/internal/cli/printer/spinner_test.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 printer - -import ( - "os" - - . "github.com/onsi/ginkgo/v2" -) - -var _ = Describe("Spinner", func() { - It("Test Spinner", func() { - spinner := Spinner(os.Stdout, "spinner test ... ") - spinner(true) - - spinner = Spinner(os.Stdout, "spinner test ... ") - spinner(false) - }) -}) diff --git a/internal/cli/spinner/spinner.go b/internal/cli/spinner/spinner.go new file mode 100644 index 000000000..c84699ec3 --- /dev/null +++ b/internal/cli/spinner/spinner.go @@ -0,0 +1,156 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package spinner + +import ( + "fmt" + "io" + "os" + "os/signal" + "strings" + "syscall" + "time" + + "github.com/briandowns/spinner" + + "github.com/apecloud/kubeblocks/internal/cli/printer" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +type Spinner struct { + s *spinner.Spinner + delay time.Duration + cancel chan struct{} +} + +type Interface interface { + Start() + Done(status string) + Success() + Fail() + SetMessage(msg string) + SetFinalMsg(msg string) + updateSpinnerMessage(msg string) +} + +type Option func(Interface) + +func WithMessage(msg string) Option { + return func(s Interface) { + s.updateSpinnerMessage(msg) + } +} + +func (s *Spinner) updateSpinnerMessage(msg string) { + s.s.Suffix = fmt.Sprintf(" %s", msg) +} + +func (s *Spinner) SetMessage(msg string) { + s.updateSpinnerMessage(msg) + if !s.s.Active() { + s.Start() + } +} + +func (s *Spinner) Start() { + if s.cancel != nil { + return + } + if s.delay == 0 { + s.s.Start() + return + } + s.cancel = make(chan struct{}, 1) + go func() { + select { + case <-s.cancel: + return + case <-time.After(s.delay): + s.s.Start() + s.cancel = nil + } + time.Sleep(50 * time.Millisecond) + }() +} + +func (s *Spinner) Done(status string) { + if s.cancel != nil { + close(s.cancel) + } + s.stop(status) +} + +func (s *Spinner) SetFinalMsg(msg string) { + s.s.FinalMSG = msg + if !s.s.Active() { + s.Start() + } +} + +func (s *Spinner) stop(status string) { + if s.s == nil { + return + } + + if status != "" { + s.s.FinalMSG = fmt.Sprintf("%s %s\n", strings.TrimPrefix(s.s.Suffix, " "), status) + } + s.s.Stop() + + // show cursor in terminal. + fmt.Fprintf(s.s.Writer, "\033[?25h") +} + +func (s *Spinner) Success() { + s.Done(printer.BoldGreen("OK")) +} + +func (s *Spinner) Fail() { + s.Done(printer.BoldRed("FAIL")) +} + +func New(w io.Writer, opts ...Option) Interface { + if util.IsWindows() { + return NewWindowsSpinner(w, opts...) + } + + res := &Spinner{} + res.s = spinner.New(spinner.CharSets[11], + 100*time.Millisecond, + spinner.WithWriter(w), + spinner.WithHiddenCursor(true), + spinner.WithColor("cyan"), + ) + + for _, opt := range opts { + opt(res) + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + // Capture the interrupt signal, make the `spinner` program exit gracefully, and prevent the cursor from disappearing. + go func() { + <-c + res.Done("") + os.Exit(0) + }() + res.Start() + return res +} diff --git a/internal/cli/spinner/spinner_test.go b/internal/cli/spinner/spinner_test.go new file mode 100644 index 000000000..a678e43bf --- /dev/null +++ b/internal/cli/spinner/spinner_test.go @@ -0,0 +1,36 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package spinner + +import ( + "os" + + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("Spinner", func() { + It("Test Spinner", func() { + s := New(os.Stdout, WithMessage("spinner test ... ")) + s.Success() + + s = New(os.Stdout, WithMessage("spinner test ... ")) + s.Fail() + }) +}) diff --git a/internal/cli/spinner/windows_spinner.go b/internal/cli/spinner/windows_spinner.go new file mode 100644 index 000000000..94136cf52 --- /dev/null +++ b/internal/cli/spinner/windows_spinner.go @@ -0,0 +1,161 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package spinner + +import ( + "fmt" + "io" + "os" + "os/signal" + "strings" + "sync" + "syscall" + + "time" + + "github.com/apecloud/kubeblocks/internal/cli/printer" +) + +var char = []string{"⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"} + +type WindowsSpinner struct { // no thread/goroutine safe + msg string + lastOutput string + FinalMSG string + active bool + chars []string + cancel chan struct{} + Writer io.Writer + delay time.Duration + mu *sync.RWMutex +} + +func NewWindowsSpinner(w io.Writer, opts ...Option) *WindowsSpinner { + res := &WindowsSpinner{ + chars: char, + active: false, + cancel: make(chan struct{}, 1), + Writer: w, + mu: &sync.RWMutex{}, + delay: 100 * time.Millisecond, + } + for _, opt := range opts { + opt(res) + } + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt, syscall.SIGTERM) + go func() { + <-c + res.Done("") + os.Exit(0) + }() + res.Start() + return res +} + +func (s *WindowsSpinner) updateSpinnerMessage(msg string) { + s.msg = fmt.Sprintf(" %s", msg) +} + +func (s *WindowsSpinner) Done(status string) { + if status != "" { + s.FinalMSG = fmt.Sprintf("%s %s\n", strings.TrimPrefix(s.msg, " "), status) + } + s.stop() +} + +func (s *WindowsSpinner) Success() { + if len(s.msg) == 0 { + return + } + s.Done(printer.BoldGreen("OK")) + +} + +func (s *WindowsSpinner) Fail() { + if len(s.msg) == 0 { + return + } + s.Done(printer.BoldRed("FAIL")) +} + +func (s *WindowsSpinner) Start() { + s.active = true + + go func() { + for { + for i := 0; i < len(s.chars); i++ { + select { + case <-s.cancel: + return + default: + s.mu.Lock() + if !s.active { + defer s.mu.Unlock() + return + } + outPlain := fmt.Sprintf("\r%s%s", s.chars[i], s.msg) + s.erase() + s.lastOutput = outPlain + fmt.Fprint(s.Writer, outPlain) + s.mu.Unlock() + time.Sleep(s.delay) + } + } + } + }() +} + +func (s *WindowsSpinner) SetMessage(msg string) { + s.mu.Lock() + defer s.mu.Unlock() + s.msg = msg +} + +func (s *WindowsSpinner) SetFinalMsg(msg string) { + s.FinalMSG = msg +} + +// remove lastOutput +func (s *WindowsSpinner) erase() { + split := strings.Split(s.lastOutput, "\n") + for i := 0; i < len(split); i++ { + if i > 0 { + fmt.Fprint(s.Writer, "\033[A") + } + fmt.Fprint(s.Writer, "\r\033[K") + } +} + +// stop stops the indicator. +func (s *WindowsSpinner) stop() { + s.mu.Lock() + defer s.mu.Unlock() + if s.active { + s.active = false + if s.FinalMSG != "" { + s.erase() + fmt.Fprint(s.Writer, s.FinalMSG) + } + s.cancel <- struct{}{} + close(s.cancel) + } +} diff --git a/internal/cli/testing/client.go b/internal/cli/testing/client.go index cd8551be8..147d0e2c9 100644 --- a/internal/cli/testing/client.go +++ b/internal/cli/testing/client.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/cli/testing/factory.go b/internal/cli/testing/factory.go index b4d658a80..c72cb20f1 100644 --- a/internal/cli/testing/factory.go +++ b/internal/cli/testing/factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing @@ -30,7 +33,7 @@ import ( cmdutil "k8s.io/kubectl/pkg/cmd/util" ) -// NewTestFactory like cmdtesting.NewTestFactory, register KubeBlocks custom objects +// NewTestFactory is like cmdtesting.NewTestFactory, registers KubeBlocks custom objects func NewTestFactory(namespace string) *cmdtesting.TestFactory { tf := cmdtesting.NewTestFactory() mapper := restmapper.NewDiscoveryRESTMapper(testDynamicResources()) @@ -113,6 +116,9 @@ func testDynamicResources() []*restmapper.APIGroupResources { VersionedResources: map[string][]metav1.APIResource{ "v1alpha1": { {Name: "clusters", Namespaced: true, Kind: "Cluster"}, + {Name: "clusterdefinitions", Namespaced: false, Kind: "clusterdefinition"}, + {Name: "clusterversions", Namespaced: false, Kind: "clusterversion"}, + {Name: "opsrequests", Namespaced: true, Kind: "OpsRequest"}, }, }, }, diff --git a/internal/cli/testing/factory_test.go b/internal/cli/testing/factory_test.go index 8eccdcc64..891f82f60 100644 --- a/internal/cli/testing/factory_test.go +++ b/internal/cli/testing/factory_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/cli/testing/fake.go b/internal/cli/testing/fake.go index 1461d4f7f..c1ca8712f 100644 --- a/internal/cli/testing/fake.go +++ b/internal/cli/testing/fake.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing @@ -23,9 +26,14 @@ import ( snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" "github.com/sethvargo/go-password/password" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" + storagev1 "k8s.io/api/storage/v1" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/kubectl/pkg/util/storage" "k8s.io/utils/pointer" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -33,27 +41,32 @@ import ( extensionsv1alpha1 "github.com/apecloud/kubeblocks/apis/extensions/v1alpha1" "github.com/apecloud/kubeblocks/internal/cli/types" "github.com/apecloud/kubeblocks/internal/constant" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) const ( - ClusterName = "fake-cluster-name" - Namespace = "fake-namespace" - ClusterVersionName = "fake-cluster-version" - ClusterDefName = "fake-cluster-definition" - ComponentName = "fake-component-name" - ComponentDefName = "fake-component-type" - NodeName = "fake-node-name" - SecretName = "fake-secret-conn-credential" - StorageClassName = "fake-storage-class" - PVCName = "fake-pvc" - GeneralClassFamily = "kb-class-family-general" - MemoryOptimizedClassFamily = "kb-class-family-memory-optimized" + ClusterName = "fake-cluster-name" + Namespace = "fake-namespace" + ClusterVersionName = "fake-cluster-version" + ClusterDefName = "fake-cluster-definition" + ComponentName = "fake-component-name" + ComponentDefName = "fake-component-type" + NodeName = "fake-node-name" + SecretName = "fake-secret-conn-credential" + StorageClassName = "fake-storage-class" + PVCName = "fake-pvc" KubeBlocksRepoName = "fake-kubeblocks-repo" KubeBlocksChartName = "fake-kubeblocks" KubeBlocksChartURL = "fake-kubeblocks-chart-url" BackupToolName = "fake-backup-tool" - BackupTemplateName = "fake-backup-policy-template" + + IsDefautl = true + IsNotDefault = false +) + +var ( + ExtraComponentDefName = fmt.Sprintf("%s-%d", ComponentDefName, 1) ) func GetRandomStr() string { @@ -246,32 +259,54 @@ func FakeClusterDef() *appsv1alpha1.ClusterDefinition { PasswordConfig: appsv1alpha1.PasswordConfig{}, Accounts: []appsv1alpha1.SystemAccountConfig{}, }, + ConfigSpecs: []appsv1alpha1.ComponentConfigSpec{ + { + ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ + Name: "mysql-consensusset-config", + TemplateRef: "mysql8.0-config-template", + Namespace: Namespace, + VolumeName: "mysql-config", + }, + ConfigConstraintRef: "mysql8.0-config-constraints", + }, + }, }, { - Name: fmt.Sprintf("%s-%d", ComponentDefName, 1), + Name: ExtraComponentDefName, CharacterType: "mysql", + ConfigSpecs: []appsv1alpha1.ComponentConfigSpec{ + { + ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ + Name: "mysql-consensusset-config", + TemplateRef: "mysql8.0-config-template", + Namespace: Namespace, + VolumeName: "mysql-config", + }, + ConfigConstraintRef: "mysql8.0-config-constraints", + }, + }, }, } return clusterDef } -func FakeComponentClassDef(clusterDef *appsv1alpha1.ClusterDefinition, def []byte) *corev1.ConfigMapList { - result := &corev1.ConfigMapList{} - cm := &corev1.ConfigMap{} - cm.Name = fmt.Sprintf("fake-kubeblocks-classes-%s", ComponentName) - cm.SetLabels(map[string]string{ - types.ClassLevelLabelKey: "component", - constant.KBAppComponentDefRefLabelKey: ComponentDefName, - types.ClassProviderLabelKey: "kubeblocks", - constant.ClusterDefLabelKey: clusterDef.Name, - }) - cm.Data = map[string]string{"families-20230223162700": string(def)} - result.Items = append(result.Items, *cm) - return result +func FakeComponentClassDef(name string, clusterDefRef string, componentDefRef string) *appsv1alpha1.ComponentClassDefinition { + constraint := testapps.NewComponentResourceConstraintFactory(testapps.DefaultResourceConstraintName). + AddConstraints(testapps.GeneralResourceConstraint). + GetObject() + + componentClassDefinition := testapps.NewComponentClassDefinitionFactory(name, clusterDefRef, componentDefRef). + AddClasses(constraint.Name, []string{testapps.Class1c1gName, testapps.Class2c4gName}). + GetObject() + + return componentClassDefinition } func FakeClusterVersion() *appsv1alpha1.ClusterVersion { cv := &appsv1alpha1.ClusterVersion{} + gvr := types.ClusterVersionGVR() + cv.TypeMeta.APIVersion = gvr.GroupVersion().String() + cv.TypeMeta.Kind = types.KindClusterVersion cv.Name = ClusterVersionName cv.SetLabels(map[string]string{ constant.ClusterDefLabelKey: ClusterDefName, @@ -288,14 +323,24 @@ func FakeBackupTool() *dpv1alpha1.BackupTool { return tool } -func FakeBackupPolicyTemplate() *dpv1alpha1.BackupPolicyTemplate { - template := &dpv1alpha1.BackupPolicyTemplate{ +func FakeBackupPolicy(backupPolicyName, clusterName string) *dpv1alpha1.BackupPolicy { + template := &dpv1alpha1.BackupPolicy{ TypeMeta: metav1.TypeMeta{ APIVersion: fmt.Sprintf("%s/%s", types.DPAPIGroup, types.DPAPIVersion), - Kind: types.KindBackupPolicyTemplate, + Kind: types.KindBackupPolicy, }, ObjectMeta: metav1.ObjectMeta{ - Name: BackupTemplateName, + Name: backupPolicyName, + Namespace: Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: clusterName, + }, + Annotations: map[string]string{ + constant.DefaultBackupPolicyAnnotationKey: "true", + }, + }, + Status: dpv1alpha1.BackupPolicyStatus{ + Phase: dpv1alpha1.PolicyAvailable, }, } return template @@ -467,3 +512,302 @@ func FakeAddon(name string) *extensionsv1alpha1.Addon { addon.SetCreationTimestamp(metav1.Now()) return addon } + +func FakeConfigMap(cmName string) *corev1.ConfigMap { + cm := &corev1.ConfigMap{ + TypeMeta: metav1.TypeMeta{ + APIVersion: "v1", + Kind: "ConfigMap", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: cmName, + Namespace: Namespace, + }, + Data: map[string]string{ + "fake": "fake", + }, + } + return cm +} + +func FakeConfigConstraint(ccName string) *appsv1alpha1.ConfigConstraint { + cm := &appsv1alpha1.ConfigConstraint{ + ObjectMeta: metav1.ObjectMeta{ + Name: ccName, + }, + Spec: appsv1alpha1.ConfigConstraintSpec{ + FormatterConfig: &appsv1alpha1.FormatterConfig{}, + }, + } + return cm +} + +func FakeStorageClass(name string, isDefault bool) *storagev1.StorageClass { + storageClassObj := &storagev1.StorageClass{ + TypeMeta: metav1.TypeMeta{ + Kind: "StorageClass", + APIVersion: "storage.k8s.io/v1", + }, + ObjectMeta: metav1.ObjectMeta{ + Name: name, + }, + } + if isDefault { + storageClassObj.ObjectMeta.Annotations = make(map[string]string) + storageClassObj.ObjectMeta.Annotations[storage.IsDefaultStorageClassAnnotation] = "true" + } + return storageClassObj +} + +func FakeServiceAccount(name string) *corev1.ServiceAccount { + return &corev1.ServiceAccount{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + constant.AppNameLabelKey: KubeBlocksChartName}, + }, + } +} + +func FakeClusterRole(name string) *rbacv1.ClusterRole { + return &rbacv1.ClusterRole{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + constant.AppNameLabelKey: KubeBlocksChartName}, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + }, + } +} + +func FakeClusterRoleBinding(name string, sa *corev1.ServiceAccount, clusterRole *rbacv1.ClusterRole) *rbacv1.ClusterRoleBinding { + return &rbacv1.ClusterRoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + constant.AppNameLabelKey: KubeBlocksChartName}, + }, + RoleRef: rbacv1.RoleRef{ + Kind: clusterRole.Kind, + Name: clusterRole.Name, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + } +} + +func FakeRole(name string) *rbacv1.Role { + return &rbacv1.Role{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + constant.AppNameLabelKey: KubeBlocksChartName}, + }, + Rules: []rbacv1.PolicyRule{ + { + APIGroups: []string{"*"}, + Resources: []string{"*"}, + Verbs: []string{"*"}, + }, + }, + } +} + +func FakeRoleBinding(name string, sa *corev1.ServiceAccount, role *rbacv1.Role) *rbacv1.RoleBinding { + return &rbacv1.RoleBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + constant.AppNameLabelKey: KubeBlocksChartName}, + }, + RoleRef: rbacv1.RoleRef{ + Kind: role.Kind, + Name: role.Name, + }, + Subjects: []rbacv1.Subject{ + { + Kind: "ServiceAccount", + Name: sa.Name, + Namespace: sa.Namespace, + }, + }, + } +} + +func FakeDeploy(name string, namespace string, extraLabels map[string]string) *appsv1.Deployment { + labels := map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + } + // extraLabels will override the labels above if there is a conflict + for k, v := range extraLabels { + labels[k] = v + } + labels["app"] = name + + return &appsv1.Deployment{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.DeploymentSpec{ + Replicas: pointer.Int32(1), + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + }, + }, + } +} + +func FakeStatefulSet(name string, namespace string, extraLabels map[string]string) *appsv1.StatefulSet { + labels := map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + } + // extraLabels will override the labels above if there is a conflict + for k, v := range extraLabels { + labels[k] = v + } + labels["app"] = name + return &appsv1.StatefulSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: appsv1.StatefulSetSpec{ + Replicas: pointer.Int32(1), + Selector: &metav1.LabelSelector{ + MatchLabels: labels, + }, + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + }, + }, + Status: appsv1.StatefulSetStatus{ + Replicas: 1, + }, + } +} + +func FakePodForSts(sts *appsv1.StatefulSet) *corev1.PodList { + pods := &corev1.PodList{} + for i := 0; i < int(*sts.Spec.Replicas); i++ { + pod := &corev1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%d", sts.Name, i), + Namespace: sts.Namespace, + Labels: sts.Spec.Template.Labels, + }, + Spec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: sts.Name, + Image: "fake-image", + }, + }, + }, + Status: corev1.PodStatus{ + Phase: corev1.PodRunning, + }, + } + pods.Items = append(pods.Items, *pod) + } + return pods +} + +func FakeJob(name string, namespace string, extraLabels map[string]string) *batchv1.Job { + labels := map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + } + // extraLabels will override the labels above if there is a conflict + for k, v := range extraLabels { + labels[k] = v + } + labels["app"] = name + + return &batchv1.Job{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: batchv1.JobSpec{ + Completions: pointer.Int32(1), + Template: corev1.PodTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + }, + }, + Status: batchv1.JobStatus{ + Active: 1, + Ready: pointer.Int32(1), + }, + } +} + +func FakeCronJob(name string, namespace string, extraLabels map[string]string) *batchv1.CronJob { + labels := map[string]string{ + constant.AppInstanceLabelKey: types.KubeBlocksReleaseName, + } + // extraLabels will override the labels above if there is a conflict + for k, v := range extraLabels { + labels[k] = v + } + labels["app"] = name + + return &batchv1.CronJob{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: namespace, + Labels: labels, + }, + Spec: batchv1.CronJobSpec{ + Schedule: "*/1 * * * *", + JobTemplate: batchv1.JobTemplateSpec{ + ObjectMeta: metav1.ObjectMeta{ + Labels: labels, + }, + }, + }, + } +} + +func FakeResourceNotFound(versionResource schema.GroupVersionResource, name string) *metav1.Status { + return &metav1.Status{ + TypeMeta: metav1.TypeMeta{ + Kind: "Status", + APIVersion: "v1", + }, + Status: "Failure", + Message: fmt.Sprintf("%s.%s \"%s\" not found", versionResource.Resource, versionResource.Group, name), + Reason: "NotFound", + Details: nil, + Code: 404, + } +} diff --git a/internal/cli/testing/fake_test.go b/internal/cli/testing/fake_test.go index ce2e20f1d..8e6f308fc 100644 --- a/internal/cli/testing/fake_test.go +++ b/internal/cli/testing/fake_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing @@ -83,4 +86,11 @@ var _ = Describe("test fake", func() { events := FakeEvents() Expect(events).ShouldNot(BeNil()) }) + + It("fake storageClass", func() { + StorageClassDefault := FakeStorageClass(StorageClassName, IsDefautl) + Expect(StorageClassDefault).ShouldNot(BeNil()) + StorageClassNotDefault := FakeStorageClass(StorageClassName, IsDefautl) + Expect(StorageClassNotDefault).ShouldNot(BeNil()) + }) }) diff --git a/internal/cli/testing/printer.go b/internal/cli/testing/printer.go index f9ccd2a64..05cb84c33 100644 --- a/internal/cli/testing/printer.go +++ b/internal/cli/testing/printer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing @@ -23,7 +26,7 @@ import ( ) // Capture replaces os.Stdout with a writer that buffers any data written -// to os.Stdout. Call the returned function to cleanup and get the data +// to os.Stdout. Call the returned function to clean up and return the data // as a string. func Capture() func() (string, error) { r, w, err := os.Pipe() diff --git a/internal/cli/testing/suite_test.go b/internal/cli/testing/suite_test.go index c112d986b..730ff3d2e 100644 --- a/internal/cli/testing/suite_test.go +++ b/internal/cli/testing/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/cli/testing/testdata/class.yaml b/internal/cli/testing/testdata/class.yaml index 9e1c965fd..dc97670b3 100644 --- a/internal/cli/testing/testdata/class.yaml +++ b/internal/cli/testing/testdata/class.yaml @@ -1,61 +1,60 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ComponentClassDefinition +metadata: + name: kb.classes.default.apecloud-mysql.mysql + labels: + class.kubeblocks.io/provider: kubeblocks + apps.kubeblocks.io/component-def-ref: mysql + clusterdefinition.kubeblocks.io/name: apecloud-mysql +spec: + groups: + - # resource constraint name, such as general, memory-optimized, cpu-optimized etc. + resourceConstraintRef: kb-resource-constraint-general + # class schema template, you can set default resource values here + template: | + cpu: "{{ .cpu }}" + memory: "{{ .memory }}Gi" + # class schema template variables + vars: [ cpu, memory] + series: + - # class name generator, you can reference variables in class schema template + # it's also ok to define static class name in following class definitions + namingTemplate: "general-{{ .cpu }}c{{ .memory }}g" -- # class family name, such as general, memory-optimized, cpu-optimized etc. - family: kb-class-family-general - # class schema template, you can set default resource values here - template: | - cpu: "{{ or .cpu 1 }}" - memory: "{{ or .memory 4 }}Gi" - storage: - - name: data - size: "{{ or .dataStorageSize 10 }}Gi" - - name: log - size: "{{ or .logStorageSize 1 }}Gi" - # class schema template variables - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - # class name generator, you can reference variables in class schema template - # it's also ok to define static class name in following class definitions - name: "general-{{ .cpu }}c{{ .memory }}g" + # class definitions, we support two kinds of class definitions: + # 1. define arguments for class schema variables, class schema will be dynamically generated + # 2. statically define complete class schema + classes: + - args: [ "1", "1"] + - args: [ "2", "2"] + - args: [ "2", "4"] + - args: [ "2", "8"] + - args: [ "4", "16"] + - args: [ "8", "32"] + - args: [ "16", "64"] + - args: [ "32", "128"] + - args: [ "64", "256"] + - args: [ "128", "512"] - # class definitions, we support two kinds of class definitions: - # 1. define arguments for class schema variables, class schema will be dynamically generated - # 2. statically define complete class schema - classes: - - args: [1, 1, 100, 10] - - args: [2, 2, 100, 10] - - args: [2, 4, 100, 10] - - args: [2, 8, 100, 10] - - args: [4, 16, 100, 10] - - args: [8, 32, 100, 10] - - args: [16, 64, 200, 10] - - args: [32, 128, 200, 10] - - args: [64, 256, 200, 10] - - args: [128, 512, 200, 10] - -- family: kb-class-family-memory-optimized - template: | - cpu: "{{ or .cpu 1 }}" - memory: "{{ or .memory 8 }}Gi" - storage: - - name: data - size: "{{ or .dataStorageSize 10 }}Gi" - - name: log - size: "{{ or .logStorageSize 1 }}Gi" - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - name: "mo-{{ .cpu }}c{{ .memory }}g" - classes: - - args: [2, 16, 100, 10] - - args: [4, 32, 100, 10] - - args: [8, 64, 100, 10] - - args: [12, 96, 100, 10] - - args: [24, 192, 200, 10] - - args: [48, 384, 200, 10] - - args: [2, 32, 100, 10] - - args: [4, 64, 100, 10] - - args: [8, 128, 100, 10] - - args: [16, 256, 100, 10] - - args: [32, 512, 200, 10] - - args: [48, 768, 200, 10] - - args: [64, 1024, 200, 10] - - args: [128, 2048, 200, 10] + - resourceConstraintRef: kb-resource-constraint-memory-optimized + template: | + cpu: "{{ .cpu }}" + memory: "{{ .memory }}Gi" + vars: [ cpu, memory] + series: + - namingTemplate: "mo-{{ .cpu }}c{{ .memory }}g" + classes: + - args: [ "2", "16"] + - args: [ "4", "32"] + - args: [ "8", "64"] + - args: [ "12", "96"] + - args: [ "24", "192"] + - args: [ "48", "384"] + - args: [ "2", "32"] + - args: [ "4", "64"] + - args: [ "8", "128"] + - args: [ "16", "256"] + - args: [ "32", "512"] + - args: [ "48", "768"] + - args: [ "64", "1024"] + - args: [ "128", "2048"] \ No newline at end of file diff --git a/internal/cli/testing/testdata/cluster.yaml b/internal/cli/testing/testdata/cluster.yaml new file mode 100644 index 000000000..832caf7af --- /dev/null +++ b/internal/cli/testing/testdata/cluster.yaml @@ -0,0 +1,30 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + annotations: {} + name: test-mycluster + namespace: default +spec: + affinity: + nodeLabels: {} + podAntiAffinity: Preferred + tenancy: SharedNode + topologyKeys: [] + clusterDefinitionRef: apecloud-mysql + clusterVersionRef: ac-mysql-8.0.30 + componentSpecs: + - componentDefRef: mysql + monitor: true + name: mysql + replicas: 3 + resources: {} + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi + terminationPolicy: Delete + tolerations: [] \ No newline at end of file diff --git a/internal/cli/testing/testdata/component.yaml b/internal/cli/testing/testdata/component.yaml index 644530ad8..a4984b2ff 100644 --- a/internal/cli/testing/testdata/component.yaml +++ b/internal/cli/testing/testdata/component.yaml @@ -11,4 +11,4 @@ resources: requests: storage: 1Gi - volumeMode: Filesystem + volumeMode: Filesystem \ No newline at end of file diff --git a/internal/cli/testing/testdata/component_with_class_1c1g.yaml b/internal/cli/testing/testdata/component_with_class_1c1g.yaml new file mode 100644 index 000000000..a5adfde07 --- /dev/null +++ b/internal/cli/testing/testdata/component_with_class_1c1g.yaml @@ -0,0 +1,16 @@ +- name: test + componentDefRef: mysql + monitor: true + enabledLogs: [error, slow] + replicas: 1 + classDefRef: + class: general-1c1g + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + volumeMode: Filesystem \ No newline at end of file diff --git a/internal/cli/testing/testdata/component_with_invalid_class.yaml b/internal/cli/testing/testdata/component_with_invalid_class.yaml new file mode 100644 index 000000000..d9f359f45 --- /dev/null +++ b/internal/cli/testing/testdata/component_with_invalid_class.yaml @@ -0,0 +1,16 @@ +- name: test + componentDefRef: mysql + monitor: true + enabledLogs: [error, slow] + replicas: 1 + classDefRef: + class: class-not-exists + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + volumeMode: Filesystem \ No newline at end of file diff --git a/internal/cli/testing/testdata/component_with_invalid_resource.yaml b/internal/cli/testing/testdata/component_with_invalid_resource.yaml new file mode 100644 index 000000000..efdc476b1 --- /dev/null +++ b/internal/cli/testing/testdata/component_with_invalid_resource.yaml @@ -0,0 +1,18 @@ +- name: test + componentDefRef: mysql + monitor: true + enabledLogs: [error, slow] + replicas: 1 + resources: + requests: + cpu: 3 + memory: 7Gi + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + volumeMode: Filesystem \ No newline at end of file diff --git a/internal/cli/testing/testdata/component_with_resource_1c1g.yaml b/internal/cli/testing/testdata/component_with_resource_1c1g.yaml new file mode 100644 index 000000000..cfdf53ae8 --- /dev/null +++ b/internal/cli/testing/testdata/component_with_resource_1c1g.yaml @@ -0,0 +1,18 @@ +- name: test + componentDefRef: mysql + monitor: true + enabledLogs: [error, slow] + replicas: 1 + resources: + requests: + cpu: 1 + memory: 1Gi + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + volumeMode: Filesystem \ No newline at end of file diff --git a/internal/cli/testing/testdata/custom_class.yaml b/internal/cli/testing/testdata/custom_class.yaml index b51a653ea..6ba0196c7 100644 --- a/internal/cli/testing/testdata/custom_class.yaml +++ b/internal/cli/testing/testdata/custom_class.yaml @@ -1,33 +1,23 @@ -- family: kb-class-family-general - template: | - cpu: "{{ or .cpu 1 }}" - memory: "{{ or .memory 4 }}Gi" - storage: - - name: data - size: "{{ or .dataStorageSize 10 }}Gi" - - name: log - size: "{{ or .logStorageSize 1 }}Gi" - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - name: "custom-{{ .cpu }}c{{ .memory }}g" - classes: - - args: [1, 1, 100, 10] - - name: custom-200c400g - cpu: 200 - memory: 400Gi + - resourceConstraintRef: kb-resource-constraint-general + template: | + cpu: "{{ or .cpu 1 }}" + memory: "{{ or .memory 4 }}Gi" + vars: [cpu, memory] + series: + - namingTemplate: "custom-{{ .cpu }}c{{ .memory }}g" + classes: + - args: ["1", "1"] + - name: custom-4c16g + cpu: 4 + memory: 16Gi -- family: kb-class-family-memory-optimized - template: | - cpu: "{{ or .cpu 1 }}" - memory: "{{ or .memory 4 }}Gi" - storage: - - name: data - size: "{{ or .dataStorageSize 10 }}Gi" - - name: log - size: "{{ or .logStorageSize 1 }}Gi" - vars: [cpu, memory, dataStorageSize, logStorageSize] - series: - - name: "custom-{{ .cpu }}c{{ .memory }}g" - classes: - - args: [1, 32, 100, 10] - - args: [2, 64, 100, 10] + - resourceConstraintRef: kb-resource-constraint-memory-optimized + template: | + cpu: "{{ or .cpu 1 }}" + memory: "{{ or .memory 4 }}Gi" + vars: [cpu, memory] + series: + - namingTemplate: "custom-{{ .cpu }}c{{ .memory }}g" + classes: + - args: ["2", "16"] + - args: ["4", "64"] diff --git a/internal/cli/testing/testdata/classfamily-general.yaml b/internal/cli/testing/testdata/resource-constraint-general.yaml similarity index 68% rename from internal/cli/testing/testdata/classfamily-general.yaml rename to internal/cli/testing/testdata/resource-constraint-general.yaml index 0ba574d26..a6f300221 100644 --- a/internal/cli/testing/testdata/classfamily-general.yaml +++ b/internal/cli/testing/testdata/resource-constraint-general.yaml @@ -1,11 +1,11 @@ apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClassFamily +kind: ComponentResourceConstraint metadata: - name: kb-class-family-general + name: kb-resource-constraint-general labels: - classfamily.kubeblocks.io/provider: kubeblocks + resourceconstraint.kubeblocks.io/provider: kubeblocks spec: - models: + constraints: - cpu: min: 0.5 max: 2 diff --git a/internal/cli/testing/testdata/classfamily-memory-optimized.yaml b/internal/cli/testing/testdata/resource-constraint-memory-optimized.yaml similarity index 59% rename from internal/cli/testing/testdata/classfamily-memory-optimized.yaml rename to internal/cli/testing/testdata/resource-constraint-memory-optimized.yaml index b02f488b9..a0acf512a 100644 --- a/internal/cli/testing/testdata/classfamily-memory-optimized.yaml +++ b/internal/cli/testing/testdata/resource-constraint-memory-optimized.yaml @@ -1,11 +1,11 @@ apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClassFamily +kind: ComponentResourceConstraint metadata: - name: kb-class-family-memory-optimized + name: kb-resource-constraint-memory-optimized labels: - classfamily.kubeblocks.io/provider: kubeblocks + resourceconstraint.kubeblocks.io/provider: kubeblocks spec: - models: + constraints: - cpu: slots: [2, 4, 8, 12, 24, 48] memory: diff --git a/internal/cli/types/migrationapi/migration_object_express.go b/internal/cli/types/migrationapi/migration_object_express.go new file mode 100644 index 000000000..00c4ded8d --- /dev/null +++ b/internal/cli/types/migrationapi/migration_object_express.go @@ -0,0 +1,96 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + "fmt" + "strings" +) + +type MigrationObjectExpress struct { + WhiteList []DBObjectExpress `json:"whiteList"` + // +optional + BlackList []DBObjectExpress `json:"blackList"` +} + +func (m *MigrationObjectExpress) String(isWhite bool) string { + expressArr := m.WhiteList + if !isWhite { + expressArr = m.BlackList + } + stringArr := make([]string, 0) + for _, db := range expressArr { + stringArr = append(stringArr, db.String()...) + } + return strings.Join(stringArr, ",") +} + +type DBObjectExpress struct { + SchemaName string `json:"schemaName"` + // +optional + SchemaMappingName string `json:"schemaMappingName"` + // +optional + IsAll bool `json:"isAll"` + // +optional + TableList []TableObjectExpress `json:"tableList"` + DxlOpConfig `json:""` +} + +func (d *DBObjectExpress) String() []string { + stringArr := make([]string, 0) + if d.IsAll { + stringArr = append(stringArr, d.SchemaName) + } else { + for _, tb := range d.TableList { + stringArr = append(stringArr, fmt.Sprintf("%s.%s", d.SchemaName, tb.TableName)) + } + } + return stringArr +} + +type TableObjectExpress struct { + TableName string `json:"tableName"` + // +optional + TableMappingName string `json:"tableMappingName"` + // +optional + IsAll bool `json:"isAll"` + // +optional + FieldList []FieldObjectExpress `json:"fieldList"` + DxlOpConfig `json:""` +} + +type FieldObjectExpress struct { + FieldName string `json:"fieldName"` + // +optional + FieldMappingName string `json:"fieldMappingName"` +} + +type DxlOpConfig struct { + // +optional + DmlOp []DMLOpEnum `json:"dmlOp"` + // +optional + DdlOp []DDLOpEnum `json:"ddlOp"` + // +optional + DclOp []DCLOpEnum `json:"dclOp"` +} + +func (op *DxlOpConfig) IsEmpty() bool { + return len(op.DmlOp) == 0 +} diff --git a/internal/cli/types/migrationapi/migrationtask_types.go b/internal/cli/types/migrationapi/migrationtask_types.go new file mode 100644 index 000000000..9121ddd23 --- /dev/null +++ b/internal/cli/types/migrationapi/migrationtask_types.go @@ -0,0 +1,156 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MigrationTaskSpec defines the desired state of MigrationTask +type MigrationTaskSpec struct { + TaskType TaskTypeEnum `json:"taskType,omitempty"` + Template string `json:"template"` + SourceEndpoint Endpoint `json:"sourceEndpoint,omitempty"` + SinkEndpoint Endpoint `json:"sinkEndpoint,omitempty"` + // +optional + Cdc CdcConfig `json:"cdc,omitempty"` + // +optional + Initialization InitializationConfig `json:"initialization,omitempty"` + MigrationObj MigrationObjectExpress `json:"migrationObj,omitempty"` + // +optional + IsForceDelete bool `json:"isForceDelete,omitempty"` + // +optional + GlobalTolerations []v1.Toleration `json:"globalTolerations,omitempty"` + // +optional + GlobalResources v1.ResourceRequirements `json:"globalResources,omitempty"` +} + +type Endpoint struct { + // +optional + EndpointType EndpointTypeEnum `json:"endpointType,omitempty"` + Address string `json:"address"` + // +optional + DatabaseName string `json:"databaseName,omitempty"` + // +optional + UserName string `json:"userName"` + // +optional + Password string `json:"password"` + // +optional + Secret UserPswSecret `json:"secret"` +} + +type UserPswSecret struct { + Name string `json:"name"` + // +optional + Namespace string `json:"namespace,omitempty"` + // +optional + UserKeyword string `json:"userKeyword,omitempty"` + // +optional + PasswordKeyword string `json:"passwordKeyword,omitempty"` +} + +type CdcConfig struct { + // +optional + Config BaseConfig `json:"config"` +} + +type InitializationConfig struct { + // +optional + Steps []StepEnum `json:"steps,omitempty"` + // +optional + Config map[StepEnum]BaseConfig `json:"config,omitempty"` +} + +type BaseConfig struct { + // +optional + Resource v1.ResourceRequirements `json:"resource,omitempty"` + // +optional + Tolerations []v1.Toleration `json:"tolerations,omitempty"` + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + Param IntOrStringMap `json:"param"` + // +optional + PersistentVolumeClaimName string `json:"persistentVolumeClaimName"` + // +optional + Metrics Metrics `json:"metrics,omitempty"` +} + +// MigrationTaskStatus defines the observed state of MigrationTask +type MigrationTaskStatus struct { + // +optional + TaskStatus TaskStatus `json:"taskStatus"` + // +optional + StartTime *metav1.Time `json:"startTime"` + // +optional + FinishTime *metav1.Time `json:"finishTime"` + // +optional + Cdc RunTimeStatus `json:"cdc"` + // +optional + Initialization RunTimeStatus `json:"initialization"` +} + +type RunTimeStatus struct { + // +optional + StartTime *metav1.Time `json:"startTime"` + // +optional + FinishTime *metav1.Time `json:"finishTime"` + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + RunTimeParam IntOrStringMap `json:"runTimeParam,omitempty"` + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + Metrics IntOrStringMap `json:"metrics,omitempty"` + // +optional + FailedReason string `json:"failedReason,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={dtplatform},scope=Cluster,shortName=mt +// +kubebuilder:printcolumn:name="TEMPLATE",type="string",JSONPath=".spec.template",description="spec migration template" +// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.taskStatus",description="status taskStatus" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// MigrationTask is the Schema for the migrationTasks API +type MigrationTask struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MigrationTaskSpec `json:"spec,omitempty"` + Status MigrationTaskStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MigrationTaskList contains a list of MigrationTask +type MigrationTaskList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MigrationTask `json:"items"` +} + +type Metrics struct { + IsDisable bool `json:"isDisable,omitempty"` + PeriodSeconds int32 `json:"periodSeconds,omitempty"` +} diff --git a/internal/cli/types/migrationapi/migrationtemplate_types.go b/internal/cli/types/migrationapi/migrationtemplate_types.go new file mode 100644 index 000000000..66acc6e6a --- /dev/null +++ b/internal/cli/types/migrationapi/migrationtemplate_types.go @@ -0,0 +1,106 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// MigrationTemplateSpec defines the desired state of MigrationTemplate +type MigrationTemplateSpec struct { + TaskType []TaskTypeEnum `json:"taskType,omitempty"` + Source DBTypeSupport `json:"source"` + Sink DBTypeSupport `json:"target"` + Initialization InitializationModel `json:"initialization,omitempty"` + Cdc CdcModel `json:"cdc,omitempty"` + // +optional + Description string `json:"description,omitempty"` + // +optional + Decorator string `json:"decorator,omitempty"` +} + +type DBTypeSupport struct { + DBType DBTypeEnum `json:"dbType"` + DBVersion string `json:"dbVersion"` +} + +type InitializationModel struct { + // +optional + IsPositionPreparation bool `json:"isPositionPreparation,omitempty"` + Steps []StepModel `json:"steps,omitempty"` +} + +type StepModel struct { + Step StepEnum `json:"step"` + Container BasicContainerTemplate `json:"container"` + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + Param IntOrStringMap `json:"param"` +} + +type CdcModel struct { + Container BasicContainerTemplate `json:"container"` + // +optional + Replicas *int32 `json:"replicas,omitempty"` + // +optional + // +kubebuilder:pruning:PreserveUnknownFields + // +kubebuilder:validation:Schemaless + Param IntOrStringMap `json:"param"` +} + +type BasicContainerTemplate struct { + Image string `json:"image"` + // +optional + Command []string `json:"command,omitempty"` + // +optional + Env []v1.EnvVar `json:"env,omitempty"` +} + +// MigrationTemplateStatus defines the observed state of MigrationTemplate +type MigrationTemplateStatus struct { + Phase Phase `json:"phase,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// +kubebuilder:resource:categories={dtplatform},scope=Cluster,shortName=mtp +// +kubebuilder:printcolumn:name="DATABASE-MAPPING",type="string",JSONPath=".spec.description",description="the database mapping that supported" +// +kubebuilder:printcolumn:name="STATUS",type="string",JSONPath=".status.phase",description="the template status" +// +kubebuilder:printcolumn:name="AGE",type="date",JSONPath=".metadata.creationTimestamp" + +// MigrationTemplate is the Schema for the migrationtemplates API +type MigrationTemplate struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec MigrationTemplateSpec `json:"spec,omitempty"` + Status MigrationTemplateStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// MigrationTemplateList contains a list of MigrationTemplate +type MigrationTemplateList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []MigrationTemplate `json:"items"` +} diff --git a/internal/cli/types/migrationapi/type.go b/internal/cli/types/migrationapi/type.go new file mode 100644 index 000000000..01aa75a8d --- /dev/null +++ b/internal/cli/types/migrationapi/type.go @@ -0,0 +1,213 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package v1alpha1 + +import ( + "strings" + + appv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" +) + +// DBTypeEnum defines the MigrationTemplate CR .spec.Source.DbType or .spec.Sink.DbType +// +enum +// +kubebuilder:validation:Enum={MySQL, PostgreSQL} +type DBTypeEnum string + +const ( + MigrationDBTypeMySQL DBTypeEnum = "MySQL" // default value + MigrationDBTypePostgreSQL DBTypeEnum = "PostgreSQL" +) + +func (d DBTypeEnum) String() string { + return string(d) +} + +// TaskTypeEnum defines the MigrationTask CR .spec.taskType +// +enum +// +kubebuilder:validation:Enum={initialization,cdc,initialization-and-cdc,initialization-and-twoway-cdc} +type TaskTypeEnum string + +const ( + Initialization TaskTypeEnum = "initialization" + CDC TaskTypeEnum = "cdc" + InitializationAndCdc TaskTypeEnum = "initialization-and-cdc" // default value +) + +// EndpointTypeEnum defines the MigrationTask CR .spec.source.endpointType and .spec.sink.endpointType +// +enum +// +kubebuilder:validation:Enum={address} +type EndpointTypeEnum string + +const ( + AddressDirectConnect EndpointTypeEnum = "address" // default value +) + +// non-use yet + +type ConflictPolicyEnum string + +const ( + Ignore ConflictPolicyEnum = "ignore" // default in FullLoad + Override ConflictPolicyEnum = "override" // default in CDC +) + +// DMLOpEnum defines the MigrationTask CR .spec.migrationObj +// +enum +// +kubebuilder:validation:Enum={all,none,insert,update,delete} +type DMLOpEnum string + +const ( + AllDML DMLOpEnum = "all" + NoneDML DMLOpEnum = "none" + Insert DMLOpEnum = "insert" + Update DMLOpEnum = "update" + Delete DMLOpEnum = "delete" +) + +// DDLOpEnum defines the MigrationTask CR .spec.migrationObj +// +enum +// +kubebuilder:validation:Enum={all,none} +type DDLOpEnum string + +const ( + AllDDL DDLOpEnum = "all" + NoneDDL DDLOpEnum = "none" +) + +// DCLOpEnum defines the MigrationTask CR .spec.migrationObj +// +enum +// +kubebuilder:validation:Enum={all,none} +type DCLOpEnum string + +const ( + AllDCL DDLOpEnum = "all" + NoneDCL DDLOpEnum = "none" +) + +// TaskStatus defines the MigrationTask CR .status.taskStatus +// +enum +// +kubebuilder:validation:Enum={Prepare,InitPrepared,Init,InitFinished,Running,Cached,Pause,Done} +type TaskStatus string + +const ( + PrepareStatus TaskStatus = "Prepare" + InitPrepared TaskStatus = "InitPrepared" + InitStatus TaskStatus = "Init" + InitFinished TaskStatus = "InitFinished" + RunningStatus TaskStatus = "Running" + CachedStatus TaskStatus = "Cached" + PauseStatus TaskStatus = "Pause" + DoneStatus TaskStatus = "Done" +) + +// StepEnum defines the MigrationTask CR .spec.steps +// +enum +// +kubebuilder:validation:Enum={preCheck,initStruct,initData,initStructLater} +type StepEnum string + +const ( + StepPreCheck StepEnum = "preCheck" + StepStructPreFullLoad StepEnum = "initStruct" + StepFullLoad StepEnum = "initData" + StepStructAfterFullLoad StepEnum = "initStructLater" + StepInitialization StepEnum = "initialization" + StepPreDelete StepEnum = "preDelete" + StepCdc StepEnum = "cdc" +) + +func (s StepEnum) String() string { + return string(s) +} + +func (s StepEnum) LowerCaseString() string { + return strings.ToLower(s.String()) +} + +func (s StepEnum) CliString() string { + switch s { + case StepPreCheck: + return CliStepPreCheck.String() + case StepStructPreFullLoad: + return CliStepInitStruct.String() + case StepFullLoad: + return CliStepInitData.String() + case StepCdc: + return CliStepCdc.String() + default: + return "unknown" + } +} + +type CliStepEnum string + +const ( + CliStepGlobal CliStepEnum = "global" + CliStepPreCheck CliStepEnum = "precheck" + CliStepInitStruct CliStepEnum = "init-struct" + CliStepInitData CliStepEnum = "init-data" + CliStepCdc CliStepEnum = "cdc" +) + +func (s CliStepEnum) String() string { + return string(s) +} + +// Phase defines the MigrationTemplate CR .status.phase +// +enum +// +kubebuilder:validation:Enum={Available,Unavailable} +type Phase string + +const ( + AvailablePhase Phase = "Available" + UnavailablePhase Phase = "Unavailable" +) + +type MigrationObjects struct { + Task *MigrationTask + Template *MigrationTemplate + + Jobs *batchv1.JobList + Pods *v1.PodList + StatefulSets *appv1.StatefulSetList +} + +// +k8s:deepcopy-gen=false + +type IntOrStringMap map[string]interface{} + +func (in *IntOrStringMap) DeepCopyInto(out *IntOrStringMap) { + if in == nil { + *out = nil + } else { + *out = runtime.DeepCopyJSON(*in) + } +} + +func (in *IntOrStringMap) DeepCopy() *IntOrStringMap { + if in == nil { + return nil + } + out := new(IntOrStringMap) + in.DeepCopyInto(out) + return out +} diff --git a/internal/cli/types/types.go b/internal/cli/types/types.go index 324700fef..284fd4abd 100644 --- a/internal/cli/types/types.go +++ b/internal/cli/types/types.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package types @@ -20,8 +23,10 @@ import ( "fmt" appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" rbacv1 "k8s.io/api/rbac/v1" + "k8s.io/apimachinery/pkg/runtime/schema" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -34,6 +39,9 @@ const ( // CliHomeEnv defines kbcli home system env CliHomeEnv = "KBCLI_HOME" + // DefaultLogFilePrefix is the default log file prefix + DefaultLogFilePrefix = "kbcli" + // DefaultNamespace is the namespace where kubeblocks is installed if // no other namespace is specified DefaultNamespace = "kb-system" @@ -52,9 +60,18 @@ const ( ResourceDeployments = "deployments" ResourceConfigmaps = "configmaps" ResourceStatefulSets = "statefulsets" + ResourceDaemonSets = "daemonsets" ResourceSecrets = "secrets" ) +// K8s batch API group +const ( + K8SBatchAPIGroup = batchv1.GroupName + K8sBatchAPIVersion = "v1" + ResourceJobs = "jobs" + ResourceCronJobs = "cronjobs" +) + // K8s webhook API group const ( WebhookAPIGroup = "admissionregistration.k8s.io" @@ -65,22 +82,25 @@ const ( // Apps API group const ( - AppsAPIGroup = "apps.kubeblocks.io" - AppsAPIVersion = "v1alpha1" - ResourceClusters = "clusters" - ResourceClusterDefs = "clusterdefinitions" - ResourceClusterVersions = "clusterversions" - ResourceOpsRequests = "opsrequests" - ResourceConfigConstraintVersions = "configconstraints" - ResourceClassFamily = "classfamilies" - KindCluster = "Cluster" - KindClusterDef = "ClusterDefinition" - KindClusterVersion = "ClusterVersion" - KindConfigConstraint = "ConfigConstraint" - KindBackup = "Backup" - KindRestoreJob = "RestoreJob" - KindBackupPolicyTemplate = "BackupPolicyTemplate" - KindOps = "OpsRequest" + AppsAPIGroup = "apps.kubeblocks.io" + AppsAPIVersion = "v1alpha1" + ResourcePods = "pods" + ResourceClusters = "clusters" + ResourceClusterDefs = "clusterdefinitions" + ResourceClusterVersions = "clusterversions" + ResourceOpsRequests = "opsrequests" + ResourceConfigConstraintVersions = "configconstraints" + ResourceComponentResourceConstraint = "componentresourceconstraints" + ResourceComponentClassDefinition = "componentclassdefinitions" + KindCluster = "Cluster" + KindComponentClassDefinition = "ComponentClassDefinition" + KindClusterDef = "ClusterDefinition" + KindClusterVersion = "ClusterVersion" + KindConfigConstraint = "ConfigConstraint" + KindBackup = "Backup" + KindRestoreJob = "RestoreJob" + KindBackupPolicy = "BackupPolicy" + KindOps = "OpsRequest" ) // K8S rbac API group @@ -89,6 +109,9 @@ const ( RBACAPIVersion = "v1" ClusterRoles = "clusterroles" ClusterRoleBindings = "clusterrolebindings" + Roles = "roles" + RoleBindings = "rolebindings" + ServiceAccounts = "serviceaccounts" ) // Annotations @@ -97,21 +120,19 @@ const ( ServiceHAVIPTypeAnnotationValue = "private-ip" ServiceFloatingIPAnnotationKey = "service.kubernetes.io/kubeblocks-havip-floating-ip" - ClassLevelLabelKey = "class.kubeblocks.io/level" - ClassProviderLabelKey = "class.kubeblocks.io/provider" - ClassFamilyProviderLabelKey = "classfamily.kubeblocks.io/provider" - ComponentClassAnnotationKey = "cluster.kubeblocks.io/component-class" + ClassProviderLabelKey = "class.kubeblocks.io/provider" + ResourceConstraintProviderLabelKey = "resourceconstraint.kubeblocks.io/provider" + ReloadConfigMapAnnotationKey = "kubeblocks.io/reload-configmap" // mark an annotation to load configmap ) // DataProtection API group const ( - DPAPIGroup = "dataprotection.kubeblocks.io" - DPAPIVersion = "v1alpha1" - ResourceBackups = "backups" - ResourceBackupTools = "backuptools" - ResourceRestoreJobs = "restorejobs" - ResourceBackupPolicies = "backuppolicies" - ResourceBackupPolicyTemplates = "backuppolicytemplates" + DPAPIGroup = "dataprotection.kubeblocks.io" + DPAPIVersion = "v1alpha1" + ResourceBackups = "backups" + ResourceBackupTools = "backuptools" + ResourceRestoreJobs = "restorejobs" + ResourceBackupPolicies = "backuppolicies" ) // Extensions API group @@ -121,6 +142,21 @@ const ( ResourceAddons = "addons" ) +// Migration API group +const ( + MigrationAPIGroup = "datamigration.apecloud.io" + MigrationAPIVersion = "v1alpha1" + ResourceMigrationTasks = "migrationtasks" + ResourceMigrationTemplates = "migrationtemplates" +) + +// Crd Api group +const ( + CustomResourceDefinitionAPIGroup = "apiextensions.k8s.io" + CustomResourceDefinitionAPIVersion = "v1" + ResourceCustomResourceDefinition = "customresourcedefinitions" +) + const ( None = "" @@ -129,6 +165,9 @@ const ( ) var ( + // KubeBlocksName is the name of KubeBlocks project + KubeBlocksName = "kubeblocks" + // KubeBlocksRepoName helm repo name for kubeblocks KubeBlocksRepoName = "kubeblocks" @@ -166,6 +205,10 @@ type ConfigTemplateInfo struct { CMObj *corev1.ConfigMap } +func PodGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: "", Version: K8sCoreAPIVersion, Resource: ResourcePods} +} + func ClusterGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIVersion, Resource: ResourceClusters} } @@ -186,8 +229,8 @@ func BackupGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: DPAPIGroup, Version: DPAPIVersion, Resource: ResourceBackups} } -func BackupPolicyTemplateGVR() schema.GroupVersionResource { - return schema.GroupVersionResource{Group: DPAPIGroup, Version: DPAPIVersion, Resource: ResourceBackupPolicyTemplates} +func BackupPolicyGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: DPAPIGroup, Version: DPAPIVersion, Resource: ResourceBackupPolicies} } func BackupToolGVR() schema.GroupVersionResource { @@ -202,8 +245,12 @@ func AddonGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: ExtensionsAPIGroup, Version: ExtensionsAPIVersion, Resource: ResourceAddons} } -func ClassFamilyGVR() schema.GroupVersionResource { - return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIVersion, Resource: ResourceClassFamily} +func ComponentResourceConstraintGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIVersion, Resource: ResourceComponentResourceConstraint} +} + +func ComponentClassDefinitionGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: AppsAPIGroup, Version: AppsAPIVersion, Resource: ResourceComponentClassDefinition} } func CRDGVR() schema.GroupVersionResource { @@ -226,6 +273,10 @@ func StatefulSetGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: appsv1.GroupName, Version: K8sCoreAPIVersion, Resource: ResourceStatefulSets} } +func DaemonSetGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: appsv1.GroupName, Version: K8sCoreAPIVersion, Resource: ResourceDaemonSets} +} + func DeployGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: appsv1.GroupName, Version: K8sCoreAPIVersion, Resource: ResourceDeployments} } @@ -284,3 +335,46 @@ func ClusterRoleGVR() schema.GroupVersionResource { func ClusterRoleBindingGVR() schema.GroupVersionResource { return schema.GroupVersionResource{Group: RBACAPIGroup, Version: RBACAPIVersion, Resource: ClusterRoleBindings} } + +func RoleGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: RBACAPIGroup, Version: RBACAPIVersion, Resource: Roles} +} + +func RoleBindingGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: RBACAPIGroup, Version: RBACAPIVersion, Resource: RoleBindings} +} + +func ServiceAccountGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: corev1.GroupName, Version: K8sCoreAPIVersion, Resource: ServiceAccounts} +} + +func MigrationTaskGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: MigrationAPIGroup, + Version: MigrationAPIVersion, + Resource: ResourceMigrationTasks, + } +} + +func MigrationTemplateGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: MigrationAPIGroup, + Version: MigrationAPIVersion, + Resource: ResourceMigrationTemplates, + } +} + +func CustomResourceDefinitionGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{ + Group: CustomResourceDefinitionAPIGroup, + Version: CustomResourceDefinitionAPIVersion, + Resource: ResourceCustomResourceDefinition, + } +} + +func JobGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: K8SBatchAPIGroup, Version: K8sBatchAPIVersion, Resource: ResourceJobs} +} +func CronJobGVR() schema.GroupVersionResource { + return schema.GroupVersionResource{Group: K8SBatchAPIGroup, Version: K8sBatchAPIVersion, Resource: ResourceCronJobs} +} diff --git a/internal/cli/util/completion.go b/internal/cli/util/completion.go index ceccb7e03..7d2f10c5e 100644 --- a/internal/cli/util/completion.go +++ b/internal/cli/util/completion.go @@ -1,28 +1,114 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util import ( + "bytes" + "io/ioutil" + "os" + "strings" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/printers" + "k8s.io/kubectl/pkg/cmd/get" cmdutil "k8s.io/kubectl/pkg/cmd/util" utilcomp "k8s.io/kubectl/pkg/util/completion" ) func ResourceNameCompletionFunc(f cmdutil.Factory, gvr schema.GroupVersionResource) func(*cobra.Command, []string, string) ([]string, cobra.ShellCompDirective) { - return utilcomp.ResourceNameCompletionFunc(f, GVRToString(gvr)) + return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + comps := utilcomp.CompGetResource(f, cmd, GVRToString(gvr), toComplete) + seen := make(map[string]bool) + + var availableComps []string + for _, arg := range args { + seen[arg] = true + } + for _, comp := range comps { + if !seen[comp] { + availableComps = append(availableComps, comp) + } + } + return availableComps, cobra.ShellCompDirectiveNoFileComp + } +} + +// CompGetResourceWithLabels gets the list of the resource specified which begin with `toComplete` and have the specified labels. +// example: CompGetResourceWithLabels(f, cmd, "pods", []string{"app=nginx"}, toComplete) +// gets the name of the pods which have the label `app=nginx` and begin with `toComplete` +func CompGetResourceWithLabels(f cmdutil.Factory, cmd *cobra.Command, resourceName string, labels []string, toComplete string) []string { + template := "{{ range .items }}{{ .metadata.name }} {{ end }}" + return CompGetFromTemplateWithLabels(&template, f, "", cmd, []string{resourceName}, labels, toComplete) +} + +// CompGetFromTemplateWithLabels executes a Get operation using the specified template and args and returns the results +// which begin with `toComplete` and have the specified labels. +// example: CompGetFromTemplateWithLabels(&template, f, "", cmd, []string{"pods"}, []string{"app=nginx"}, toComplete) +// will get the output of `kubectl get pods --template=template -l app=nginx`, and split the output by space and return +func CompGetFromTemplateWithLabels(template *string, f cmdutil.Factory, namespace string, cmd *cobra.Command, args []string, labels []string, toComplete string) []string { + buf := new(bytes.Buffer) + streams := genericclioptions.IOStreams{In: os.Stdin, Out: buf, ErrOut: ioutil.Discard} + o := get.NewGetOptions("kubectl", streams) + + // Get the list of names of the specified resource + o.PrintFlags.TemplateFlags.GoTemplatePrintFlags.TemplateArgument = template + format := "go-template" + o.PrintFlags.OutputFormat = &format + + // Do the steps Complete() would have done. + // We cannot actually call Complete() or Validate() as these function check for + // the presence of flags, which, in our case won't be there + if namespace != "" { + o.Namespace = namespace + o.ExplicitNamespace = true + } else { + var err error + o.Namespace, o.ExplicitNamespace, err = f.ToRawKubeConfigLoader().Namespace() + if err != nil { + return nil + } + } + + o.ToPrinter = func(mapping *meta.RESTMapping, outputObjects *bool, withNamespace bool, withKind bool) (printers.ResourcePrinterFunc, error) { + printer, err := o.PrintFlags.ToPrinter() + if err != nil { + return nil, err + } + return printer.PrintObj, nil + } + + if len(labels) > 0 { + o.LabelSelector = strings.Join(labels, ",") + } + + _ = o.Run(f, cmd, args) + + var comps []string + resources := strings.Split(buf.String(), " ") + for _, res := range resources { + if res != "" && strings.HasPrefix(res, toComplete) { + comps = append(comps, res) + } + } + return comps } diff --git a/internal/cli/util/completion_test.go b/internal/cli/util/completion_test.go new file mode 100644 index 000000000..80f604c3e --- /dev/null +++ b/internal/cli/util/completion_test.go @@ -0,0 +1,82 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "fmt" + "net/http" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/cli-runtime/pkg/genericclioptions" + "k8s.io/cli-runtime/pkg/resource" + "k8s.io/client-go/kubernetes/scheme" + clientfake "k8s.io/client-go/rest/fake" + "k8s.io/kubectl/pkg/cmd/get" + cmdtesting "k8s.io/kubectl/pkg/cmd/testing" + + "github.com/apecloud/kubeblocks/internal/cli/testing" + "github.com/apecloud/kubeblocks/internal/constant" +) + +var _ = Describe("completion", func() { + const ( + namespace = testing.Namespace + clusterName = testing.ClusterName + ) + + var ( + tf *cmdtesting.TestFactory + streams genericclioptions.IOStreams + pods = testing.FakePods(3, namespace, clusterName) + ) + + BeforeEach(func() { + streams, _, _, _ = genericclioptions.NewTestIOStreams() + tf = cmdtesting.NewTestFactory().WithNamespace(testing.Namespace) + }) + + AfterEach(func() { + tf.Cleanup() + }) + + It("test completion pods", func() { + cmd := get.NewCmdGet("kbcli", tf, streams) + codec := scheme.Codecs.LegacyCodec(scheme.Scheme.PrioritizedVersionsAllGroups()...) + tf.UnstructuredClient = &clientfake.RESTClient{ + NegotiatedSerializer: resource.UnstructuredPlusDefaultContentConfig().NegotiatedSerializer, + Client: clientfake.CreateHTTPClient(func(req *http.Request) (*http.Response, error) { + switch req.URL.Query().Get(metav1.LabelSelectorQueryParam("v1")) { + case fmt.Sprintf("%s=%s", constant.RoleLabelKey, "leader"): + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil + case "": + return &http.Response{StatusCode: http.StatusOK, Header: cmdtesting.DefaultHeader(), Body: cmdtesting.ObjBody(codec, &pods.Items[0])}, nil + default: + return nil, fmt.Errorf("unexpected request: %v", req.URL) + } + }), + } + + Expect(len(CompGetResourceWithLabels(tf, cmd, "pods", []string{}, ""))).Should(Equal(1)) + Expect(len(CompGetResourceWithLabels(tf, cmd, "pods", []string{fmt.Sprintf("%s=%s", constant.RoleLabelKey, "leader")}, ""))).Should(Equal(1)) + }) +}) diff --git a/internal/cli/util/error.go b/internal/cli/util/error.go index 3ff7810ce..530fa30e1 100644 --- a/internal/cli/util/error.go +++ b/internal/cli/util/error.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/cli/util/error_test.go b/internal/cli/util/error_test.go index a8ae8a536..6a841050e 100644 --- a/internal/cli/util/error_test.go +++ b/internal/cli/util/error_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/cli/util/flags/flags.go b/internal/cli/util/flags/flags.go new file mode 100644 index 000000000..e9ce26654 --- /dev/null +++ b/internal/cli/util/flags/flags.go @@ -0,0 +1,39 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package flags + +import ( + "github.com/spf13/cobra" + cmdutil "k8s.io/kubectl/pkg/cmd/util" + utilcomp "k8s.io/kubectl/pkg/util/completion" + + "github.com/apecloud/kubeblocks/internal/cli/types" + "github.com/apecloud/kubeblocks/internal/cli/util" +) + +// AddClusterDefinitionFlag adds a flag "cluster-definition" for the cmd and stores the value of the flag +// in string p +func AddClusterDefinitionFlag(f cmdutil.Factory, cmd *cobra.Command, p *string) { + cmd.Flags().StringVar(p, "cluster-definition", *p, "Specify cluster definition, run \"kbcli clusterdefinition list\" to show all available cluster definition") + util.CheckErr(cmd.RegisterFlagCompletionFunc("cluster-definition", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return utilcomp.CompGetResource(f, cmd, util.GVRToString(types.ClusterDefGVR()), toComplete), cobra.ShellCompDirectiveNoFileComp + })) +} diff --git a/internal/cli/util/git.go b/internal/cli/util/git.go index 520202abc..bf8ea9adf 100644 --- a/internal/cli/util/git.go +++ b/internal/cli/util/git.go @@ -1,29 +1,39 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util import ( + "bytes" + "io" "os" + "os/exec" + "path/filepath" + "strings" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/pkg/errors" + "k8s.io/klog/v2" ) -// CloneGitRepo clone git repo to local path +// CloneGitRepo clones git repo to local path func CloneGitRepo(url, branch, path string) error { pullFunc := func(repo *git.Repository) error { // Get the working directory for the repository @@ -67,3 +77,68 @@ func CloneGitRepo(url, branch, path string) error { }) return err } + +func GitGetRemoteURL(dir string) (string, error) { + return ExecGitCommand(dir, "config", "--get", "remote.origin.url") +} + +// EnsureCloned clones into the destination path, otherwise returns no error. +func EnsureCloned(uri, destinationPath string) error { + if ok, err := IsGitCloned(destinationPath); err != nil { + return err + } else if !ok { + _, err = ExecGitCommand("", "clone", "-v", uri, destinationPath) + return err + } + return nil +} + +// IsGitCloned tests if the path is a git dir. +func IsGitCloned(gitPath string) (bool, error) { + f, err := os.Stat(filepath.Join(gitPath, ".git")) + if os.IsNotExist(err) { + return false, nil + } + return err == nil && f.IsDir(), err +} + +// EnsureUpdated ensures the destination path exists and is up to date. +func EnsureUpdated(uri, destinationPath string) error { + if err := EnsureCloned(uri, destinationPath); err != nil { + return err + } + return UpdateAndCleanUntracked(destinationPath) +} + +// UpdateAndCleanUntracked fetches origin and sets HEAD to origin/HEAD +// and also creates a pristine working directory by removing +// untracked files and directories. +func UpdateAndCleanUntracked(destinationPath string) error { + if _, err := ExecGitCommand(destinationPath, "fetch", "-v"); err != nil { + return errors.Wrapf(err, "fetch index at %q failed", destinationPath) + } + + if _, err := ExecGitCommand(destinationPath, "reset", "--hard", "@{upstream}"); err != nil { + return errors.Wrapf(err, "reset index at %q failed", destinationPath) + } + + _, err := ExecGitCommand(destinationPath, "clean", "-xfd") + return errors.Wrapf(err, "clean index at %q failed", destinationPath) +} + +// ExecGitCommand executes a git command in the given directory. +func ExecGitCommand(pwd string, args ...string) (string, error) { + klog.V(4).Infof("Going to run git %s", strings.Join(args, " ")) + cmd := exec.Command("git", args...) + cmd.Dir = pwd + buf := bytes.Buffer{} + var w io.Writer = &buf + if klog.V(2).Enabled() { + w = io.MultiWriter(w, os.Stderr) + } + cmd.Stdout, cmd.Stderr = w, w + if err := cmd.Run(); err != nil { + return "", errors.Wrapf(err, "command execution failure, output=%q", buf.String()) + } + return strings.TrimSpace(buf.String()), nil +} diff --git a/internal/cli/util/helm/config.go b/internal/cli/util/helm/config.go index 74ca56e4e..3c9338805 100644 --- a/internal/cli/util/helm/config.go +++ b/internal/cli/util/helm/config.go @@ -1,24 +1,25 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package helm import ( - "os" - "helm.sh/helm/v3/pkg/action" ) @@ -40,7 +41,7 @@ func NewConfig(namespace string, kubeConfig string, ctx string, debug bool) *Con } if debug { - cfg.logFn = GetVerboseLog(os.Stdout) + cfg.logFn = GetVerboseLog() } else { cfg.logFn = GetQuiteLog() } diff --git a/internal/cli/util/helm/errors.go b/internal/cli/util/helm/errors.go index a46e41af8..687872499 100644 --- a/internal/cli/util/helm/errors.go +++ b/internal/cli/util/helm/errors.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package helm diff --git a/internal/cli/util/helm/helm.go b/internal/cli/util/helm/helm.go index a67775d6f..2f3ff95d7 100644 --- a/internal/cli/util/helm/helm.go +++ b/internal/cli/util/helm/helm.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package helm @@ -48,6 +51,7 @@ import ( "helm.sh/helm/v3/pkg/storage/driver" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/rest" + "k8s.io/klog/v2" "github.com/apecloud/kubeblocks/internal/cli/types" ) @@ -66,11 +70,16 @@ type InstallOpts struct { ValueOpts *values.Options Timeout time.Duration Atomic bool + DisableHooks bool + + // for helm template + DryRun *bool + OutputDir string } type Option func(*cli.EnvSettings) -// AddRepo will add a repo +// AddRepo adds a repo func AddRepo(r *repo.Entry) error { settings := cli.New() repoFile := settings.RepositoryConfig @@ -92,8 +101,7 @@ func AddRepo(r *repo.Entry) error { if f.Has(r.Name) { existing := f.Get(r.Name) if *r != *existing && r.Name != types.KubeBlocksChartName { - // The input coming in for the Name is different from what is already - // configured. Return an error. + // The input Name is different from the existing one, return an error return errors.Errorf("repository name (%s) already exists, please specify a different name", r.Name) } } @@ -115,7 +123,7 @@ func AddRepo(r *repo.Entry) error { return nil } -// RemoveRepo will remove a repo +// RemoveRepo removes a repo func RemoveRepo(r *repo.Entry) error { settings := cli.New() repoFile := settings.RepositoryConfig @@ -138,7 +146,7 @@ func RemoveRepo(r *repo.Entry) error { return nil } -// GetInstalled get helm package release info if installed. +// GetInstalled gets helm package release info if installed. func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, error) { res, err := action.NewGet(cfg).Run(i.Name) if err != nil { @@ -154,7 +162,7 @@ func (i *InstallOpts) GetInstalled(cfg *action.Configuration) (*release.Release, return res, nil } -// Install will install a Chart +// Install installs a Chart func (i *InstallOpts) Install(cfg *Config) (string, error) { ctx := context.Background() opts := retry.Options{ @@ -208,6 +216,11 @@ func (i *InstallOpts) tryInstall(cfg *action.Configuration) (string, error) { client.Timeout = i.Timeout client.Version = i.Version client.Atomic = i.Atomic + // for helm template + if i.DryRun != nil { + client.DryRun = *i.DryRun + client.OutputDir = i.OutputDir + } if client.Timeout == 0 { client.Timeout = defaultTimeout @@ -234,7 +247,7 @@ func (i *InstallOpts) tryInstall(cfg *action.Configuration) (string, error) { ctx := context.Background() _, cancel := context.WithCancel(ctx) - // Set up channel on which to send signal notifications. + // Set up channel through which to send signal notifications. // We must use a buffered channel or risk missing the signal // if we're not ready to receive when the signal is sent. cSignal := make(chan os.Signal, 2) @@ -252,7 +265,7 @@ func (i *InstallOpts) tryInstall(cfg *action.Configuration) (string, error) { return released.Info.Notes, nil } -// Uninstall will uninstall a Chart +// Uninstall uninstalls a Chart func (i *InstallOpts) Uninstall(cfg *Config) error { ctx := context.Background() opts := retry.Options{ @@ -282,12 +295,13 @@ func (i *InstallOpts) tryUninstall(cfg *action.Configuration) error { client := action.NewUninstall(cfg) client.Wait = i.Wait client.Timeout = defaultTimeout + client.DisableHooks = i.DisableHooks // Create context and prepare the handle of SIGTERM ctx := context.Background() _, cancel := context.WithCancel(ctx) - // Set up channel on which to send signal notifications. + // Set up channel through which to send signal notifications. // We must use a buffered channel or risk missing the signal // if we're not ready to receive when the signal is sent. cSignal := make(chan os.Signal, 2) @@ -442,7 +456,7 @@ func (i *InstallOpts) tryUpgrade(cfg *action.Configuration) (string, error) { ctx := context.Background() _, cancel := context.WithCancel(ctx) - // Set up channel on which to send signal notifications. + // Set up channel through which to send signal notifications. // We must use a buffered channel or risk missing the signal // if we're not ready to receive when the signal is sent. cSignal := make(chan os.Signal, 2) @@ -548,8 +562,19 @@ func GetQuiteLog() action.DebugLog { return func(format string, v ...interface{}) {} } -func GetVerboseLog(out io.Writer) action.DebugLog { +func GetVerboseLog() action.DebugLog { return func(format string, v ...interface{}) { - fmt.Fprintf(out, format+"\n", v...) + klog.Infof(format+"\n", v...) + } +} + +// GetValues gives an implementation of 'helm get values' for target release +func GetValues(release string, cfg *Config) (map[string]interface{}, error) { + actionConfig, err := NewActionConfig(cfg) + if err != nil { + return nil, err } + client := action.NewGetValues(actionConfig) + client.AllValues = true + return client.Run(release) } diff --git a/internal/cli/util/helm/helm_test.go b/internal/cli/util/helm/helm_test.go index f6618c3a9..56addc920 100644 --- a/internal/cli/util/helm/helm_test.go +++ b/internal/cli/util/helm/helm_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package helm diff --git a/internal/cli/util/helm/suite_test.go b/internal/cli/util/helm/suite_test.go index 7570dba14..8c628ef8c 100644 --- a/internal/cli/util/helm/suite_test.go +++ b/internal/cli/util/helm/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package helm diff --git a/internal/cli/util/log.go b/internal/cli/util/log.go new file mode 100644 index 000000000..f1ee506ad --- /dev/null +++ b/internal/cli/util/log.go @@ -0,0 +1,88 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "flag" + "fmt" + "path/filepath" + "strings" + "time" + + "github.com/spf13/pflag" + "k8s.io/klog/v2" + + "github.com/apecloud/kubeblocks/internal/cli/types" +) + +func EnableLogToFile(fs *pflag.FlagSet) error { + logFile, err := getCliLogFile() + if err != nil { + return err + } + + setFlag := func(kv map[string]string) { + for k, v := range kv { + _ = fs.Set(k, v) + } + } + + if klog.V(1).Enabled() { + // if log is enabled, write log to standard output and log file + setFlag(map[string]string{ + "alsologtostderr": "true", + "logtostderr": "false", + "log-file": logFile, + }) + } else { + // if log is not enabled, enable it and write log to file + setFlag(map[string]string{ + "v": "1", + "logtostderr": "false", + "alsologtostderr": "false", + "log-file": logFile, + }) + } + return nil +} + +func getCliLogFile() (string, error) { + homeDir, err := GetCliHomeDir() + if err != nil { + return "", err + } + return filepath.Join(homeDir, fmt.Sprintf("%s-%s.log", types.DefaultLogFilePrefix, time.Now().Format("2006-01-02"))), nil +} + +// AddKlogFlags adds flags from k8s.io/klog +// marks the flags as hidden to avoid showing them in help +func AddKlogFlags(fs *pflag.FlagSet) { + local := flag.NewFlagSet("klog", flag.ExitOnError) + klog.InitFlags(local) + local.VisitAll(func(f *flag.Flag) { + f.Name = strings.ReplaceAll(f.Name, "_", "-") + if fs.Lookup(f.Name) != nil { + return + } + newFlag := pflag.PFlagFromGoFlag(f) + newFlag.Hidden = true + fs.AddFlag(newFlag) + }) +} diff --git a/internal/cli/util/prompt/prompt.go b/internal/cli/util/prompt/prompt.go index 6b6b90f26..381f2bfb0 100644 --- a/internal/cli/util/prompt/prompt.go +++ b/internal/cli/util/prompt/prompt.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package prompt diff --git a/internal/cli/util/prompt/prompt_test.go b/internal/cli/util/prompt/prompt_test.go index 855580c98..d12b4b8c3 100644 --- a/internal/cli/util/prompt/prompt_test.go +++ b/internal/cli/util/prompt/prompt_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package prompt diff --git a/internal/cli/util/provider.go b/internal/cli/util/provider.go index 266a99468..d5b8cdae9 100644 --- a/internal/cli/util/provider.go +++ b/internal/cli/util/provider.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -81,17 +84,18 @@ var ( // GetK8sProvider returns the k8s provider func GetK8sProvider(version string, client kubernetes.Interface) (K8sProvider, error) { - nodes, err := client.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) - if err != nil { - return UnknownProvider, err - } - - provider := GetK8sProviderFromNodes(nodes) + // get provider from version first + provider := GetK8sProviderFromVersion(version) if provider != UnknownProvider { return provider, nil } - return GetK8sProviderFromVersion(version), nil + // if provider is unknown, get provider from node + nodes, err := client.CoreV1().Nodes().List(context.Background(), metav1.ListOptions{}) + if err != nil { + return UnknownProvider, err + } + return GetK8sProviderFromNodes(nodes), nil } // GetK8sProviderFromNodes get k8s provider from node.spec.providerID @@ -115,7 +119,7 @@ func GetK8sProviderFromNodes(nodes *corev1.NodeList) K8sProvider { return UnknownProvider } -// GetK8sProviderFromVersion get k8s provider from field GitVersion in cluster server version +// GetK8sProviderFromVersion gets k8s provider from field GitVersion in cluster server version func GetK8sProviderFromVersion(version string) K8sProvider { for provider, reg := range k8sVersionRegex { match, err := regexp.Match(reg, []byte(version)) @@ -129,7 +133,7 @@ func GetK8sProviderFromVersion(version string) K8sProvider { return UnknownProvider } -func GetK8sVersion(version string) string { +func GetK8sSemVer(version string) string { removeFirstChart := func(v string) string { if len(v) == 0 { return v diff --git a/internal/cli/util/provider_test.go b/internal/cli/util/provider_test.go index 5e9568164..0dd1adb18 100644 --- a/internal/cli/util/provider_test.go +++ b/internal/cli/util/provider_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -125,7 +128,7 @@ var _ = Describe("provider util", func() { for _, c := range cases { By(c.description) - Expect(GetK8sVersion(c.version)).Should(Equal(c.expectVersion)) + Expect(GetK8sSemVer(c.version)).Should(Equal(c.expectVersion)) client := testing.FakeClientSet(c.nodes) p, err := GetK8sProvider(c.version, client) Expect(err).ShouldNot(HaveOccurred()) diff --git a/internal/cli/util/suite_test.go b/internal/cli/util/suite_test.go index 65af1d282..ab1fa6284 100644 --- a/internal/cli/util/suite_test.go +++ b/internal/cli/util/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/cli/util/util.go b/internal/cli/util/util.go index ea1533671..e6e7458e0 100644 --- a/internal/cli/util/util.go +++ b/internal/cli/util/util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -21,6 +24,7 @@ import ( "crypto/rand" "crypto/rsa" "crypto/x509" + "encoding/json" "encoding/pem" "fmt" "io" @@ -37,17 +41,20 @@ import ( "text/template" "time" + "github.com/fatih/color" "github.com/go-logr/logr" - "github.com/google/uuid" "github.com/pkg/errors" + "github.com/pmezard/go-difflib/difflib" "golang.org/x/crypto/ssh" corev1 "k8s.io/api/core/v1" apiextv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" apiruntime "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" + k8sapitypes "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/duration" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" @@ -66,19 +73,13 @@ import ( "github.com/apecloud/kubeblocks/internal/constant" ) -func init() { - if _, err := GetCliHomeDir(); err != nil { - fmt.Println("Failed to create kbcli home dir:", err) - } -} - // CloseQuietly closes `io.Closer` quietly. Very handy and helpful for code // quality too. func CloseQuietly(d io.Closer) { _ = d.Close() } -// GetCliHomeDir return kbcli home dir +// GetCliHomeDir returns kbcli home dir func GetCliHomeDir() (string, error) { var cliHome string if custom := os.Getenv(types.CliHomeEnv); custom != "" { @@ -138,7 +139,7 @@ func GetPublicIP() (string, error) { return string(body), nil } -// MakeSSHKeyPair make a pair of public and private keys for SSH access. +// MakeSSHKeyPair makes a pair of public and private keys for SSH access. // Public key is encoded in the format for inclusion in an OpenSSH authorized_keys file. // Private Key generated is PEM encoded func MakeSSHKeyPair(pubKeyPath, privateKeyPath string) error { @@ -205,10 +206,6 @@ func DoWithRetry(ctx context.Context, logger logr.Logger, operation func() error return err } -func GenRequestID() string { - return uuid.New().String() -} - func PrintGoTemplate(wr io.Writer, tpl string, values interface{}) error { tmpl, err := template.New("output").Parse(tpl) if err != nil { @@ -222,7 +219,7 @@ func PrintGoTemplate(wr io.Writer, tpl string, values interface{}) error { return nil } -// SetKubeConfig set KUBECONFIG environment +// SetKubeConfig sets KUBECONFIG environment func SetKubeConfig(cfg string) error { return os.Setenv("KUBECONFIG", cfg) } @@ -255,17 +252,17 @@ func GVRToString(gvr schema.GroupVersionResource) string { return strings.Join([]string{gvr.Resource, gvr.Version, gvr.Group}, ".") } -// GetNodeByName choose node by name from a node array +// GetNodeByName chooses node by name from a node array func GetNodeByName(nodes []*corev1.Node, name string) *corev1.Node { for _, node := range nodes { if node.Name == name { return node } } - return &corev1.Node{} + return nil } -// ResourceIsEmpty check if resource is empty or not +// ResourceIsEmpty checks if resource is empty or not func ResourceIsEmpty(res *resource.Quantity) bool { resStr := res.String() if resStr == "0" || resStr == "" { @@ -290,7 +287,7 @@ func GetPodStatus(pods []*corev1.Pod) (running, waiting, succeeded, failed int) return } -// OpenBrowser will open browser by url in different OS system +// OpenBrowser opens browser with url in different OS system func OpenBrowser(url string) error { var err error switch runtime.GOOS { @@ -307,10 +304,15 @@ func OpenBrowser(url string) error { } func TimeFormat(t *metav1.Time) string { + return TimeFormatWithDuration(t, time.Minute) +} + +// TimeFormatWithDuration formats time with specified precision +func TimeFormatWithDuration(t *metav1.Time, duration time.Duration) string { if t == nil || t.IsZero() { return "" } - return TimeTimeFormat(t.Time) + return TimeTimeFormatWithDuration(t.Time, duration) } func TimeTimeFormat(t time.Time) string { @@ -318,6 +320,27 @@ func TimeTimeFormat(t time.Time) string { return t.Format(layout) } +func timeLayout(precision time.Duration) string { + layout := "Jan 02,2006 15:04 UTC-0700" + switch precision { + case time.Second: + layout = "Jan 02,2006 15:04:05 UTC-0700" + case time.Millisecond: + layout = "Jan 02,2006 15:04:05.000 UTC-0700" + } + return layout +} + +func TimeTimeFormatWithDuration(t time.Time, precision time.Duration) string { + layout := timeLayout(precision) + return t.Format(layout) +} + +func TimeParse(t string, precision time.Duration) (time.Time, error) { + layout := timeLayout(precision) + return time.Parse(layout, t) +} + // GetHumanReadableDuration returns a succinct representation of the provided startTime and endTime // with limited precision for consumption by humans. func GetHumanReadableDuration(startTime metav1.Time, endTime metav1.Time) string { @@ -335,7 +358,7 @@ func GetHumanReadableDuration(startTime metav1.Time, endTime metav1.Time) string return duration.HumanDuration(d) } -// CheckEmpty check if string is empty, if yes, return for displaying +// CheckEmpty checks if string is empty, if yes, returns for displaying func CheckEmpty(str string) string { if len(str) == 0 { return types.None @@ -343,7 +366,7 @@ func CheckEmpty(str string) string { return str } -// BuildLabelSelectorByNames build the label selector by instance names, the label selector is +// BuildLabelSelectorByNames builds the label selector by instance names, the label selector is // like "instance-key in (name1, name2)" func BuildLabelSelectorByNames(selector string, names []string) string { if len(names) == 0 { @@ -445,7 +468,7 @@ func GetConfigTemplateListWithResource(cComponents []appsv1alpha1.ClusterCompone return validConfigSpecs, nil } -// GetResourceObjectFromGVR query the resource object using GVR. +// GetResourceObjectFromGVR queries the resource object using GVR. func GetResourceObjectFromGVR(gvr schema.GroupVersionResource, key client.ObjectKey, client dynamic.Interface, k8sObj interface{}) error { unstructuredObj, err := client. Resource(gvr). @@ -499,7 +522,7 @@ func enableReconfiguring(component *appsv1alpha1.ClusterComponentDefinition) boo return false } -// IsSupportReconfigureParams check whether all updated parameters belong to config template parameters. +// IsSupportReconfigureParams checks whether all updated parameters belong to config template parameters. func IsSupportReconfigureParams(tpl appsv1alpha1.ComponentConfigSpec, values map[string]string, cli dynamic.Interface) (bool, error) { var ( err error @@ -554,7 +577,7 @@ func getIPLocation() (string, error) { return string(location[:len(location)-1]), nil } -// GetHelmChartRepoURL get helm chart repo, we will choose one from GitHub and GitLab based on the IP location +// GetHelmChartRepoURL gets helm chart repo, chooses one from GitHub and GitLab based on the IP location func GetHelmChartRepoURL() string { if types.KubeBlocksChartURL == testing.KubeBlocksChartURL { return testing.KubeBlocksChartURL @@ -640,20 +663,6 @@ func GetExposeAnnotations(provider K8sProvider, exposeType ExposeType) (map[stri return annotations, nil } -func GetK8SProvider(client kubernetes.Interface) (K8sProvider, error) { - versionInfo, err := GetVersionInfo(client) - if err != nil { - return "", err - } - - versionErr := fmt.Errorf("failed to get kubernetes version") - k8sVersionStr, ok := versionInfo[KubernetesApp] - if !ok { - return "", versionErr - } - return GetK8sProvider(k8sVersionStr, client) -} - // BuildAddonReleaseName returns the release name of addon, its f func BuildAddonReleaseName(addon string) string { return fmt.Sprintf("%s-%s", types.AddonReleasePrefix, addon) @@ -661,9 +670,218 @@ func BuildAddonReleaseName(addon string) string { // CombineLabels combines labels into a string func CombineLabels(labels map[string]string) string { - var labelStr string + var labelStr []string for k, v := range labels { - labelStr += fmt.Sprintf("%s=%s,", k, v) + labelStr = append(labelStr, fmt.Sprintf("%s=%s", k, v)) + } + + // sort labelStr to make sure the order is stable + sort.Strings(labelStr) + + return strings.Join(labelStr, ",") +} + +func BuildComponentNameLabels(prefix string, names []string) string { + return buildLabelSelectors(prefix, constant.KBAppComponentLabelKey, names) +} + +// buildLabelSelectors builds the label selector by given label key, the label selector is +// like "label-key in (name1, name2)" +func buildLabelSelectors(prefix string, key string, names []string) string { + if len(names) == 0 { + return prefix } - return strings.TrimSuffix(labelStr, ",") + + label := fmt.Sprintf("%s in (%s)", key, strings.Join(names, ",")) + if len(prefix) == 0 { + return label + } else { + return prefix + "," + label + } +} + +// NewOpsRequestForReconfiguring returns a new common OpsRequest for Reconfiguring operation +func NewOpsRequestForReconfiguring(opsName, namespace, clusterName string) *appsv1alpha1.OpsRequest { + return &appsv1alpha1.OpsRequest{ + TypeMeta: metav1.TypeMeta{ + APIVersion: fmt.Sprintf("%s/%s", types.AppsAPIGroup, types.AppsAPIVersion), + Kind: types.KindOps, + }, + ObjectMeta: metav1.ObjectMeta{ + Name: opsName, + Namespace: namespace, + }, + Spec: appsv1alpha1.OpsRequestSpec{ + ClusterRef: clusterName, + Type: appsv1alpha1.ReconfiguringType, + Reconfigure: &appsv1alpha1.Reconfigure{}, + }, + } +} +func ConvertObjToUnstructured(obj any) (*unstructured.Unstructured, error) { + var ( + contentBytes []byte + err error + unstructuredObj = &unstructured.Unstructured{} + ) + + if contentBytes, err = json.Marshal(obj); err != nil { + return nil, err + } + if err = json.Unmarshal(contentBytes, unstructuredObj); err != nil { + return nil, err + } + return unstructuredObj, nil +} + +func CreateResourceIfAbsent( + dynamic dynamic.Interface, + gvr schema.GroupVersionResource, + namespace string, + unstructuredObj *unstructured.Unstructured) error { + objectName, isFound, err := unstructured.NestedString(unstructuredObj.Object, "metadata", "name") + if !isFound || err != nil { + return err + } + objectByte, err := json.Marshal(unstructuredObj) + if err != nil { + return err + } + if _, err = dynamic.Resource(gvr).Namespace(namespace).Patch( + context.TODO(), objectName, k8sapitypes.MergePatchType, + objectByte, metav1.PatchOptions{}); err != nil { + if apierrors.IsNotFound(err) { + if _, err = dynamic.Resource(gvr).Namespace(namespace).Create( + context.TODO(), unstructuredObj, metav1.CreateOptions{}); err != nil { + return err + } + } else { + return err + } + } + return nil +} + +func BuildClusterDefinitionRefLable(prefix string, clusterDef []string) string { + return buildLabelSelectors(prefix, constant.AppNameLabelKey, clusterDef) +} + +// IsWindows returns true if the kbcli runtime situation is windows +func IsWindows() bool { + return runtime.GOOS == types.GoosWindows +} + +func GetUnifiedDiffString(original, edited string) (string, error) { + diff := difflib.UnifiedDiff{ + A: difflib.SplitLines(original), + B: difflib.SplitLines(edited), + FromFile: "Original", + ToFile: "Current", + Context: 3, + } + return difflib.GetUnifiedDiffString(diff) +} + +func DisplayDiffWithColor(out io.Writer, diffText string) { + for _, line := range difflib.SplitLines(diffText) { + switch { + case strings.HasPrefix(line, "---"), strings.HasPrefix(line, "+++"): + line = color.HiYellowString(line) + case strings.HasPrefix(line, "@@"): + line = color.HiBlueString(line) + case strings.HasPrefix(line, "-"): + line = color.RedString(line) + case strings.HasPrefix(line, "+"): + line = color.GreenString(line) + } + fmt.Fprint(out, line) + } +} + +// BuildTolerations toleration format: key=value:effect or key:effect, +func BuildTolerations(raw []string) ([]interface{}, error) { + tolerations := make([]interface{}, 0) + for _, tolerationRaw := range raw { + for _, entries := range strings.Split(tolerationRaw, ",") { + toleration := make(map[string]interface{}) + parts := strings.Split(entries, ":") + if len(parts) != 2 { + return tolerations, fmt.Errorf("invalid toleration %s", entries) + } + toleration["effect"] = parts[1] + + partsKV := strings.Split(parts[0], "=") + switch len(partsKV) { + case 1: + toleration["operator"] = "Exists" + toleration["key"] = partsKV[0] + case 2: + toleration["operator"] = "Equal" + toleration["key"] = partsKV[0] + toleration["value"] = partsKV[1] + default: + return tolerations, fmt.Errorf("invalid toleration %s", entries) + } + tolerations = append(tolerations, toleration) + } + } + return tolerations, nil +} + +// BuildNodeAffinity build node affinity from node labels +func BuildNodeAffinity(nodeLabels map[string]string) *corev1.NodeAffinity { + var nodeAffinity *corev1.NodeAffinity + + var matchExpressions []corev1.NodeSelectorRequirement + for key, value := range nodeLabels { + values := strings.Split(value, ",") + matchExpressions = append(matchExpressions, corev1.NodeSelectorRequirement{ + Key: key, + Operator: corev1.NodeSelectorOpIn, + Values: values, + }) + } + if len(matchExpressions) > 0 { + nodeSelectorTerm := corev1.NodeSelectorTerm{ + MatchExpressions: matchExpressions, + } + nodeAffinity = &corev1.NodeAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{ + { + Preference: nodeSelectorTerm, + }, + }, + } + } + + return nodeAffinity +} + +// BuildPodAntiAffinity build pod anti affinity from topology keys +func BuildPodAntiAffinity(podAntiAffinityStrategy string, topologyKeys []string) *corev1.PodAntiAffinity { + var podAntiAffinity *corev1.PodAntiAffinity + var podAffinityTerms []corev1.PodAffinityTerm + for _, topologyKey := range topologyKeys { + podAffinityTerms = append(podAffinityTerms, corev1.PodAffinityTerm{ + TopologyKey: topologyKey, + }) + } + if podAntiAffinityStrategy == string(appsv1alpha1.Required) { + podAntiAffinity = &corev1.PodAntiAffinity{ + RequiredDuringSchedulingIgnoredDuringExecution: podAffinityTerms, + } + } else { + var weightedPodAffinityTerms []corev1.WeightedPodAffinityTerm + for _, podAffinityTerm := range podAffinityTerms { + weightedPodAffinityTerms = append(weightedPodAffinityTerms, corev1.WeightedPodAffinityTerm{ + Weight: 100, + PodAffinityTerm: podAffinityTerm, + }) + } + podAntiAffinity = &corev1.PodAntiAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: weightedPodAffinityTerms, + } + } + + return podAntiAffinity } diff --git a/internal/cli/util/util_test.go b/internal/cli/util/util_test.go index c474e83b5..5c5708853 100644 --- a/internal/cli/util/util_test.go +++ b/internal/cli/util/util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -84,7 +87,7 @@ var _ = Describe("util", func() { "spec": map[string]interface{}{ "backupPolicyName": "backup-policy-demo", "backupType": "full", - "ttl": "168h0m0s", + "ttl": "7d", }, }, } @@ -108,7 +111,10 @@ var _ = Describe("util", func() { testFn := func(name string) bool { n := GetNodeByName(nodes, name) - return n.Name == name + if n != nil { + return n.Name == name + } + return false } Expect(testFn("test")).Should(BeTrue()) Expect(testFn("non-exists")).Should(BeFalse()) @@ -138,6 +144,9 @@ var _ = Describe("util", func() { t, _ := time.Parse(time.RFC3339, "2023-01-04T01:00:00.000Z") metav1Time := metav1.Time{Time: t} Expect(TimeFormat(&metav1Time)).Should(Equal("Jan 04,2023 01:00 UTC+0000")) + Expect(TimeFormatWithDuration(&metav1Time, time.Minute)).Should(Equal("Jan 04,2023 01:00 UTC+0000")) + Expect(TimeFormatWithDuration(&metav1Time, time.Second)).Should(Equal("Jan 04,2023 01:00:00 UTC+0000")) + Expect(TimeFormatWithDuration(&metav1Time, time.Millisecond)).Should(Equal("Jan 04,2023 01:00:00.000 UTC+0000")) }) It("CheckEmpty", func() { @@ -249,4 +258,48 @@ var _ = Describe("util", func() { It("get helm chart repo url", func() { Expect(GetHelmChartRepoURL()).ShouldNot(BeEmpty()) }) + + It("new OpsRequest for Reconfiguring ", func() { + Expect(NewOpsRequestForReconfiguring("logs", "test", "cluster")).ShouldNot(BeNil()) + }) + + It("convert obj to unstructured ", func() { + unstructuredObj, err := ConvertObjToUnstructured(testing.FakeConfigMap("cm-test")) + Expect(err).ShouldNot(HaveOccurred()) + Expect(unstructuredObj.Object).Should(HaveLen(4)) + + _, err = ConvertObjToUnstructured(struct{ name string }{name: "test"}) + Expect(err).Should(HaveOccurred()) + }) + + It("test build toleration", func() { + validRaws := []string{"dev=true:NoSchedule,large:NoSchedule"} + tolerations, err := BuildTolerations(validRaws) + Expect(err).Should(BeNil()) + Expect(len(tolerations)).Should(Equal(2)) + + // optimize these codes + invalidRaws := []string{"dev=true"} + _, err = BuildTolerations(invalidRaws) + Expect(err).Should(HaveOccurred()) + }) + + It("test build node affinity", func() { + nodeLabels := make(map[string]string) + Expect(BuildNodeAffinity(nodeLabels)).Should(BeNil()) + + nodeLabels["testNodeLabels"] = "testNodeLabels" + Expect(BuildNodeAffinity(nodeLabels)).ShouldNot(BeNil()) + }) + + It("test build pod affinity", func() { + topologyKey := "testTopologyKey" + + topologyKeys := []string{topologyKey} + podAntiAffinityStrategy := "testPodAntiAffinityStrategy" + podAntiAffinity := BuildPodAntiAffinity(podAntiAffinityStrategy, topologyKeys) + Expect(podAntiAffinity).ShouldNot(BeNil()) + Expect(podAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight).ShouldNot(BeNil()) + Expect(podAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].PodAffinityTerm.TopologyKey).Should(Equal(topologyKey)) + }) }) diff --git a/internal/cli/util/version.go b/internal/cli/util/version.go index bcbcd116f..e0cc10e7f 100644 --- a/internal/cli/util/version.go +++ b/internal/cli/util/version.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -30,39 +33,37 @@ import ( "github.com/apecloud/kubeblocks/version" ) -type AppName string - -const ( - KubernetesApp AppName = "Kubernetes" - KubeBlocksApp AppName = "KubeBlocks" - KBCLIApp AppName = "kbcli" -) +type Version struct { + KubeBlocks string + Kubernetes string + Cli string +} -// GetVersionInfo get application version include KubeBlocks, CLI and kubernetes -func GetVersionInfo(client kubernetes.Interface) (map[AppName]string, error) { +// GetVersionInfo gets version include KubeBlocks, CLI and kubernetes +func GetVersionInfo(client kubernetes.Interface) (Version, error) { var err error - versionInfo := map[AppName]string{ - KBCLIApp: version.GetVersion(), + version := Version{ + Cli: version.GetVersion(), } if client == nil || reflect.ValueOf(client).IsNil() { - return versionInfo, nil + return version, nil } - if versionInfo[KubernetesApp], err = getK8sVersion(client.Discovery()); err != nil { - return versionInfo, err + if version.Kubernetes, err = GetK8sVersion(client.Discovery()); err != nil { + return version, err } - if versionInfo[KubeBlocksApp], err = getKubeBlocksVersion(client); err != nil { - return versionInfo, err + if version.KubeBlocks, err = getKubeBlocksVersion(client); err != nil { + return version, err } - return versionInfo, nil + return version, nil } -// getKubeBlocksVersion get KubeBlocks version +// getKubeBlocksVersion gets KubeBlocks version func getKubeBlocksVersion(client kubernetes.Interface) (string, error) { - deploy, err := getKubeBlocksDeploy(client) + deploy, err := GetKubeBlocksDeploy(client) if err != nil || deploy == nil { return "", err } @@ -79,8 +80,8 @@ func getKubeBlocksVersion(client kubernetes.Interface) (string, error) { return v, nil } -// getK8sVersion get k8s server version -func getK8sVersion(discoveryClient discovery.DiscoveryInterface) (string, error) { +// GetK8sVersion gets k8s server version +func GetK8sVersion(discoveryClient discovery.DiscoveryInterface) (string, error) { if discoveryClient == nil { return "", nil } @@ -96,9 +97,9 @@ func getK8sVersion(discoveryClient discovery.DiscoveryInterface) (string, error) return "", nil } -// getKubeBlocksDeploy get KubeBlocks deployments, now one kubernetes cluster +// GetKubeBlocksDeploy gets KubeBlocks deployments, now one kubernetes cluster // only support one KubeBlocks -func getKubeBlocksDeploy(client kubernetes.Interface) (*appsv1.Deployment, error) { +func GetKubeBlocksDeploy(client kubernetes.Interface) (*appsv1.Deployment, error) { deploys, err := client.AppsV1().Deployments(metav1.NamespaceAll).List(context.Background(), metav1.ListOptions{ LabelSelector: "app.kubernetes.io/name=" + types.KubeBlocksChartName, }) diff --git a/internal/cli/util/version_test.go b/internal/cli/util/version_test.go index 838dbcbe8..94c2f130a 100644 --- a/internal/cli/util/version_test.go +++ b/internal/cli/util/version_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util @@ -29,42 +32,38 @@ const kbVersion = "0.3.0" var _ = Describe("version util", func() { It("get version info when client is nil", func() { - info, err := GetVersionInfo(nil) + v, err := GetVersionInfo(nil) Expect(err).Should(Succeed()) - Expect(info).ShouldNot(BeEmpty()) - Expect(info[KubeBlocksApp]).Should(BeEmpty()) - Expect(info[KubernetesApp]).Should(BeEmpty()) - Expect(info[KBCLIApp]).ShouldNot(BeEmpty()) + Expect(v.KubeBlocks).Should(BeEmpty()) + Expect(v.Kubernetes).Should(BeEmpty()) + Expect(v.Cli).ShouldNot(BeEmpty()) }) It("get version info when client variable is a nil pointer", func() { var client *kubernetes.Clientset - info, err := GetVersionInfo(client) + v, err := GetVersionInfo(client) Expect(err).Should(Succeed()) - Expect(info).ShouldNot(BeEmpty()) - Expect(info[KubeBlocksApp]).Should(BeEmpty()) - Expect(info[KubernetesApp]).Should(BeEmpty()) - Expect(info[KBCLIApp]).ShouldNot(BeEmpty()) + Expect(v.KubeBlocks).Should(BeEmpty()) + Expect(v.Kubernetes).Should(BeEmpty()) + Expect(v.Cli).ShouldNot(BeEmpty()) }) - It("get version info when KubeBlocks is deployed", func() { + It("get vsion info when KubeBlocks is deployed", func() { client := testing.FakeClientSet(testing.FakeKBDeploy(kbVersion)) - info, err := GetVersionInfo(client) + v, err := GetVersionInfo(client) Expect(err).Should(Succeed()) - Expect(info).ShouldNot(BeEmpty()) - Expect(info[KubeBlocksApp]).Should(Equal(kbVersion)) - Expect(info[KubernetesApp]).ShouldNot(BeEmpty()) - Expect(info[KBCLIApp]).ShouldNot(BeEmpty()) + Expect(v.KubeBlocks).Should(Equal(kbVersion)) + Expect(v.Kubernetes).ShouldNot(BeEmpty()) + Expect(v.Cli).ShouldNot(BeEmpty()) }) It("get version info when KubeBlocks is not deployed", func() { client := testing.FakeClientSet() - info, err := GetVersionInfo(client) + v, err := GetVersionInfo(client) Expect(err).Should(Succeed()) - Expect(info).ShouldNot(BeEmpty()) - Expect(info[KubeBlocksApp]).Should(BeEmpty()) - Expect(info[KubernetesApp]).ShouldNot(BeEmpty()) - Expect(info[KBCLIApp]).ShouldNot(BeEmpty()) + Expect(v.KubeBlocks).Should(BeEmpty()) + Expect(v.Kubernetes).ShouldNot(BeEmpty()) + Expect(v.Cli).ShouldNot(BeEmpty()) }) It("getKubeBlocksVersion", func() { @@ -79,9 +78,9 @@ var _ = Describe("version util", func() { Expect(err).Should(Succeed()) }) - It("getK8sVersion", func() { + It("GetK8sVersion", func() { client := testing.FakeClientSet() - v, err := getK8sVersion(client.Discovery()) + v, err := GetK8sVersion(client.Discovery()) Expect(v).ShouldNot(BeEmpty()) Expect(err).Should(Succeed()) }) diff --git a/internal/configuration/config.go b/internal/configuration/config.go index cd50ee978..078bcfd6d 100644 --- a/internal/configuration/config.go +++ b/internal/configuration/config.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -29,6 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/configuration/util" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/unstructured" ) @@ -168,14 +172,14 @@ type dataConfig struct { // Option is config for Option CfgOption - // cfgWrapper reference configuration template or configmap + // cfgWrapper references configuration template or configmap *cfgWrapper } func NewConfigLoader(option CfgOption) (*dataConfig, error) { loader, ok := loaderProvider[option.Type] if !ok { - return nil, MakeError("not support config type: %s", option.Type) + return nil, MakeError("not supported config type: %s", option.Type) } meta, err := loader(option) @@ -238,51 +242,6 @@ type ConfigPatchInfo struct { LastVersion *cfgWrapper } -func (c *cfgWrapper) Diff(target *cfgWrapper) (*ConfigPatchInfo, error) { - fromOMap := ToSet(c.indexer) - fromNMap := ToSet(target.indexer) - - addSet := Difference(fromNMap, fromOMap) - deleteSet := Difference(fromOMap, fromNMap) - updateSet := Difference(fromOMap, deleteSet) - - reconfigureInfo := &ConfigPatchInfo{ - IsModify: false, - AddConfig: make(map[string]interface{}, addSet.Length()), - DeleteConfig: make(map[string]interface{}, deleteSet.Length()), - UpdateConfig: make(map[string][]byte, updateSet.Length()), - - Target: target, - LastVersion: c, - } - - for elem := range addSet.Iter() { - reconfigureInfo.AddConfig[elem] = target.indexer[elem].GetAllParameters() - reconfigureInfo.IsModify = true - } - - for elem := range deleteSet.Iter() { - reconfigureInfo.DeleteConfig[elem] = c.indexer[elem].GetAllParameters() - reconfigureInfo.IsModify = true - } - - for elem := range updateSet.Iter() { - old := c.indexer[elem] - new := target.indexer[elem] - - patch, err := jsonPatch(old.GetAllParameters(), new.GetAllParameters()) - if err != nil { - return nil, err - } - if len(patch) > len(emptyJSON) { - reconfigureInfo.UpdateConfig[elem] = patch - reconfigureInfo.IsModify = true - } - } - - return reconfigureInfo, nil -} - func NewCfgOptions(filename string, options ...Option) CfgOpOption { context := CfgOpOption{ FileName: filename, @@ -305,6 +264,13 @@ func WithFormatterConfig(formatConfig *appsv1alpha1.FormatterConfig) Option { } } +func NestedPrefixField(formatConfig *appsv1alpha1.FormatterConfig) string { + if formatConfig != nil && formatConfig.Format == appsv1alpha1.Ini && formatConfig.IniConfig != nil { + return formatConfig.IniConfig.SectionName + } + return "" +} + func (c *cfgWrapper) Query(jsonpath string, option CfgOpOption) ([]byte, error) { if option.AllSearch && c.fileCount > 1 { return c.queryAllCfg(jsonpath, option) @@ -323,7 +289,7 @@ func (c *cfgWrapper) Query(jsonpath string, option CfgOpOption) ([]byte, error) } } - return retrievalWithJSONPath(cfg.GetAllParameters(), jsonpath) + return util.RetrievalWithJSONPath(cfg.GetAllParameters(), jsonpath) } func (c *cfgWrapper) queryAllCfg(jsonpath string, option CfgOpOption) ([]byte, error) { @@ -332,7 +298,7 @@ func (c *cfgWrapper) queryAllCfg(jsonpath string, option CfgOpOption) ([]byte, e for filename, v := range c.indexer { tops[filename] = v.GetAllParameters() } - return retrievalWithJSONPath(tops, jsonpath) + return util.RetrievalWithJSONPath(tops, jsonpath) } func (c cfgWrapper) getConfigObject(option CfgOpOption) unstructured.ConfigObject { @@ -363,37 +329,12 @@ func FromCMKeysSelector(keys []string) *set.LinkedHashSetString { return cmKeySet } -func CreateMergePatch(oldVersion, newVersion interface{}, option CfgOption) (*ConfigPatchInfo, error) { - - ok, err := compareWithConfig(oldVersion, newVersion, option) - if err != nil { - return nil, err - } else if ok { - return &ConfigPatchInfo{IsModify: false}, err - } - - old, err := NewConfigLoader(withOption(option, oldVersion)) - if err != nil { - return nil, WrapError(err, "failed to create config: [%s]", oldVersion) - } - - new, err := NewConfigLoader(withOption(option, newVersion)) - if err != nil { - return nil, WrapError(err, "failed to create config: [%s]", oldVersion) - } - - return old.Diff(new.cfgWrapper) -} - func GenerateVisualizedParamsList(configPatch *ConfigPatchInfo, formatConfig *appsv1alpha1.FormatterConfig, sets *set.LinkedHashSetString) []VisualizedParam { if !configPatch.IsModify { return nil } - var trimPrefix = "" - if formatConfig != nil && formatConfig.Format == appsv1alpha1.Ini && formatConfig.IniConfig != nil { - trimPrefix = formatConfig.IniConfig.SectionName - } + var trimPrefix = NestedPrefixField(formatConfig) r := make([]VisualizedParam, 0) r = append(r, generateUpdateParam(configPatch.UpdateConfig, trimPrefix, sets)...) @@ -475,7 +416,7 @@ func generateUpdateKeyParam(files map[string]interface{}, trimPrefix string, upd return r } -// isQuotesString check whether a string is quoted. +// isQuotesString checks whether a string is quoted. func isQuotesString(str string) bool { const ( singleQuotes = '\'' diff --git a/internal/configuration/config_manager/builder.go b/internal/configuration/config_manager/builder.go index 31a872ed9..0abc784b6 100644 --- a/internal/configuration/config_manager/builder.go +++ b/internal/configuration/config_manager/builder.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/builder_test.go b/internal/configuration/config_manager/builder_test.go index 8e8ebf7cf..2804182e2 100644 --- a/internal/configuration/config_manager/builder_test.go +++ b/internal/configuration/config_manager/builder_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/dynamic_paramter_updater.go b/internal/configuration/config_manager/dynamic_parameter_updater.go similarity index 80% rename from internal/configuration/config_manager/dynamic_paramter_updater.go rename to internal/configuration/config_manager/dynamic_parameter_updater.go index 0f1bf02fc..94acd9194 100644 --- a/internal/configuration/config_manager/dynamic_paramter_updater.go +++ b/internal/configuration/config_manager/dynamic_parameter_updater.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -70,7 +73,7 @@ func NewCommandChannel(ctx context.Context, dataType, dsn string) (DynamicParamU default: // TODO mock db begin support dapper } - return nil, cfgcore.MakeError("not support type[%s]", dataType) + return nil, cfgcore.MakeError("not supported type[%s]", dataType) } func NewMysqlConnection(ctx context.Context, dsn string) (DynamicParamUpdater, error) { @@ -93,10 +96,10 @@ func NewMysqlConnection(ctx context.Context, dsn string) (DynamicParamUpdater, e ctx, cancel := context.WithTimeout(ctx, connectTimeout) defer cancel() if err := db.PingContext(ctx); err != nil { - logger.Error(err, "failed to pinging mysqld.") + logger.Error(err, "failed to ping mysqld.") return nil, err } - logger.V(1).Info("succeed to connect mysql.") + logger.V(1).Info("succeed to connect to mysql.") return &mysqlCommandChannel{db: db}, nil } @@ -132,7 +135,7 @@ func (p *pgPatroniCommandChannel) ExecCommand(ctx context.Context, command strin ) if len(args) == 0 { - return "", cfgcore.MakeError("require patroni functional.") + return "", cfgcore.MakeError("patroni is required.") } functional := args[0] @@ -144,7 +147,7 @@ func (p *pgPatroniCommandChannel) ExecCommand(ctx context.Context, command strin case Restart, Reload: return sendRestRequest(ctx, command, restPath, "POST") } - return "", cfgcore.MakeError("not support patroni functional[%s]", args[0]) + return "", cfgcore.MakeError("not supported patroni function: [%s]", args[0]) } func (p *pgPatroniCommandChannel) Close() { diff --git a/internal/configuration/config_manager/files.go b/internal/configuration/config_manager/files.go index 8343cc96f..2cd0166c1 100644 --- a/internal/configuration/config_manager/files.go +++ b/internal/configuration/config_manager/files.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/files_test.go b/internal/configuration/config_manager/files_test.go index 7b1c3089e..800b422a7 100644 --- a/internal/configuration/config_manager/files_test.go +++ b/internal/configuration/config_manager/files_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/handler.go b/internal/configuration/config_manager/handler.go index 4cdb19113..f254f43e0 100644 --- a/internal/configuration/config_manager/handler.go +++ b/internal/configuration/config_manager/handler.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -44,7 +47,7 @@ func SetLogger(zapLogger *zap.Logger) { logger = logger.WithName("configmap_volume_watcher") } -// findPidFromProcessName get parent pid +// findPidFromProcessName gets parent pid func findPidFromProcessName(processName string) (PID, error) { allProcess, err := process.Processes() if err != nil { @@ -54,9 +57,8 @@ func findPidFromProcessName(processName string) (PID, error) { psGraph := map[PID]int32{} for _, proc := range allProcess { name, err := proc.Name() - // OS X getting the name of the system process sometimes fails, - // because OS X Process.Name function depends on sysctl, - // the function requires elevated permissions. + // OS X getting the name of the system process may fail, + // because OS X Process.Name function depends on sysctl and elevated permissions if err != nil { logger.Error(err, fmt.Sprintf("failed to get process name from pid[%d], and pass", proc.Pid)) continue @@ -77,13 +79,13 @@ func findPidFromProcessName(processName string) (PID, error) { } } - return InvalidPID, cfgutil.MakeError("not find pid of process name: [%s]", processName) + return InvalidPID, cfgutil.MakeError("cannot find pid of process name: [%s]", processName) } func CreateSignalHandler(sig appsv1alpha1.SignalType, processName string) (WatchEventHandler, error) { signal, ok := allUnixSignals[sig] if !ok { - err := cfgutil.MakeError("not support unix signal: %s", sig) + err := cfgutil.MakeError("not supported unix signal: %s", sig) logger.Error(err, "failed to create signal handler") return nil, err } diff --git a/internal/configuration/config_manager/handler_test.go b/internal/configuration/config_manager/handler_test.go index c74645e8e..557a9d8fb 100644 --- a/internal/configuration/config_manager/handler_test.go +++ b/internal/configuration/config_manager/handler_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -61,7 +64,7 @@ func TestCreateSignalHandler(t *testing.T) { _, err := CreateSignalHandler(appsv1alpha1.SIGALRM, "test") require.Nil(t, err) _, err = CreateSignalHandler("NOSIGNAL", "test") - require.ErrorContains(t, err, "not support unix signal") + require.ErrorContains(t, err, "not supported unix signal") } func TestCreateExecHandler(t *testing.T) { diff --git a/internal/configuration/config_manager/handler_util.go b/internal/configuration/config_manager/handler_util.go index 7dfd9dfa7..86323cdf3 100644 --- a/internal/configuration/config_manager/handler_util.go +++ b/internal/configuration/config_manager/handler_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -71,7 +74,7 @@ func checkTPLScriptTrigger(options *appsv1alpha1.TPLScriptTrigger, cli client.Cl func checkShellTrigger(options *appsv1alpha1.ShellTrigger) error { if options.Exec == "" { - return cfgutil.MakeError("shell trigger require exec not empty!") + return cfgutil.MakeError("required shell trigger") } return nil } @@ -79,7 +82,7 @@ func checkShellTrigger(options *appsv1alpha1.ShellTrigger) error { func checkSignalTrigger(options *appsv1alpha1.UnixSignalTrigger) error { signal := options.Signal if !IsValidUnixSignal(signal) { - return cfgutil.MakeError("this special signal [%s] is not supported for now!", signal) + return cfgutil.MakeError("this special signal [%s] is not supported now.", signal) } return nil } @@ -95,7 +98,7 @@ func CreateCfgRegexFilter(regexString string) (NotifyEventFilter, error) { }, nil } -// CreateValidConfigMapFilter process configmap volume +// CreateValidConfigMapFilter processes configmap volume // https://github.com/ossrs/srs/issues/1635 func CreateValidConfigMapFilter() NotifyEventFilter { return func(event fsnotify.Event) (bool, error) { diff --git a/internal/configuration/config_manager/handler_util_test.go b/internal/configuration/config_manager/handler_util_test.go index 3dd158c6a..c7884a44e 100644 --- a/internal/configuration/config_manager/handler_util_test.go +++ b/internal/configuration/config_manager/handler_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -92,7 +95,7 @@ func TestIsSupportReload(t *testing.T) { } } -var _ = Describe("Hander Util Test", func() { +var _ = Describe("Handler Util Test", func() { var mockK8sCli *testutil.K8sClientMockHelper @@ -111,7 +114,7 @@ var _ = Describe("Hander Util Test", func() { }) Context("TestValidateReloadOptions", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { type args struct { reloadOptions *appsv1alpha1.ReloadOptions cli client.Client diff --git a/internal/configuration/config_manager/reload_util.go b/internal/configuration/config_manager/reload_util.go index 2d54ba547..5e32a6fea 100644 --- a/internal/configuration/config_manager/reload_util.go +++ b/internal/configuration/config_manager/reload_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -30,6 +33,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgutil "github.com/apecloud/kubeblocks/internal/configuration" cfgcontainer "github.com/apecloud/kubeblocks/internal/configuration/container" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/gotemplate" ) @@ -126,11 +130,11 @@ func createUpdatedParamsPatch(newVersion []string, oldVersion []string, formatCf } logger.V(1).Info(fmt.Sprintf("new version files: %v, old version files: %v", newVersion, oldVersion)) - oldData, err := fromConfigFiles(oldVersion) + oldData, err := util.FromConfigFiles(oldVersion) if err != nil { return nil, err } - newData, err := fromConfigFiles(newVersion) + newData, err := util.FromConfigFiles(newVersion) if err != nil { return nil, err } @@ -153,18 +157,6 @@ func createUpdatedParamsPatch(newVersion []string, oldVersion []string, formatCf return r, nil } -func fromConfigFiles(files []string) (map[string]string, error) { - m := make(map[string]string) - for _, file := range files { - b, err := os.ReadFile(file) - if err != nil { - return nil, err - } - m[filepath.Base(file)] = string(b) - } - return m, nil -} - func resolveLink(path string) (string, error) { logger.V(1).Info(fmt.Sprintf("resolveLink : %s", path)) diff --git a/internal/configuration/config_manager/reload_util_test.go b/internal/configuration/config_manager/reload_util_test.go index f02b2386f..b52ecd4fb 100644 --- a/internal/configuration/config_manager/reload_util_test.go +++ b/internal/configuration/config_manager/reload_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/signal.go b/internal/configuration/config_manager/signal.go index c90069f88..e2e77fc06 100644 --- a/internal/configuration/config_manager/signal.go +++ b/internal/configuration/config_manager/signal.go @@ -1,19 +1,20 @@ -//go:build linux || darwin - /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project -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 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - http://www.apache.org/licenses/LICENSE-2.0 +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. -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. +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -35,7 +36,7 @@ func sendSignal(pid PID, sig os.Signal) error { return err } - logger.Info(fmt.Sprintf("send pid[%d] to signal: %s", pid, sig.String())) + logger.Info(fmt.Sprintf("send signal:%s to process[%d]", sig.String(), pid)) err = process.Signal(sig) if err != nil { return err diff --git a/internal/configuration/config_manager/signal_darwin.go b/internal/configuration/config_manager/signal_darwin.go index 13c859997..6775f098a 100644 --- a/internal/configuration/config_manager/signal_darwin.go +++ b/internal/configuration/config_manager/signal_darwin.go @@ -1,19 +1,22 @@ //go:build darwin /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/signal_linux.go b/internal/configuration/config_manager/signal_linux.go index 8fc2ae694..f8944342d 100644 --- a/internal/configuration/config_manager/signal_linux.go +++ b/internal/configuration/config_manager/signal_linux.go @@ -1,19 +1,22 @@ //go:build linux /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/signal_test.go b/internal/configuration/config_manager/signal_test.go index 3b08a35d9..0934a89e5 100644 --- a/internal/configuration/config_manager/signal_test.go +++ b/internal/configuration/config_manager/signal_test.go @@ -1,17 +1,22 @@ +//go:build linux || darwin + /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project -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 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - http://www.apache.org/licenses/LICENSE-2.0 +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. -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. +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/signal_unknown.go b/internal/configuration/config_manager/signal_unknown.go deleted file mode 100644 index aaa1feebe..000000000 --- a/internal/configuration/config_manager/signal_unknown.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build !linux && !darwin - -/* -Copyright ApeCloud, Inc. - -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 configmanager - -import ( - "os" - "runtime" - - cfgcore "github.com/apecloud/kubeblocks/internal/configuration" -) - -func sendSignal(pid PID, sig os.Signal) error { - return cfgcore.MakeError("not support os: ", runtime.GOOS) -} diff --git a/internal/configuration/config_manager/signal_windows.go b/internal/configuration/config_manager/signal_windows.go new file mode 100644 index 000000000..e82951eb8 --- /dev/null +++ b/internal/configuration/config_manager/signal_windows.go @@ -0,0 +1,45 @@ +//go:build windows + +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package configmanager + +import ( + "os" + "syscall" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +var allUnixSignals = map[appsv1alpha1.SignalType]os.Signal{ + appsv1alpha1.SIGHUP: syscall.SIGHUP, + appsv1alpha1.SIGINT: syscall.SIGINT, + appsv1alpha1.SIGQUIT: syscall.SIGQUIT, + appsv1alpha1.SIGILL: syscall.SIGILL, + appsv1alpha1.SIGTRAP: syscall.SIGTRAP, + appsv1alpha1.SIGABRT: syscall.SIGABRT, + appsv1alpha1.SIGBUS: syscall.SIGBUS, + appsv1alpha1.SIGFPE: syscall.SIGFPE, + appsv1alpha1.SIGKILL: syscall.SIGKILL, + appsv1alpha1.SIGSEGV: syscall.SIGSEGV, + appsv1alpha1.SIGPIPE: syscall.SIGPIPE, + appsv1alpha1.SIGALRM: syscall.SIGALRM, + appsv1alpha1.SIGTERM: syscall.SIGTERM, +} diff --git a/internal/configuration/config_manager/suite_test.go b/internal/configuration/config_manager/suite_test.go index 2997ffb9e..06d4cd58d 100644 --- a/internal/configuration/config_manager/suite_test.go +++ b/internal/configuration/config_manager/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager diff --git a/internal/configuration/config_manager/volume_watcher.go b/internal/configuration/config_manager/volume_watcher.go index 007f7e3bb..56d6ad7a9 100644 --- a/internal/configuration/config_manager/volume_watcher.go +++ b/internal/configuration/config_manager/volume_watcher.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -37,7 +40,7 @@ const ( type ConfigMapVolumeWatcher struct { retryCount int - // volumeDirectory watch directory witch volumeCount + // volumeDirectory watches directory witch volumeCount volumeDirectory []string // regexSelector string @@ -86,7 +89,7 @@ func (w *ConfigMapVolumeWatcher) Close() error { func (w *ConfigMapVolumeWatcher) Run() error { if w.handler == nil { - return cfgcore.MakeError("require process event handler.") + return cfgcore.MakeError("required process event handler.") } watcher, err := fsnotify.NewWatcher() @@ -99,7 +102,7 @@ func (w *ConfigMapVolumeWatcher) Run() error { w.log.Infof("add watched fs directory: %s", d) err = watcher.Add(d) if err != nil { - return cfgcore.WrapError(err, "failed to add watch directory[%s] failed", d) + return cfgcore.WrapError(err, "failed to add watch directory[%s]", d) } } @@ -129,7 +132,7 @@ func (w *ConfigMapVolumeWatcher) loopNotifyEvent(watcher *fsnotify.Watcher, ctx case err := <-watcher.Errors: w.log.Error(err) case <-ctx.Done(): - w.log.Info("The process has received the end signal.") + w.log.Info("The process has received the exit signal.") return } } @@ -145,7 +148,7 @@ func runWithRetry(ctx context.Context, handler WatchEventHandler, event fsnotify if retryCount <= 0 { return } - logger.Errorf("event handler failed, will retry after [%d]s : %s", DefaultSleepRetryTime, err) + logger.Errorf("failed event handler, please retry after [%d]s : %s", DefaultSleepRetryTime, err) time.Sleep(time.Second * DefaultRetryCount) } } diff --git a/internal/configuration/config_manager/volume_watcher_test.go b/internal/configuration/config_manager/volume_watcher_test.go index 9cbac366d..75b1c7733 100644 --- a/internal/configuration/config_manager/volume_watcher_test.go +++ b/internal/configuration/config_manager/volume_watcher_test.go @@ -1,19 +1,22 @@ //go:build linux || darwin /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configmanager @@ -44,7 +47,7 @@ func TestConfigMapVolumeWatcherFailed(t *testing.T) { volumeWatcher := NewVolumeWatcher([]string{filepath.Join(tmpDir, "not_exist")}, context.Background(), zapLog.Sugar()) defer volumeWatcher.Close() - require.EqualError(t, volumeWatcher.Run(), "require process event handler.") + require.EqualError(t, volumeWatcher.Run(), "required process event handler.") volumeWatcher.AddHandler(func(_ context.Context, event fsnotify.Event) error { return nil }) diff --git a/internal/configuration/config_patch.go b/internal/configuration/config_patch.go new file mode 100644 index 000000000..11356165a --- /dev/null +++ b/internal/configuration/config_patch.go @@ -0,0 +1,88 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package configuration + +import "github.com/apecloud/kubeblocks/internal/configuration/util" + +func CreateMergePatch(oldVersion, newVersion interface{}, option CfgOption) (*ConfigPatchInfo, error) { + + ok, err := compareWithConfig(oldVersion, newVersion, option) + if err != nil { + return nil, err + } else if ok { + return &ConfigPatchInfo{IsModify: false}, err + } + + old, err := NewConfigLoader(withOption(option, oldVersion)) + if err != nil { + return nil, WrapError(err, "failed to create config: [%s]", oldVersion) + } + + new, err := NewConfigLoader(withOption(option, newVersion)) + if err != nil { + return nil, WrapError(err, "failed to create config: [%s]", oldVersion) + } + return difference(old.cfgWrapper, new.cfgWrapper) +} + +func difference(base *cfgWrapper, target *cfgWrapper) (*ConfigPatchInfo, error) { + fromOMap := util.ToSet(base.indexer) + fromNMap := util.ToSet(target.indexer) + + addSet := util.Difference(fromNMap, fromOMap) + deleteSet := util.Difference(fromOMap, fromNMap) + updateSet := util.Difference(fromOMap, deleteSet) + + reconfigureInfo := &ConfigPatchInfo{ + IsModify: false, + AddConfig: make(map[string]interface{}, addSet.Length()), + DeleteConfig: make(map[string]interface{}, deleteSet.Length()), + UpdateConfig: make(map[string][]byte, updateSet.Length()), + + Target: target, + LastVersion: base, + } + + for elem := range addSet.Iter() { + reconfigureInfo.AddConfig[elem] = target.indexer[elem].GetAllParameters() + reconfigureInfo.IsModify = true + } + + for elem := range deleteSet.Iter() { + reconfigureInfo.DeleteConfig[elem] = base.indexer[elem].GetAllParameters() + reconfigureInfo.IsModify = true + } + + for elem := range updateSet.Iter() { + old := base.indexer[elem] + new := target.indexer[elem] + + patch, err := util.JSONPatch(old.GetAllParameters(), new.GetAllParameters()) + if err != nil { + return nil, err + } + if len(patch) > len(emptyJSON) { + reconfigureInfo.UpdateConfig[elem] = patch + reconfigureInfo.IsModify = true + } + } + + return reconfigureInfo, nil +} diff --git a/internal/configuration/util.go b/internal/configuration/config_patch_option.go similarity index 70% rename from internal/configuration/util.go rename to internal/configuration/config_patch_option.go index 63f356b55..d676cb424 100644 --- a/internal/configuration/util.go +++ b/internal/configuration/config_patch_option.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -39,7 +42,7 @@ func compareWithConfig(left, right interface{}, option CfgOption) (bool, error) } return left.(*ConfigResource) == right.(*ConfigResource), nil default: - return false, MakeError("not support config type compare!") + return false, MakeError("not supported config type to compare") } } diff --git a/internal/configuration/util_test.go b/internal/configuration/config_patch_option_test.go similarity index 68% rename from internal/configuration/util_test.go rename to internal/configuration/config_patch_option_test.go index ebe8d6327..039409f42 100644 --- a/internal/configuration/util_test.go +++ b/internal/configuration/config_patch_option_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/config_patch_test.go b/internal/configuration/config_patch_test.go new file mode 100644 index 000000000..bd16102cb --- /dev/null +++ b/internal/configuration/config_patch_test.go @@ -0,0 +1,124 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package configuration + +import ( + "context" + "testing" + + "github.com/ghodss/yaml" + "github.com/stretchr/testify/require" + "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +func TestConfigPatch(t *testing.T) { + + cfg, err := NewConfigLoader(CfgOption{ + Type: CfgRawType, + Log: log.FromContext(context.Background()), + CfgType: appsv1alpha1.Ini, + RawData: []byte(iniConfig), + }) + + if err != nil { + t.Fatalf("new config loader failed [%v]", err) + } + + ctx := NewCfgOptions("", + func(ctx *CfgOpOption) { + // filter mysqld + ctx.IniContext = &IniContext{ + SectionName: "mysqld", + } + }) + + // ctx := NewCfgOptions("$..slow_query_log_file", "") + + result, err := cfg.Query("$..slow_query_log_file", NewCfgOptions("")) + require.Nil(t, err) + require.NotNil(t, result) + require.Equal(t, "[\"/data/mysql/mysqld-slow.log\"]", string(result)) + + require.Nil(t, + cfg.MergeFrom(map[string]interface{}{ + "slow_query_log": 1, + "server-id": 2, + "socket": "xxxxxxxxxxxxxxx", + }, ctx)) + + content, err := cfg.ToCfgContent() + require.NotNil(t, content) + require.Nil(t, err) + + newContent, exist := content[cfg.name] + require.True(t, exist) + patch, err := CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) + require.Nil(t, err) + log.Log.Info("patch : %v", patch) + require.True(t, patch.IsModify) + require.Equal(t, string(patch.UpdateConfig["raw"]), `{"mysqld":{"server-id":"2","socket":"xxxxxxxxxxxxxxx"}}`) + + { + require.Nil(t, + cfg.MergeFrom(map[string]interface{}{ + "server-id": 1, + "socket": "/data/mysql/tmp/mysqld.sock", + }, ctx)) + content, err := cfg.ToCfgContent() + require.Nil(t, err) + newContent := content[cfg.name] + // CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) + patch, err := CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) + require.Nil(t, err) + log.Log.Info("patch : %v", patch) + require.False(t, patch.IsModify) + } +} + +func TestYamlConfigPatch(t *testing.T) { + yamlContext := ` +net: + port: 2000 + bindIp: + type: "string" + trim: "whitespace" + tls: + mode: requireTLS + certificateKeyFilePassword: + type: "string" + digest: b08519162ba332985ac18204851949611ef73835ec99067b85723e10113f5c26 + digest_key: 6d795365637265744b65795374756666 +` + + patchOption := CfgOption{ + Type: CfgTplType, + CfgType: appsv1alpha1.YAML, + } + patch, err := CreateMergePatch(&ConfigResource{ConfigData: map[string]string{"test": ""}}, &ConfigResource{ConfigData: map[string]string{"test": yamlContext}}, patchOption) + require.Nil(t, err) + + yb, err := yaml.YAMLToJSON([]byte(yamlContext)) + require.Nil(t, err) + + require.Nil(t, err) + require.Equal(t, yb, patch.UpdateConfig["test"]) +} diff --git a/internal/configuration/config_patch_util.go b/internal/configuration/config_patch_util.go index 9700bb62c..8c8e6c74b 100644 --- a/internal/configuration/config_patch_util.go +++ b/internal/configuration/config_patch_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -22,12 +25,12 @@ import ( "github.com/StudioSol/set" "sigs.k8s.io/controller-runtime/pkg/log" - "github.com/apecloud/kubeblocks/internal/unstructured" - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/configuration/util" + "github.com/apecloud/kubeblocks/internal/unstructured" ) -// CreateConfigPatch creates a patch for configuration files with difference version. +// CreateConfigPatch creates a patch for configuration files with different version. func CreateConfigPatch(oldVersion, newVersion map[string]string, format appsv1alpha1.CfgFileFormat, keys []string, comparableAllFiles bool) (*ConfigPatchInfo, bool, error) { var hasFilesUpdated = false @@ -49,10 +52,10 @@ func CreateConfigPatch(oldVersion, newVersion map[string]string, format appsv1al func checkExcludeConfigDifference(oldVersion map[string]string, newVersion map[string]string, keys []string) bool { keySet := set.NewLinkedHashSetString(keys...) - leftOldKey := Difference(ToSet(oldVersion), keySet) - leftNewKey := Difference(ToSet(newVersion), keySet) + leftOldKey := util.Difference(util.ToSet(oldVersion), keySet) + leftNewKey := util.Difference(util.ToSet(newVersion), keySet) - if !EqSet(leftOldKey, leftNewKey) { + if !util.EqSet(leftOldKey, leftNewKey) { return true } @@ -82,3 +85,31 @@ func LoadRawConfigObject(data map[string]string, formatConfig *appsv1alpha1.Form } return r, nil } + +// TransformConfigFileToKeyValueMap transforms a config file in appsv1alpha1.CfgFileFormat format to a map in which the key is config name and the value is config value +// sectionName means the desired section of config file, such as [mysqld] section. +// If config file has no section structure, sectionName should be default to get all values in this config file. +func TransformConfigFileToKeyValueMap(fileName string, formatterConfig *appsv1alpha1.FormatterConfig, configData []byte) (map[string]string, error) { + oldData := map[string]string{ + fileName: "", + } + newData := map[string]string{ + fileName: string(configData), + } + keys := []string{fileName} + patchInfo, _, err := CreateConfigPatch(oldData, newData, formatterConfig.Format, keys, false) + if err != nil { + return nil, err + } + params := GenerateVisualizedParamsList(patchInfo, formatterConfig, nil) + result := make(map[string]string) + for _, param := range params { + if param.Key != fileName { + continue + } + for _, kv := range param.Parameters { + result[kv.Key] = kv.Value + } + } + return result, nil +} diff --git a/internal/configuration/config_patch_util_test.go b/internal/configuration/config_patch_util_test.go index 72c856699..1600a088e 100644 --- a/internal/configuration/config_patch_util_test.go +++ b/internal/configuration/config_patch_util_test.go @@ -1,22 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration import ( + "reflect" "testing" "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -348,3 +352,66 @@ func TestLoadRawConfigObject(t *testing.T) { }) } } + +func TestTransformConfigFileToKeyValueMap(t *testing.T) { + mysqlConfig := ` +[mysqld] +key_buffer_size=16777216 +log_error=/data/mysql/logs/mysql.log +` + mongodbConfig := ` +systemLog: + logRotate: reopen + path: /data/mongodb/logs/mongodb.log + verbosity: 0 +` + tests := []struct { + name string + fileName string + formatConfig *v1alpha1.FormatterConfig + configData []byte + expected map[string]string + }{{ + name: "mysql-test", + fileName: "my.cnf", + formatConfig: &v1alpha1.FormatterConfig{ + Format: v1alpha1.Ini, + FormatterOptions: v1alpha1.FormatterOptions{ + IniConfig: &v1alpha1.IniConfig{ + SectionName: "mysqld", + }, + }, + }, + configData: []byte(mysqlConfig), + expected: map[string]string{ + "key_buffer_size": "16777216", + "log_error": "/data/mysql/logs/mysql.log", + }, + }, { + name: "mongodb-test", + fileName: "mongodb.conf", + formatConfig: &v1alpha1.FormatterConfig{ + Format: v1alpha1.YAML, + FormatterOptions: v1alpha1.FormatterOptions{ + IniConfig: &v1alpha1.IniConfig{ + SectionName: "default", + }, + }, + }, + configData: []byte(mongodbConfig), + expected: map[string]string{ + "systemLog.logRotate": "reopen", + "systemLog.path": "/data/mongodb/logs/mongodb.log", + "systemLog.verbosity": "0", + }, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + res, _ := TransformConfigFileToKeyValueMap(tt.fileName, tt.formatConfig, tt.configData) + if !reflect.DeepEqual(res, tt.expected) { + t.Errorf("TransformConfigFileToKeyValueMap() res = %v, res %v", res, tt.expected) + return + } + }) + } +} diff --git a/internal/configuration/config_query.go b/internal/configuration/config_query.go index f88b15c8b..104e5f01c 100644 --- a/internal/configuration/config_query.go +++ b/internal/configuration/config_query.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -25,14 +28,14 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) -// GetParameterFromConfiguration get configure parameter +// GetParameterFromConfiguration gets configure parameter // ctx: apiserver context // cli: apiserver client // cluster: appsv1alpha1.Cluster // component: component name func GetParameterFromConfiguration(configMap *corev1.ConfigMap, allFiles bool, fieldPath ...string) ([]string, error) { if configMap == nil || len(configMap.Data) == 0 { - return nil, MakeError("configmap not any configuration files. [%v]", configMap) + return nil, MakeError("required configmap [%v]", configMap) } // Load configmap diff --git a/internal/configuration/config_query_test.go b/internal/configuration/config_query_test.go index cea2fdc8b..dc037cfb3 100644 --- a/internal/configuration/config_query_test.go +++ b/internal/configuration/config_query_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/config_test.go b/internal/configuration/config_test.go index b9466c759..bbc45d1fc 100644 --- a/internal/configuration/config_test.go +++ b/internal/configuration/config_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -64,70 +67,6 @@ socket=/data/mysql/tmp/mysqld.sock host=localhost ` -func TestRawConfig(t *testing.T) { - - cfg, err := NewConfigLoader(CfgOption{ - Type: CfgRawType, - Log: log.FromContext(context.Background()), - CfgType: appsv1alpha1.Ini, - RawData: []byte(iniConfig), - }) - - if err != nil { - t.Fatalf("new config loader failed [%v]", err) - } - - ctx := NewCfgOptions("", - func(ctx *CfgOpOption) { - // filter mysqld - ctx.IniContext = &IniContext{ - SectionName: "mysqld", - } - }) - - // ctx := NewCfgOptions("$..slow_query_log_file", "") - - result, err := cfg.Query("$..slow_query_log_file", NewCfgOptions("")) - require.Nil(t, err) - require.NotNil(t, result) - require.Equal(t, "[\"/data/mysql/mysqld-slow.log\"]", string(result)) - - require.Nil(t, - cfg.MergeFrom(map[string]interface{}{ - "slow_query_log": 1, - "server-id": 2, - "socket": "xxxxxxxxxxxxxxx", - }, ctx)) - - content, err := cfg.ToCfgContent() - require.NotNil(t, content) - require.Nil(t, err) - - newContent, exist := content[cfg.name] - require.True(t, exist) - patch, err := CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) - require.Nil(t, err) - log.Log.Info("patch : %v", patch) - require.True(t, patch.IsModify) - require.Equal(t, string(patch.UpdateConfig["raw"]), `{"mysqld":{"server-id":"2","socket":"xxxxxxxxxxxxxxx"}}`) - - { - require.Nil(t, - cfg.MergeFrom(map[string]interface{}{ - "server-id": 1, - "socket": "/data/mysql/tmp/mysqld.sock", - }, ctx)) - content, err := cfg.ToCfgContent() - require.Nil(t, err) - newContent := content[cfg.name] - // CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) - patch, err := CreateMergePatch([]byte(iniConfig), []byte(newContent), cfg.Option) - require.Nil(t, err) - log.Log.Info("patch : %v", patch) - require.False(t, patch.IsModify) - } -} - func TestConfigMapConfig(t *testing.T) { cfg, err := NewConfigLoader(CfgOption{ Type: CfgCmType, diff --git a/internal/configuration/config_util.go b/internal/configuration/config_util.go index a46e17d36..e63e72c25 100644 --- a/internal/configuration/config_util.go +++ b/internal/configuration/config_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -20,6 +23,7 @@ import ( "context" "github.com/StudioSol/set" + "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -30,7 +34,7 @@ type ParamPairs struct { UpdatedParams map[string]interface{} } -// MergeAndValidateConfigs does merge configuration files and validate +// MergeAndValidateConfigs merges and validates configuration files func MergeAndValidateConfigs(configConstraint appsv1alpha1.ConfigConstraintSpec, baseConfigs map[string]string, cmKey []string, updatedParams []ParamPairs) (map[string]string, error) { var ( err error @@ -64,9 +68,9 @@ func MergeAndValidateConfigs(configConstraint appsv1alpha1.ConfigConstraintSpec, return nil, WrapError(err, "failed to generate config file") } - // The ToCfgContent interface returns the file contents of all keys, and after the configuration file is encoded and decoded, - // the content may be inconsistent, such as comments, blank lines, etc, - // in order to minimize the impact on the original configuration file, only update the changed file content. + // The ToCfgContent interface returns the file contents of all keys, the configuration file is encoded and decoded into keys, + // the content may be different with the original file, such as comments, blank lines, etc, + // in order to minimize the impact on the original file, only update the changed part. updatedCfg := fromUpdatedConfig(newCfg, updatedKeys) if err = NewConfigValidator(&configConstraint, WithKeySelector(cmKey)).Validate(updatedCfg); err != nil { return nil, WrapError(err, "failed to validate updated config") @@ -88,7 +92,7 @@ func mergeUpdatedConfig(baseMap, updatedMap map[string]string) map[string]string return r } -// fromUpdatedConfig function is to filter out changed file contents. +// fromUpdatedConfig filters out changed file contents. func fromUpdatedConfig(m map[string]string, sets *set.LinkedHashSetString) map[string]string { if sets.Length() == 0 { return map[string]string{} @@ -132,3 +136,28 @@ func ApplyConfigPatch(baseCfg []byte, updatedParameters map[string]string, forma mergedConfig := configWrapper.getConfigObject(mergedOptions) return mergedConfig.Marshal() } + +func NeedReloadVolume(config appsv1alpha1.ComponentConfigSpec) bool { + // TODO distinguish between scripts and configuration + return config.ConfigConstraintRef != "" +} + +func GetReloadOptions(cli client.Client, ctx context.Context, configSpecs []appsv1alpha1.ComponentConfigSpec) (*appsv1alpha1.ReloadOptions, *appsv1alpha1.FormatterConfig, error) { + for _, configSpec := range configSpecs { + if !NeedReloadVolume(configSpec) { + continue + } + ccKey := client.ObjectKey{ + Namespace: "", + Name: configSpec.ConfigConstraintRef, + } + cfgConst := &appsv1alpha1.ConfigConstraint{} + if err := cli.Get(ctx, ccKey, cfgConst); err != nil { + return nil, nil, WrapError(err, "failed to get ConfigConstraint, key[%v]", ccKey) + } + if cfgConst.Spec.ReloadOptions != nil { + return cfgConst.Spec.ReloadOptions, cfgConst.Spec.FormatterConfig, nil + } + } + return nil, nil, nil +} diff --git a/internal/configuration/config_util_test.go b/internal/configuration/config_util_test.go index 723ca45ba..fe40e91f5 100644 --- a/internal/configuration/config_util_test.go +++ b/internal/configuration/config_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -41,7 +44,7 @@ var _ = Describe("config_util", func() { }) Context("MergeAndValidateConfigs", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { type args struct { configConstraint v1alpha1.ConfigConstraintSpec baseCfg map[string]string diff --git a/internal/configuration/config_validate.go b/internal/configuration/config_validate.go index d63be5222..ac85ac418 100644 --- a/internal/configuration/config_validate.go +++ b/internal/configuration/config_validate.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -34,17 +37,17 @@ type ConfigValidator interface { } type cmKeySelector struct { - // A ConfigMap object may contain multiple configuration files and only some configuration files can recognize their format and verify their by kubeblocks, - // such as pg, there are two files, pg_hba.conf and postgresql.conf in the ConfigMap, we can only validate postgresql.conf, - // thus pg_hba.conf file needs to be ignored during the verification. - // keySelector is used to filter the keys in the configmap. + // A ConfigMap object may contain multiple configuration files and only some of them can be parsed and verified by kubeblocks, + // such as postgresql, there are two files pg_hba.conf & postgresql.conf in the ConfigMap, and we can only validate postgresql.conf, + // so pg_hba.conf file needs to be ignored during when doing verification. + // keySelector filters the keys in the configmap. keySelector []ValidatorOptions } type configCueValidator struct { cmKeySelector - // cue describe configuration template + // cue describes configuration template cueScript string cfgType appsv1alpha1.CfgFileFormat } diff --git a/internal/configuration/config_validate_test.go b/internal/configuration/config_validate_test.go index eb0960799..5a6128c27 100644 --- a/internal/configuration/config_validate_test.go +++ b/internal/configuration/config_validate_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -57,6 +60,14 @@ func TestSchemaValidatorWithCue(t *testing.T) { args args err error }{{ + name: "mongod_test", + args: args{ + cueFile: "cue_testdata/mongod.cue", + configFile: "cue_testdata/mongod.conf", + format: appsv1alpha1.YAML, + }, + err: nil, + }, { name: "test_wesql", args: args{ cueFile: "cue_testdata/wesql.cue", @@ -95,13 +106,13 @@ func TestSchemaValidatorWithCue(t *testing.T) { configFile: "cue_testdata/mysql_err.cnf", format: appsv1alpha1.Ini, }, - err: errors.New(`failed to cue template render configure: [mysqld.innodb_autoinc_lock_mode: 3 errors in empty disjunction: + err: errors.New(`failed to render cue template configure: [mysqld.innodb_autoinc_lock_mode: 3 errors in empty disjunction: mysqld.innodb_autoinc_lock_mode: conflicting values 0 and 100: - 28:35 + 31:35 mysqld.innodb_autoinc_lock_mode: conflicting values 1 and 100: - 28:39 + 31:39 mysqld.innodb_autoinc_lock_mode: conflicting values 2 and 100: - 28:43 + 31:43 ]`), }, { name: "configmap_key_filter", diff --git a/internal/configuration/configtemplate_util.go b/internal/configuration/configtemplate_util.go index 1bcc3b994..008a9fae2 100644 --- a/internal/configuration/configtemplate_util.go +++ b/internal/configuration/configtemplate_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -39,7 +42,7 @@ func filter[T ComponentsType](components []T, f filterFn[T]) *T { return nil } -// GetConfigTemplatesFromComponent returns ConfigTemplate list used by the component. +// GetConfigTemplatesFromComponent returns ConfigTemplate list used by the component func GetConfigTemplatesFromComponent( cComponents []appsv1alpha1.ClusterComponentSpec, dComponents []appsv1alpha1.ClusterComponentDefinition, @@ -77,7 +80,7 @@ func GetConfigTemplatesFromComponent( return MergeConfigTemplates(cvConfigSpecs, cdConfigSpecs), nil } -// MergeConfigTemplates merge ClusterVersion.ComponentDefs[*].ConfigTemplateRefs and ClusterDefinition.ComponentDefs[*].ConfigTemplateRefs +// MergeConfigTemplates merges ClusterVersion.ComponentDefs[*].ConfigTemplateRefs and ClusterDefinition.ComponentDefs[*].ConfigTemplateRefs func MergeConfigTemplates(cvConfigSpecs []appsv1alpha1.ComponentConfigSpec, cdConfigSpecs []appsv1alpha1.ComponentConfigSpec) []appsv1alpha1.ComponentConfigSpec { if len(cvConfigSpecs) == 0 { @@ -102,7 +105,7 @@ func MergeConfigTemplates(cvConfigSpecs []appsv1alpha1.ComponentConfigSpec, } for _, configSpec := range cdConfigSpecs { - // ClusterVersion replace clusterDefinition + // ClusterVersion replaces clusterDefinition tplName := configSpec.Name if _, ok := (mergedTplMap)[tplName]; ok { continue @@ -128,12 +131,12 @@ func GetClusterVersionResource(cvName string, cv *appsv1alpha1.ClusterVersion, c return nil } -func CheckConfigTemplateReconfigureKey(configSpec appsv1alpha1.ComponentConfigSpec, key string) bool { - if len(configSpec.Keys) == 0 { +func IsSupportConfigFileReconfigure(configTemplateSpec appsv1alpha1.ComponentConfigSpec, configFileKey string) bool { + if len(configTemplateSpec.Keys) == 0 { return true } - for _, keySelector := range configSpec.Keys { - if keySelector == key { + for _, keySelector := range configTemplateSpec.Keys { + if keySelector == configFileKey { return true } } diff --git a/internal/configuration/configtemplate_util_test.go b/internal/configuration/configtemplate_util_test.go index 2cd817d2f..7ec9423ff 100644 --- a/internal/configuration/configtemplate_util_test.go +++ b/internal/configuration/configtemplate_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/constraint.go b/internal/configuration/constraint.go new file mode 100644 index 000000000..81655fdff --- /dev/null +++ b/internal/configuration/constraint.go @@ -0,0 +1,31 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package configuration + +import appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + +const ( + ReconfigureCreatedPhase = "created" + ReconfigureNoChangeType = "noChange" + ReconfigureAutoReloadPhase = string(appsv1alpha1.AutoReload) + ReconfigureSimplePhase = string(appsv1alpha1.NormalPolicy) + ReconfigureParallelPhase = string(appsv1alpha1.RestartPolicy) + ReconfigureRollingPhase = string(appsv1alpha1.RollingPolicy) +) diff --git a/internal/configuration/constrant.go b/internal/configuration/constrant.go deleted file mode 100644 index fa2dea63b..000000000 --- a/internal/configuration/constrant.go +++ /dev/null @@ -1,28 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 configuration - -import appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - -const ( - ReconfigureCreatedPhase = "created" - ReconfigureNoChangeType = "noChange" - ReconfigureAutoReloadPhase = string(appsv1alpha1.AutoReload) - ReconfigureSimplePhase = string(appsv1alpha1.NormalPolicy) - ReconfigureParallelPhase = string(appsv1alpha1.RestartPolicy) - ReconfigureRollingPhase = string(appsv1alpha1.RollingPolicy) -) diff --git a/internal/configuration/container/container_kill.go b/internal/configuration/container/container_kill.go index 65564ca3f..1b7289909 100644 --- a/internal/configuration/container/container_kill.go +++ b/internal/configuration/container/container_kill.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package container @@ -34,6 +37,7 @@ import ( runtimeapi "k8s.io/cri-api/pkg/apis/runtime/v1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" + "github.com/apecloud/kubeblocks/internal/configuration/util" ) const ( @@ -44,7 +48,7 @@ const ( KillContainerSignalEnvName = "KILL_CONTAINER_SIGNAL" ) -// dockerContainer support docker cri +// dockerContainer supports docker cri type dockerContainer struct { dockerEndpoint string logger *zap.SugaredLogger @@ -62,27 +66,27 @@ func init() { } func (d *dockerContainer) Kill(ctx context.Context, containerIDs []string, signal string, _ *time.Duration) error { - d.logger.Debugf("following docker containers are going to be stopped: %v", containerIDs) + d.logger.Debugf("docker containers going to be stopped: %v", containerIDs) if signal == "" { signal = defaultSignal } allContainer, err := getExistsContainers(ctx, containerIDs, d.dc) if err != nil { - return cfgcore.WrapError(err, "failed to search container") + return cfgcore.WrapError(err, "failed to search docker container") } errs := make([]error, 0, len(containerIDs)) - d.logger.Debugf("all docker container: %v", cfgcore.ToSet(allContainer).AsSlice()) + d.logger.Debugf("all containers: %v", util.ToSet(allContainer).AsSlice()) for _, containerID := range containerIDs { d.logger.Infof("stopping docker container: %s", containerID) container, ok := allContainer[containerID] if !ok { - d.logger.Infof("container[%s] not exist and pass.", containerID) + d.logger.Infof("docker container[%s] not existed and continue.", containerID) continue } if container.State == "exited" { - d.logger.Infof("container[%s] is exited, status: %s", containerID, container.Status) + d.logger.Infof("docker container[%s] exited, status: %s", containerID, container.Status) continue } if err := d.dc.ContainerKill(ctx, containerID, signal); err != nil { @@ -129,7 +133,7 @@ func (d *dockerContainer) Init(ctx context.Context) error { if err != nil { return err } - d.logger.Infof("create docker client success! docker info: %v", ping) + d.logger.Infof("create docker client succeed, docker info: %v", ping) } return err } @@ -139,14 +143,14 @@ func createDockerClient(dockerEndpoint string, logger *zap.SugaredLogger) (*dock dockerEndpoint = dockerapi.DefaultDockerHost } - logger.Infof("connecting to docker on the endpoint: %s", dockerEndpoint) + logger.Infof("connecting to docker container endpoint: %s", dockerEndpoint) return dockerapi.NewClientWithOpts( dockerapi.WithHost(formatSocketPath(dockerEndpoint)), dockerapi.WithVersion(""), ) } -// dockerContainer support docker cri +// dockerContainer supports docker cri type containerdContainer struct { runtimeEndpoint string logger *zap.SugaredLogger @@ -179,9 +183,9 @@ func (c *containerdContainer) Kill(ctx context.Context, containerIDs []string, s case err != nil: errs = append(errs, err) case containers == nil || len(containers.Containers) == 0: - c.logger.Infof("container[%s] not exist and pass.", containerID) + c.logger.Infof("containerd container[%s] not existed and continue.", containerID) case containers.Containers[0].State == runtimeapi.ContainerState_CONTAINER_EXITED: - c.logger.Infof("container[%s] not exited and pass.", containerID) + c.logger.Infof("containerd container[%s] not exited and continue.", containerID) default: request.ContainerId = containerID _, err = c.backendRuntime.StopContainer(ctx, request) @@ -258,7 +262,7 @@ func NewContainerKiller(containerRuntime CRIType, runtimeEndpoint string, logger logger: logger, } default: - return nil, cfgcore.MakeError("not support cri type: %s", containerRuntime) + return nil, cfgcore.MakeError("not supported cri type: %s", containerRuntime) } return killer, nil } diff --git a/internal/configuration/container/container_kill_test.go b/internal/configuration/container/container_kill_test.go index cea6e0809..47eb80ff5 100644 --- a/internal/configuration/container/container_kill_test.go +++ b/internal/configuration/container/container_kill_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package container @@ -128,7 +131,7 @@ func TestDockerContainerKill(t *testing.T) { // mock ContainerKill failed cli.EXPECT().ContainerKill(gomock.Any(), gomock.Any(), gomock.Any()). - Return(cfgcore.MakeError("faield to kill docker container!")) + Return(cfgcore.MakeError("failed to kill docker container!")) // mock ContainerKill success cli.EXPECT().ContainerKill(gomock.Any(), gomock.Any(), gomock.Any()). Return(nil).AnyTimes() @@ -140,7 +143,7 @@ func TestDockerContainerKill(t *testing.T) { require.Nil(t, docker.Kill(context.Background(), []string{"76f9c2ae8cf47bfa43b97626e3c95045cb3b82c50019ab759801ab52e3acff55"}, "", nil)) require.ErrorContains(t, docker.Kill(context.Background(), []string{"76f9c2ae8cf47bfa43b97626e3c95045cb3b82c50019ab759801ab52e3acff55"}, "", nil), - "faield to kill docker container") + "failed to kill docker container") require.Nil(t, docker.Kill(context.Background(), []string{"76f9c2ae8cf47bfa43b97626e3c95045cb3b82c50019ab759801ab52e3acff55"}, "", nil)) } @@ -240,7 +243,7 @@ func TestAutoCheckCRIType(t *testing.T) { t.Errorf("failed to writing settings. Err: %v", err) return } - <-dialDone // wait close conn only after dial returns. + <-dialDone // wait for dialDone before closing connection conn.Close() }() diff --git a/internal/configuration/container/container_util.go b/internal/configuration/container/container_util.go index fd372a14b..445256471 100644 --- a/internal/configuration/container/container_util.go +++ b/internal/configuration/container/container_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package container diff --git a/internal/configuration/container/container_util_test.go b/internal/configuration/container/container_util_test.go index bef540157..ed433a080 100644 --- a/internal/configuration/container/container_util_test.go +++ b/internal/configuration/container/container_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package container diff --git a/internal/configuration/container/mocks/generate.go b/internal/configuration/container/mocks/generate.go index c3cfbcf1b..eebe99bcf 100644 --- a/internal/configuration/container/mocks/generate.go +++ b/internal/configuration/container/mocks/generate.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mocks diff --git a/internal/configuration/container/type.go b/internal/configuration/container/type.go index 5843d2c1f..08d107404 100644 --- a/internal/configuration/container/type.go +++ b/internal/configuration/container/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package container @@ -42,7 +45,7 @@ var defaultContainerdEndpoints = []string{ "unix:///var/run/cri-dockerd.sock", } -// ContainerKiller kill container interface +// ContainerKiller kills container interface type ContainerKiller interface { // Kill containers in the pod by cri diff --git a/internal/configuration/cue_gen_openapi.go b/internal/configuration/cue_gen_openapi.go index 9753b7ee5..31526b02f 100644 --- a/internal/configuration/cue_gen_openapi.go +++ b/internal/configuration/cue_gen_openapi.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -29,7 +32,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log" ) -// GenerateOpenAPISchema generate openapi schema from cue type Definitions. +// GenerateOpenAPISchema generates openapi schema from cue type Definitions. func GenerateOpenAPISchema(cueTpl string, schemaType string) (*apiextv1.JSONSchemaProps, error) { const ( openAPIVersion = "3.1.0" @@ -93,7 +96,7 @@ func transformOpenAPISchema(cueSchema *openapi.OrderedMap, schemaType string) (* typeSchema := foundSchemaFromCueDefines(cueSchema, schemaType) if typeSchema == nil { - log.Log.Info(fmt.Sprintf("Cannot found schema. type:[%s], all: %v", schemaType, allSchemaType(cueSchema))) + log.Log.Info(fmt.Sprintf("not found schema type:[%s], all: %v", schemaType, allSchemaType(cueSchema))) return nil, nil } diff --git a/internal/configuration/cue_gen_openapi_test.go b/internal/configuration/cue_gen_openapi_test.go index dc3d75689..ca16d8c39 100644 --- a/internal/configuration/cue_gen_openapi_test.go +++ b/internal/configuration/cue_gen_openapi_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -105,7 +108,7 @@ func getContentFromFile(file string) []byte { func runOpenAPITest(cueFile string, typeName string) ([]byte, error) { cueTpl := getContentFromFile(cueFile) if cueTpl == nil { - return nil, MakeError("not open file[%s]", cueTpl) + return nil, MakeError("cannot open file[%s]", cueTpl) } schema, err := GenerateOpenAPISchema(string(cueTpl), typeName) @@ -114,7 +117,7 @@ func runOpenAPITest(cueFile string, typeName string) ([]byte, error) { } if schema == nil { - return nil, MakeError("Cannot found schema.") + return nil, MakeError("cannot find schema.") } b, _ := json.Marshal(schema) diff --git a/internal/configuration/cue_util.go b/internal/configuration/cue_util.go index 05e2a5b08..ff5e3a095 100644 --- a/internal/configuration/cue_util.go +++ b/internal/configuration/cue_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -25,7 +28,7 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) -// CueType define cue type +// CueType defines cue type // +enum type CueType string @@ -42,7 +45,7 @@ const ( ClassicTimeDurationType CueType = "timeDuration" ) -// CueValidate cue validate +// CueValidate validates cue file func CueValidate(cueTpl string) error { if len(cueTpl) == 0 { return nil @@ -56,7 +59,7 @@ func CueValidate(cueTpl string) error { func ValidateConfigurationWithCue(cueTpl string, cfgType appsv1alpha1.CfgFileFormat, rawData string) error { cfg, err := loadConfigObjectFromContent(cfgType, rawData) if err != nil { - return WrapError(err, "failed to load configuration. [%s]", rawData) + return WrapError(err, "failed to load configuration [%s]", rawData) } return unstructuredDataValidateByCue(cueTpl, cfg, cfgType == appsv1alpha1.Properties) @@ -91,7 +94,7 @@ func unstructuredDataValidateByCue(cueTpl string, data interface{}, trimString b tpl = tpl.Fill(data, paths...) if err := tpl.Err(); err != nil { - return WrapError(err, "failed to cue template render configure") + return WrapError(err, "failed to render cue template configure") } return tpl.Validate() diff --git a/internal/configuration/cue_util_test.go b/internal/configuration/cue_util_test.go index 0c6780eb4..2d3e9b1f5 100644 --- a/internal/configuration/cue_util_test.go +++ b/internal/configuration/cue_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/cue_visitor.go b/internal/configuration/cue_visitor.go index e7b973495..aec152930 100644 --- a/internal/configuration/cue_visitor.go +++ b/internal/configuration/cue_visitor.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -25,6 +28,8 @@ import ( "cuelang.org/go/cue" "github.com/spf13/viper" "sigs.k8s.io/controller-runtime/pkg/log" + + "github.com/apecloud/kubeblocks/internal/configuration/util" ) var disableAutoTransfer = viper.GetBool("DISABLE_AUTO_TRANSFER") @@ -45,7 +50,7 @@ func (c *cueTypeExtractor) Visit(val cue.Value) { c.fieldTypes = make(map[string]CueType) c.fieldUnits = make(map[string]string) } - c.visitStruct(val) + c.visitStruct(val, "") } func (c *cueTypeExtractor) visitValue(x cue.Value, path string) { @@ -69,28 +74,35 @@ func (c *cueTypeExtractor) visitValue(x cue.Value, path string) { c.visitList(x, path) case k&cue.StructKind == cue.StructKind: c.addFieldType(path, StructType) - c.visitStruct(x) + c.visitStruct(x, path) default: log.Log.Info(fmt.Sprintf("cannot convert value of type %s", k.String())) } } -func (c *cueTypeExtractor) visitStruct(v cue.Value) { +func (c *cueTypeExtractor) visitStruct(v cue.Value, parentPath string) { + joinFieldPath := func(path string, name string) string { + if path == "" || strings.HasPrefix(path, "#") { + return name + } + return path + "." + name + } + switch op, v := v.Expr(); op { - // SelectorOp refer of other struct type + // SelectorOp refers to other struct type case cue.NoOp, cue.SelectorOp: // pass - // cue.NoOp describes the value is an underlying field. - // cue.SelectorOp describes the value is a type reference field. + // cue.NoOp: the value is an underlying field. + // cue.SelectorOp: the value is a type reference field. default: // not support op, e.g. cue.Or, cue.And. - log.Log.V(1).Info(fmt.Sprintf("cue type extractor unsupported op %v for object type (%v)", op, v)) + log.Log.V(1).Info(fmt.Sprintf("cue type extractor does not support op %v for object type (%v)", op, v)) return } for itr, _ := v.Fields(cue.Optional(true), cue.Definitions(true)); itr.Next(); { name := itr.Label() - c.visitValue(itr.Value(), name) + c.visitValue(itr.Value(), joinFieldPath(parentPath, name)) } } @@ -99,12 +111,12 @@ func (c *cueTypeExtractor) visitList(v cue.Value, path string) { case cue.NoOp, cue.SelectorOp: // pass default: - log.Log.Info(fmt.Sprintf("unsupported op %v for object type (%v)", op, v)) + log.Log.Info(fmt.Sprintf("not supported op %v for object type (%v)", op, v)) } count := 0 for i, _ := v.List(); i.Next(); count++ { - c.visitValue(i.Value(), fmt.Sprintf("%s_%d", path, count)) + c.visitValue(i.Value(), path) } } @@ -119,7 +131,21 @@ func (c *cueTypeExtractor) addFieldUnits(path string, t CueType, base string) { } } -func transNumberOrBoolType(t CueType, obj reflect.Value, fn UpdateFn, expand string, trimString bool) error { +func (c *cueTypeExtractor) hasFieldType(parent string, cur string) (string, bool) { + fieldRef := cur + if parent != "" { + fieldRef = parent + "." + cur + } + if _, exist := c.fieldTypes[fieldRef]; exist { + return fieldRef, true + } + if _, exist := c.fieldTypes[cur]; exist { + return cur, true + } + return "", false +} + +func transNumberOrBoolType(t CueType, obj reflect.Value, fn util.UpdateFn, expand string, trimString bool) error { switch t { case IntType: return processTypeTrans[int](obj, strconv.Atoi, fn, trimString) @@ -145,7 +171,7 @@ func transNumberOrBoolType(t CueType, obj reflect.Value, fn UpdateFn, expand str return nil } -func trimStringQuotes(obj reflect.Value, fn UpdateFn) { +func trimStringQuotes(obj reflect.Value, fn util.UpdateFn) { if obj.Type().Kind() != reflect.String { return } @@ -160,7 +186,7 @@ func trimStringQuotes(obj reflect.Value, fn UpdateFn) { } } -func processTypeTrans[T int | int64 | float64 | float32 | bool](obj reflect.Value, transFn func(s string) (T, error), updateFn UpdateFn, trimString bool) error { +func processTypeTrans[T int | int64 | float64 | float32 | bool](obj reflect.Value, transFn func(s string) (T, error), updateFn util.UpdateFn, trimString bool) error { switch obj.Type().Kind() { case reflect.String: str := obj.String() @@ -173,7 +199,7 @@ func processTypeTrans[T int | int64 | float64 | float32 | bool](obj reflect.Valu } updateFn(v) case reflect.Array, reflect.Slice, reflect.Struct: - return MakeError("not support type[%s] trans.", obj.Type().Kind()) + return MakeError("not supported type[%s] trans.", obj.Type().Kind()) } return nil @@ -188,16 +214,18 @@ func processCfgNotStringParam(data interface{}, context *cue.Context, tpl cue.Va context: context, } typeTransformer.Visit(tpl) - return UnstructuredObjectWalk(typeTransformer.data, - func(parent, cur string, obj reflect.Value, fn UpdateFn) error { + return util.UnstructuredObjectWalk(typeTransformer.data, + func(parent, cur string, obj reflect.Value, fn util.UpdateFn) error { if fn == nil || cur == "" || !obj.IsValid() { return nil } - if t, exist := typeTransformer.fieldTypes[cur]; exist { - err := transNumberOrBoolType(t, obj, fn, typeTransformer.fieldUnits[cur], trimString) - if err != nil { - return WrapError(err, "failed to type convertor, field[%s]", cur) - } + fieldPath, exist := typeTransformer.hasFieldType(parent, cur) + if !exist { + return nil + } + err := transNumberOrBoolType(typeTransformer.fieldTypes[fieldPath], obj, fn, typeTransformer.fieldUnits[fieldPath], trimString) + if err != nil { + return WrapError(err, "failed to parse field %s", fieldPath) } return nil }, false) diff --git a/internal/configuration/cue_visitor_test.go b/internal/configuration/cue_visitor_test.go index 901f5a900..64efcd012 100644 --- a/internal/configuration/cue_visitor_test.go +++ b/internal/configuration/cue_visitor_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -71,19 +74,28 @@ func TestCueTypeExtractorVisit(t *testing.T) { } `, fieldTypes: map[string]CueType{ - "#a": StructType, - "b": StructType, - "g": StructType, - "#c": StructType, - "e": IntType, - "f": StringType, - "#j": StructType, - "x": StringType, - "y": IntType, - "#n": StructType, - "m": StructType, - "d": StructType, - "j": NullableType, + "#a": StructType, + "b": StructType, + "g": StructType, + "#c": StructType, + "e": IntType, + "f": StringType, + "#j": StructType, + "x": StringType, + "y": IntType, + "#n": StructType, + "m": StructType, + "d": StructType, + "j": NullableType, + "b.e": IntType, + "b.f": StringType, + "g.x": StringType, + "g.y": IntType, + "g.m": StructType, + "g.m.d": StructType, + "g.m.j": NullableType, + "m.d": StructType, + "m.j": NullableType, }, }, }, { @@ -102,14 +114,13 @@ func TestCueTypeExtractorVisit(t *testing.T) { i:[int] }`, fieldTypes: map[string]CueType{ - "#a": StructType, - "b": IntType, - "c": StringType, - "d": StringType, - "e": StringType, - "g": StructType, - "i": ListType, - "i_0": IntType, + "#a": StructType, + "b": IntType, + "c": StringType, + "d": StringType, + "e": StringType, + "g": StructType, + "i": IntType, }, }, }, { diff --git a/internal/configuration/cuelang_expansion.go b/internal/configuration/cuelang_expansion.go index ec62b6b6e..6705bee83 100644 --- a/internal/configuration/cuelang_expansion.go +++ b/internal/configuration/cuelang_expansion.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/error.go b/internal/configuration/error.go index fc29e5495..abc6deb82 100644 --- a/internal/configuration/error.go +++ b/internal/configuration/error.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/hash.go b/internal/configuration/hash.go deleted file mode 100644 index 0e1135aad..000000000 --- a/internal/configuration/hash.go +++ /dev/null @@ -1,43 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 configuration - -import ( - "bytes" - "encoding/json" - "fmt" - "hash/fnv" - "io" - - "k8s.io/apimachinery/pkg/util/rand" -) - -func ComputeHash(object interface{}) (string, error) { - objString, err := json.Marshal(object) - if err != nil { - return "", WrapError(err, "failed to compute hash.") - } - - // hasher := sha1.New() - hasher := fnv.New32() - if _, err := io.Copy(hasher, bytes.NewReader(objString)); err != nil { - return "", WrapError(err, "failed to compute hash for sha256. [%s]", objString) - } - - sha := hasher.Sum32() - return rand.SafeEncodeString(fmt.Sprint(sha)), nil -} diff --git a/internal/configuration/jsonpath.go b/internal/configuration/jsonpath.go deleted file mode 100644 index 3a74df4f0..000000000 --- a/internal/configuration/jsonpath.go +++ /dev/null @@ -1,59 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 configuration - -import ( - "encoding/json" - - "github.com/bhmj/jsonslice" - jsonpatch "github.com/evanphx/json-patch" -) - -func retrievalWithJSONPath(jsonObj interface{}, jsonpath string) ([]byte, error) { - - jsonbytes, err := json.Marshal(&jsonObj) - if err != nil { - return nil, err - } - - res, err := jsonslice.Get(jsonbytes, jsonpath) - if err != nil { - return res, err - } - - resLen := len(res) - if resLen > 2 && res[0] == '"' && res[resLen-1] == '"' { - res = res[1 : resLen-1] - } - - return res, err -} - -func jsonPatch(originalJSON, modifiedJSON interface{}) ([]byte, error) { - originalBytes, err := json.Marshal(originalJSON) - if err != nil { - return nil, err - } - - modifiedBytes, err := json.Marshal(modifiedJSON) - if err != nil { - return nil, err - } - - // TODO(zt) It's a hack to do the logic, json object --> bytes, bytes --> json object - return jsonpatch.CreateMergePatch(originalBytes, modifiedBytes) -} diff --git a/internal/configuration/math.go b/internal/configuration/math.go deleted file mode 100644 index c3f1d9ece..000000000 --- a/internal/configuration/math.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 configuration - -import "golang.org/x/exp/constraints" - -func Min[T constraints.Ordered](l, r T) T { - if l < r { - return l - } - return r -} - -func Max[T constraints.Ordered](l, r T) T { - if l < r { - return r - } - return l -} diff --git a/internal/configuration/proto/generate.go b/internal/configuration/proto/generate.go index 0440a1278..7b5436164 100644 --- a/internal/configuration/proto/generate.go +++ b/internal/configuration/proto/generate.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package proto diff --git a/internal/configuration/proto/mocks/generate.go b/internal/configuration/proto/mocks/generate.go index 293e42c8d..7d4b7442f 100644 --- a/internal/configuration/proto/mocks/generate.go +++ b/internal/configuration/proto/mocks/generate.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mocks diff --git a/internal/configuration/reconfigure_util.go b/internal/configuration/reconfigure_util.go index 9884ea695..c3ff43692 100644 --- a/internal/configuration/reconfigure_util.go +++ b/internal/configuration/reconfigure_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -22,40 +25,65 @@ import ( "github.com/StudioSol/set" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/constant" ) -func getUpdateParameterList(cfg *ConfigPatchInfo) ([]string, error) { +func getUpdateParameterList(cfg *ConfigPatchInfo, trimField string) ([]string, error) { params := make([]string, 0) - walkFn := func(parent, cur string, v reflect.Value, fn UpdateFn) error { + walkFn := func(parent, cur string, v reflect.Value, fn util.UpdateFn) error { if cur != "" { + if parent != "" { + cur = parent + "." + cur + } params = append(params, cur) } return nil } for _, diff := range cfg.UpdateConfig { + var err error var updatedParams any - if err := json.Unmarshal(diff, &updatedParams); err != nil { + if err = json.Unmarshal(diff, &updatedParams); err != nil { + return nil, err + } + if updatedParams, err = trimNestedField(updatedParams, trimField); err != nil { return nil, err } - if err := UnstructuredObjectWalk(updatedParams, walkFn, true); err != nil { + if err := util.UnstructuredObjectWalk(updatedParams, walkFn, true); err != nil { return nil, WrapError(err, "failed to walk params: [%s]", diff) } } return params, nil } -// IsUpdateDynamicParameters is used to check whether the changed parameters require a restart +func trimNestedField(updatedParams any, trimField string) (any, error) { + if trimField == "" { + return updatedParams, nil + } + if m, ok := updatedParams.(map[string]interface{}); ok { + trimParams, found, err := unstructured.NestedFieldNoCopy(m, trimField) + if err != nil { + return nil, err + } + if found { + return trimParams, nil + } + } + return updatedParams, nil +} + +// IsUpdateDynamicParameters checks if the changed parameters require a restart func IsUpdateDynamicParameters(cc *appsv1alpha1.ConfigConstraintSpec, cfg *ConfigPatchInfo) (bool, error) { - // TODO(zt) how to process new or delete file if len(cfg.DeleteConfig) > 0 || len(cfg.AddConfig) > 0 { return false, nil } - params, err := getUpdateParameterList(cfg) + params, err := getUpdateParameterList(cfg, NestedPrefixField(cc.FormatterConfig)) if err != nil { return false, err } @@ -64,7 +92,7 @@ func IsUpdateDynamicParameters(cc *appsv1alpha1.ConfigConstraintSpec, cfg *Confi // if ConfigConstraint has StaticParameters, check updated parameter if len(cc.StaticParameters) > 0 { staticParams := set.NewLinkedHashSetString(cc.StaticParameters...) - union := Union(staticParams, updateParams) + union := util.Union(staticParams, updateParams) if union.Length() > 0 { return false, nil } @@ -74,19 +102,19 @@ func IsUpdateDynamicParameters(cc *appsv1alpha1.ConfigConstraintSpec, cfg *Confi } } - // if ConfigConstraint has DynamicParameter, all updated param in dynamic params + // if ConfigConstraint has DynamicParameter, and all updated params are dynamic if len(cc.DynamicParameters) > 0 { dynamicParams := set.NewLinkedHashSetString(cc.DynamicParameters...) - union := Difference(updateParams, dynamicParams) - return union.Length() == 0, nil + diff := util.Difference(updateParams, dynamicParams) + return diff.Length() == 0, nil } - // if the updated parameter is not in list of DynamicParameter and in list of StaticParameter, - // restart is the default behavior. + // if the updated parameter is not in list of DynamicParameter, + // it is StaticParameter by default, and restart is the default behavior. return false, nil } -// IsParametersUpdateFromManager is used to check whether the parameters are updated from manager +// IsParametersUpdateFromManager checks if the parameters are updated from manager func IsParametersUpdateFromManager(cm *corev1.ConfigMap) bool { annotation := cm.ObjectMeta.Annotations if annotation == nil { @@ -96,20 +124,27 @@ func IsParametersUpdateFromManager(cm *corev1.ConfigMap) bool { return v == constant.ReconfigureManagerSource } -// IsNotUserReconfigureOperation is used to check whether the parameters are updated from operation +// IsNotUserReconfigureOperation checks if the parameters are updated from operation func IsNotUserReconfigureOperation(cm *corev1.ConfigMap) bool { labels := cm.GetLabels() - if labels == nil { - return true + annotations := cm.GetAnnotations() + if labels == nil || annotations == nil { + return false + } + if _, ok := annotations[constant.CMInsEnableRerenderTemplateKey]; !ok { + return false } lastReconfigurePhase := labels[constant.CMInsLastReconfigurePhaseKey] + if annotations[constant.KBParameterUpdateSourceAnnotationKey] != constant.ReconfigureManagerSource { + return false + } return lastReconfigurePhase == "" || ReconfigureCreatedPhase == lastReconfigurePhase } -// SetParametersUpdateSource is used to set the parameters update source +// SetParametersUpdateSource sets the parameters' update source // manager: parameter only updated from manager // external-template: parameter only updated from template -// ops: parameter has updated from operation +// ops: parameter updated from operation func SetParametersUpdateSource(cm *corev1.ConfigMap, source string) { annotation := cm.GetAnnotations() if annotation == nil { @@ -118,3 +153,25 @@ func SetParametersUpdateSource(cm *corev1.ConfigMap, source string) { annotation[constant.KBParameterUpdateSourceAnnotationKey] = source cm.SetAnnotations(annotation) } + +func IsSchedulableConfigResource(object client.Object) bool { + var requiredLabels = []string{ + constant.AppNameLabelKey, + constant.AppInstanceLabelKey, + constant.KBAppComponentLabelKey, + constant.CMConfigurationTemplateNameLabelKey, + constant.CMConfigurationTypeLabelKey, + constant.CMConfigurationSpecProviderLabelKey, + } + + labels := object.GetLabels() + if len(labels) == 0 { + return false + } + for _, label := range requiredLabels { + if _, ok := labels[label]; !ok { + return false + } + } + return true +} diff --git a/internal/configuration/reconfigure_util_test.go b/internal/configuration/reconfigure_util_test.go index 1d7c4468e..361effc89 100644 --- a/internal/configuration/reconfigure_util_test.go +++ b/internal/configuration/reconfigure_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -21,8 +24,13 @@ import ( "github.com/StudioSol/set" "github.com/stretchr/testify/require" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/configuration/util" + "github.com/apecloud/kubeblocks/internal/constant" ) func TestGetUpdateParameterList(t *testing.T) { @@ -43,14 +51,17 @@ func TestGetUpdateParameterList(t *testing.T) { ], "g": { "cd" : "abcd", - "msld" : "cakl" + "msld" : { + "cakl": 100, + "dg": "abcd" + } }} ` - params, err := getUpdateParameterList(newCfgDiffMeta(testData, nil, nil)) + expected := set.NewLinkedHashSetString("a", "f", "c", "xxx.test1", "xxx.test2", "g.msld.cakl", "g.msld.dg", "g.cd") + params, err := getUpdateParameterList(newCfgDiffMeta(testData, nil, nil), "") require.Nil(t, err) - require.True(t, EqSet( - set.NewLinkedHashSetString("a", "c_1", "c_0", "msld", "cd", "f", "test1", "test2"), - set.NewLinkedHashSetString(params...))) + require.True(t, util.EqSet(expected, + set.NewLinkedHashSetString(params...)), "param: %v, expected: %v", params, expected.AsSlice()) } func newCfgDiffMeta(testData string, add, delete map[string]interface{}) *ConfigPatchInfo { @@ -179,3 +190,50 @@ func TestIsUpdateDynamicParameters(t *testing.T) { }) } } + +func TestIsSchedulableConfigResource(t *testing.T) { + tests := []struct { + name string + object client.Object + want bool + }{{ + name: "test", + object: &corev1.ConfigMap{ObjectMeta: metav1.ObjectMeta{}}, + want: false, + }, { + name: "test", + object: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + constant.AppNameLabelKey: "test", + constant.AppInstanceLabelKey: "test", + constant.KBAppComponentLabelKey: "component", + }, + }, + }, + want: false, + }, { + name: "test", + object: &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Labels: map[string]string{ + constant.AppNameLabelKey: "test", + constant.AppInstanceLabelKey: "test", + constant.KBAppComponentLabelKey: "component", + constant.CMConfigurationTemplateNameLabelKey: "test_config_template", + constant.CMConfigurationConstraintsNameLabelKey: "test_config_constraint", + constant.CMConfigurationSpecProviderLabelKey: "for_test_config", + constant.CMConfigurationTypeLabelKey: constant.ConfigInstanceType, + }, + }, + }, + want: true, + }} + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsSchedulableConfigResource(tt.object); got != tt.want { + t.Errorf("IsSchedulableConfigResource() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/internal/configuration/suite_test.go b/internal/configuration/suite_test.go index 57ecdbfd5..6be50e691 100644 --- a/internal/configuration/suite_test.go +++ b/internal/configuration/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration diff --git a/internal/configuration/type.go b/internal/configuration/type.go index ec51c6762..1b8091924 100644 --- a/internal/configuration/type.go +++ b/internal/configuration/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package configuration @@ -89,14 +92,14 @@ type VisualizedParam struct { } type ConfigOperator interface { - // MergeFrom update parameter by keyvalue + // MergeFrom updates parameter in key-value MergeFrom(params map[string]interface{}, option CfgOpOption) error // MergeFromConfig(fileContent []byte, option CfgOpOption) error // MergePatch(jsonPatch []byte, option CfgOpOption) error // Diff(target *ConfigOperator) (*ConfigPatchInfo, error) - // Query get parameter + // Query gets parameter Query(jsonpath string, option CfgOpOption) ([]byte, error) // ToCfgContent to configuration file content @@ -136,18 +139,18 @@ func FromConfigData(data map[string]string, cmKeys *set.LinkedHashSetString) *Co } } -// GenerateTPLUniqLabelKeyWithConfig generate uniq key for configuration template +// GenerateTPLUniqLabelKeyWithConfig generates uniq key for configuration template // reference: docs/img/reconfigure-cr-relationship.drawio.png func GenerateTPLUniqLabelKeyWithConfig(configKey string) string { return GenerateUniqKeyWithConfig(constant.ConfigurationTplLabelPrefixKey, configKey) } -// GenerateUniqKeyWithConfig is similar to getInstanceCfgCMName, generate uniq label or annotations for configuration template +// GenerateUniqKeyWithConfig is similar to getInstanceCfgCMName, generates uniq label/annotations for configuration template func GenerateUniqKeyWithConfig(label string, configKey string) string { return fmt.Sprintf("%s-%s", label, configKey) } -// GenerateConstraintsUniqLabelKeyWithConfig generate uniq key for configure template +// GenerateConstraintsUniqLabelKeyWithConfig generates uniq key for configure template // reference: docs/img/reconfigure-cr-relationship.drawio.png func GenerateConstraintsUniqLabelKeyWithConfig(configKey string) string { return GenerateUniqKeyWithConfig(constant.ConfigurationConstraintsLabelPrefixKey, configKey) @@ -165,7 +168,7 @@ func getInstanceCfgCMName(objName, tplName string) string { return fmt.Sprintf("%s-%s", objName, tplName) } -// GetComponentCfgName is similar to getInstanceCfgCMName, without statefulSet object. +// GetComponentCfgName is similar to getInstanceCfgCMName, while without statefulSet object. func GetComponentCfgName(clusterName, componentName, tplName string) string { return getInstanceCfgCMName(fmt.Sprintf("%s-%s", clusterName, componentName), tplName) } diff --git a/internal/configuration/util/file_util.go b/internal/configuration/util/file_util.go new file mode 100644 index 000000000..62302546e --- /dev/null +++ b/internal/configuration/util/file_util.go @@ -0,0 +1,37 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "os" + "path/filepath" +) + +func FromConfigFiles(files []string) (map[string]string, error) { + m := make(map[string]string) + for _, file := range files { + b, err := os.ReadFile(file) + if err != nil { + return nil, err + } + m[filepath.Base(file)] = string(b) + } + return m, nil +} diff --git a/internal/configuration/util/hash.go b/internal/configuration/util/hash.go new file mode 100644 index 000000000..255fbf4bd --- /dev/null +++ b/internal/configuration/util/hash.go @@ -0,0 +1,47 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "bytes" + "encoding/json" + "fmt" + "hash/fnv" + "io" + + "github.com/pkg/errors" + "k8s.io/apimachinery/pkg/util/rand" +) + +func ComputeHash(object interface{}) (string, error) { + objString, err := json.Marshal(object) + if err != nil { + return "", errors.Wrap(err, "failed to compute hash.") + } + + // hasher := sha1.New() + hasher := fnv.New32() + if _, err := io.Copy(hasher, bytes.NewReader(objString)); err != nil { + return "", errors.Wrapf(err, "failed to compute hash for sha256. [%s]", objString) + } + + sha := hasher.Sum32() + return rand.SafeEncodeString(fmt.Sprint(sha)), nil +} diff --git a/internal/configuration/hash_test.go b/internal/configuration/util/hash_test.go similarity index 51% rename from internal/configuration/hash_test.go rename to internal/configuration/util/hash_test.go index 935f2c9e4..947157f5a 100644 --- a/internal/configuration/hash_test.go +++ b/internal/configuration/util/hash_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import "testing" diff --git a/internal/configuration/util/jsonpath.go b/internal/configuration/util/jsonpath.go new file mode 100644 index 000000000..acba9ff19 --- /dev/null +++ b/internal/configuration/util/jsonpath.go @@ -0,0 +1,58 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "encoding/json" + + "github.com/bhmj/jsonslice" + jsonpatch "github.com/evanphx/json-patch" +) + +func RetrievalWithJSONPath(jsonObj interface{}, jsonpath string) ([]byte, error) { + jsonBytes, err := json.Marshal(&jsonObj) + if err != nil { + return nil, err + } + + res, err := jsonslice.Get(jsonBytes, jsonpath) + if err != nil { + return res, err + } + + resLen := len(res) + if resLen > 2 && res[0] == '"' && res[resLen-1] == '"' { + res = res[1 : resLen-1] + } + return res, err +} + +func JSONPatch(originalJSON, modifiedJSON interface{}) ([]byte, error) { + originalBytes, err := json.Marshal(originalJSON) + if err != nil { + return nil, err + } + + modifiedBytes, err := json.Marshal(modifiedJSON) + if err != nil { + return nil, err + } + return jsonpatch.CreateMergePatch(originalBytes, modifiedBytes) +} diff --git a/internal/configuration/util/math.go b/internal/configuration/util/math.go new file mode 100644 index 000000000..f9d80db51 --- /dev/null +++ b/internal/configuration/util/math.go @@ -0,0 +1,36 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import "golang.org/x/exp/constraints" + +func Min[T constraints.Ordered](l, r T) T { + if l < r { + return l + } + return r +} + +func Max[T constraints.Ordered](l, r T) T { + if l < r { + return r + } + return l +} diff --git a/internal/configuration/math_test.go b/internal/configuration/util/math_test.go similarity index 63% rename from internal/configuration/math_test.go rename to internal/configuration/util/math_test.go index 8c20ba020..013388efd 100644 --- a/internal/configuration/math_test.go +++ b/internal/configuration/util/math_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import ( "reflect" diff --git a/internal/configuration/set.go b/internal/configuration/util/set.go similarity index 54% rename from internal/configuration/set.go rename to internal/configuration/util/set.go index 7aeb0b3c2..fb0318892 100644 --- a/internal/configuration/set.go +++ b/internal/configuration/util/set.go @@ -1,25 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import "github.com/StudioSol/set" -// Set type Reference c++ set interface to implemented stl set. -// With generics, it may be more generic. +// Set type Reference c++ set interface to implement stl set. func Difference(left, right *set.LinkedHashSetString) *set.LinkedHashSetString { diff := set.NewLinkedHashSetString() diff --git a/internal/configuration/set_test.go b/internal/configuration/util/set_test.go similarity index 74% rename from internal/configuration/set_test.go rename to internal/configuration/util/set_test.go index 4fc565f41..1ef17456f 100644 --- a/internal/configuration/set_test.go +++ b/internal/configuration/util/set_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import ( "testing" diff --git a/internal/configuration/unstructured.go b/internal/configuration/util/unstructured.go similarity index 67% rename from internal/configuration/unstructured.go rename to internal/configuration/util/unstructured.go index dc8d79dcb..a059b3904 100644 --- a/internal/configuration/unstructured.go +++ b/internal/configuration/util/unstructured.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import ( "fmt" @@ -46,7 +49,7 @@ type unstructuredAccessor struct { func (accessor *unstructuredAccessor) Visit(data interface{}) error { v := reflect.ValueOf(data) if !v.IsValid() { - return MakeError("invalid data type: %T", data) + return fmt.Errorf("invalid data type: %T", data) } return accessor.visitValueType(v, v.Type(), "", "", nil) } @@ -77,25 +80,25 @@ func (accessor *unstructuredAccessor) visitValueType(v reflect.Value, t reflect. implValue := v.Elem() return accessor.visitValueType(implValue, implValue.Type(), parent, cur, updateFn) case reflect.Struct: - return accessor.visitStruct(v, cur) + return accessor.visitStruct(v, joinFieldPath(parent, cur)) case reflect.Map: - return accessor.visitMap(v, t, cur) + return accessor.visitMap(v, t, joinFieldPath(parent, cur)) case reflect.Slice: - return accessor.visitArray(v, t.Elem(), cur) + return accessor.visitArray(v, t.Elem(), parent, cur) case reflect.Array: - return accessor.visitArray(v, t.Elem(), cur) + return accessor.visitArray(v, t.Elem(), parent, cur) case reflect.Pointer: return accessor.visitValueType(v, t.Elem(), parent, cur, updateFn) default: - return MakeError("not support type: %s", k) + return fmt.Errorf("not supported type: %s", k) } } -func (accessor *unstructuredAccessor) visitArray(v reflect.Value, t reflect.Type, parent string) error { +func (accessor *unstructuredAccessor) visitArray(v reflect.Value, t reflect.Type, parent, cur string) error { n := v.Len() for i := 0; i < n; i++ { - index := fmt.Sprintf("%s_%d", parent, i) - if err := accessor.visitValueType(v.Index(i), t, parent, index, nil); err != nil { + // index := fmt.Sprintf("%s_%d", parent, i) + if err := accessor.visitValueType(v.Index(i), t, parent, cur, nil); err != nil { return err } } @@ -111,7 +114,7 @@ func (accessor *unstructuredAccessor) visitMap(v reflect.Value, t reflect.Type, switch k := t.Key().Kind(); k { case reflect.String: default: - return MakeError("not support key type: %s", k) + return fmt.Errorf("not supported key type: %s", k) } t = t.Elem() @@ -157,6 +160,18 @@ func toString(key reflect.Value, kind reflect.Kind) string { } } +func joinFieldPath(parent, cur string) string { + if parent == "" { + return cur + } + + if cur == "" { + return parent + } + + return parent + "." + cur +} + func (accessor *unstructuredAccessor) visitStruct(v reflect.Value, parent string) error { - return MakeError("not support struct.") + return fmt.Errorf("not supported struct") } diff --git a/internal/configuration/unstructured_test.go b/internal/configuration/util/unstructured_test.go similarity index 66% rename from internal/configuration/unstructured_test.go rename to internal/configuration/util/unstructured_test.go index a96a6e10a..2f77f6994 100644 --- a/internal/configuration/unstructured_test.go +++ b/internal/configuration/util/unstructured_test.go @@ -1,20 +1,23 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ -package configuration +package util import ( "encoding/json" @@ -76,7 +79,7 @@ func TestUnstructuredObjectWalk(t *testing.T) { "g" : [ "e1","e2","e3"], "x" : [ 20,30] }}`, - expected: []string{"c", "d", "f", "x1", "x2", "x3", "x4"}, + expected: []string{"a.b.z.x1", "a.b.e.c", "a.b.e.d", "a.b.f", "a.b.z.x2", "a.b.z.x4", "a.b.z.x3", "a.g", "a.x"}, isStruct: false, }, }, { @@ -109,6 +112,9 @@ func TestUnstructuredObjectWalk(t *testing.T) { if cur == "" && parent != "" { res = append(res, parent) } else if cur != "" { + if parent != "" { + cur = parent + "." + cur + } res = append(res, cur) } return nil @@ -117,7 +123,7 @@ func TestUnstructuredObjectWalk(t *testing.T) { } if !tt.wantErr { - require.True(t, Contains(res, tt.args.expected)) + require.True(t, Contains(res, tt.args.expected), "res: %v, expected: %v", res, tt.args.expected) } }) } diff --git a/internal/constant/const.go b/internal/constant/const.go index 7f78ebab1..0ed2b5410 100644 --- a/internal/constant/const.go +++ b/internal/constant/const.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package constant @@ -23,18 +26,31 @@ const ( CfgKeyCtrlrMgrAffinity = "CM_AFFINITY" CfgKeyCtrlrMgrNodeSelector = "CM_NODE_SELECTOR" CfgKeyCtrlrMgrTolerations = "CM_TOLERATIONS" - CfgKeyCtrlrReconcileRetryDurationMS = "CM_RECON_RETRY_DURATION_MS" // accept time + CfgKeyCtrlrReconcileRetryDurationMS = "CM_RECON_RETRY_DURATION_MS" // accept time + CfgKeyBackupPVCName = "BACKUP_PVC_NAME" // the global persistent volume claim to store the backup data + CfgKeyBackupPVCInitCapacity = "BACKUP_PVC_INIT_CAPACITY" // the init capacity for creating the pvc, e.g. 10Gi. + CfgKeyBackupPVCStorageClass = "BACKUP_PVC_STORAGE_CLASS" // the pvc storage class name. + CfgKeyBackupPVCCreatePolicy = "BACKUP_PVC_CREATE_POLICY" // the pvc creation policy, choice is "IfNotPresent" or "Never" + CfgKeyBackupPVConfigmapName = "BACKUP_PV_CONFIGMAP_NAME" // the configmap containing the persistentVolume template. + CfgKeyBackupPVConfigmapNamespace = "BACKUP_PV_CONFIGMAP_NAMESPACE" // the configmap namespace containing the persistentVolume template. + CfgRecoverVolumeExpansionFailure = "RECOVER_VOLUME_EXPANSION_FAILURE" // refer to feature gates RecoverVolumeExpansionFailure of k8s. // addon config keys CfgKeyAddonJobTTL = "ADDON_JOB_TTL" CfgAddonJobImgPullPolicy = "ADDON_JOB_IMAGE_PULL_POLICY" + + // data plane config key + CfgKeyDataPlaneTolerations = "DATA_PLANE_TOLERATIONS" + CfgKeyDataPlaneAffinity = "DATA_PLANE_AFFINITY" ) const ( - ConnCredentialPlaceHolder = "$(CONN_CREDENTIAL_SECRET_NAME)" - KBCompNamePlaceHolder = "$(KB_COMP_NAME)" - KBClusterNamePlaceHolder = "$(KB_CLUSTER_NAME)" - KBClusterCompNamePlaceHolder = "$(KB_CLUSTER_COMP_NAME)" + KBConnCredentialPlaceHolder = "$(CONN_CREDENTIAL_SECRET_NAME)" + KBComponentEnvCMPlaceHolder = "$(COMP_ENV_CM_NAME)" + KBCompNamePlaceHolder = "$(KB_COMP_NAME)" + KBClusterNamePlaceHolder = "$(KB_CLUSTER_NAME)" + KBClusterCompNamePlaceHolder = "$(KB_CLUSTER_COMP_NAME)" + KBClusterUIDPostfix8PlaceHolder = "$(KB_CLUSTER_UID_POSTFIX_8)" ) const ( @@ -51,7 +67,7 @@ const ( AppName = "kubeblocks" - // K8s recommonded and well-known label and annotation keys + // K8s recommonded well-known labels and annotation keys AppInstanceLabelKey = "app.kubernetes.io/instance" AppNameLabelKey = "app.kubernetes.io/name" AppManagedByLabelKey = "app.kubernetes.io/managed-by" @@ -59,61 +75,74 @@ const ( ZoneLabelKey = "topology.kubernetes.io/zone" // kubeblocks.io labels - ClusterDefLabelKey = "clusterdefinition.kubeblocks.io/name" - KBAppComponentLabelKey = "apps.kubeblocks.io/component-name" - KBAppComponentDefRefLabelKey = "apps.kubeblocks.io/component-def-ref" - ConsensusSetAccessModeLabelKey = "cs.apps.kubeblocks.io/access-mode" - AppConfigTypeLabelKey = "apps.kubeblocks.io/config-type" - WorkloadTypeLabelKey = "apps.kubeblocks.io/workload-type" - VolumeClaimTemplateNameLabelKey = "vct.kubeblocks.io/name" - RoleLabelKey = "kubeblocks.io/role" // RoleLabelKey consensusSet and replicationSet role label key - BackupProtectionLabelKey = "kubeblocks.io/backup-protection" // BackupProtectionLabelKey Backup delete protection policy label - AddonNameLabelKey = "extensions.kubeblocks.io/addon-name" - ClusterAccountLabelKey = "account.kubeblocks.io/name" - VolumeTypeLabelKey = "kubeblocks.io/volume-type" - KBManagedByKey = "apps.kubeblocks.io/managed-by" // KBManagedByKey marks resources that auto created during operation - ClassProviderLabelKey = "class.kubeblocks.io/provider" + BackupProtectionLabelKey = "kubeblocks.io/backup-protection" // BackupProtectionLabelKey Backup delete protection policy label + BackupToolTypeLabelKey = "kubeblocks.io/backup-tool-type" + AddonProviderLableKey = "kubeblocks.io/provider" // AddonProviderLableKey marks the addon provider + RoleLabelKey = "kubeblocks.io/role" // RoleLabelKey consensusSet and replicationSet role label key + VolumeTypeLabelKey = "kubeblocks.io/volume-type" + ClusterAccountLabelKey = "account.kubeblocks.io/name" + KBAppComponentLabelKey = "apps.kubeblocks.io/component-name" + KBAppComponentDefRefLabelKey = "apps.kubeblocks.io/component-def-ref" + AppConfigTypeLabelKey = "apps.kubeblocks.io/config-type" + KBManagedByKey = "apps.kubeblocks.io/managed-by" // KBManagedByKey marks resources that auto created + PVCNameLabelKey = "apps.kubeblocks.io/pvc-name" + VolumeClaimTemplateNameLabelKey = "apps.kubeblocks.io/vct-name" + WorkloadTypeLabelKey = "apps.kubeblocks.io/workload-type" + ClassProviderLabelKey = "class.kubeblocks.io/provider" + ClusterDefLabelKey = "clusterdefinition.kubeblocks.io/name" + CMConfigurationSpecProviderLabelKey = "config.kubeblocks.io/config-spec" // CMConfigurationSpecProviderLabelKey is ComponentConfigSpec name + CMConfigurationCMKeysLabelKey = "config.kubeblocks.io/configmap-keys" // CMConfigurationCMKeysLabelKey Specify configmap keys + CMConfigurationTemplateNameLabelKey = "config.kubeblocks.io/config-template-name" + CMConfigurationTypeLabelKey = "config.kubeblocks.io/config-type" + CMInsConfigurationHashLabelKey = "config.kubeblocks.io/config-hash" + CMConfigurationConstraintsNameLabelKey = "config.kubeblocks.io/config-constraints-name" + ConsensusSetAccessModeLabelKey = "cs.apps.kubeblocks.io/access-mode" + BackupTypeLabelKeyKey = "dataprotection.kubeblocks.io/backup-type" + AddonNameLabelKey = "extensions.kubeblocks.io/addon-name" + OpsRequestTypeLabelKey = "ops.kubeblocks.io/ops-type" // kubeblocks.io annotations - OpsRequestAnnotationKey = "kubeblocks.io/ops-request" // OpsRequestAnnotationKey OpsRequest annotation key in Cluster - ReconcileAnnotationKey = "kubeblocks.io/reconcile" // ReconcileAnnotationKey Notify k8s object to reconcile - RestartAnnotationKey = "kubeblocks.io/restart" // RestartAnnotationKey the annotation which notices the StatefulSet/DeploySet to restart - SnapShotForStartAnnotationKey = "kubeblocks.io/snapshot-for-start" - RestoreFromBackUpAnnotationKey = "kubeblocks.io/restore-from-backup" // RestoreFromBackUpAnnotationKey specifies the component to recover from the backup. - ClusterSnapshotAnnotationKey = "kubeblocks.io/cluster-snapshot" // ClusterSnapshotAnnotationKey saves the snapshot of cluster. - LeaderAnnotationKey = "cs.apps.kubeblocks.io/leader" - - // ConfigurationTplLabelPrefixKey clusterVersion or clusterdefinition using tpl - ConfigurationTplLabelPrefixKey = "config.kubeblocks.io/tpl" - ConfigurationConstraintsLabelPrefixKey = "config.kubeblocks.io/constraints" - - LastAppliedOpsCRAnnotation = "config.kubeblocks.io/last-applied-ops-name" - LastAppliedConfigAnnotation = "config.kubeblocks.io/last-applied-configuration" + ClusterSnapshotAnnotationKey = "kubeblocks.io/cluster-snapshot" // ClusterSnapshotAnnotationKey saves the snapshot of cluster. + DefaultClusterVersionAnnotationKey = "kubeblocks.io/is-default-cluster-version" // DefaultClusterVersionAnnotationKey specifies the default cluster version. + OpsRequestAnnotationKey = "kubeblocks.io/ops-request" // OpsRequestAnnotationKey OpsRequest annotation key in Cluster + ReconcileAnnotationKey = "kubeblocks.io/reconcile" // ReconcileAnnotationKey Notify k8s object to reconcile + RestartAnnotationKey = "kubeblocks.io/restart" // RestartAnnotationKey the annotation which notices the StatefulSet/DeploySet to restart + RestoreFromTimeAnnotationKey = "kubeblocks.io/restore-from-time" // RestoreFromTimeAnnotationKey specifies the time to recover from the backup. + RestoreFromSrcClusterAnnotationKey = "kubeblocks.io/restore-from-source-cluster" // RestoreFromSrcClusterAnnotationKey specifies the source cluster to recover from the backup. + RestoreFromBackUpAnnotationKey = "kubeblocks.io/restore-from-backup" // RestoreFromBackUpAnnotationKey specifies the component to recover from the backup. + SnapShotForStartAnnotationKey = "kubeblocks.io/snapshot-for-start" + ComponentReplicasAnnotationKey = "apps.kubeblocks.io/component-replicas" // ComponentReplicasAnnotationKey specifies the number of pods in replicas + BackupPolicyTemplateAnnotationKey = "apps.kubeblocks.io/backup-policy-template" + LastAppliedClusterAnnotationKey = "apps.kubeblocks.io/last-applied-cluster" + PVLastClaimPolicyAnnotationKey = "apps.kubeblocks.io/pv-last-claim-policy" + HaltRecoveryAllowInconsistentCVAnnotKey = "clusters.apps.kubeblocks.io/allow-inconsistent-cv" + HaltRecoveryAllowInconsistentResAnnotKey = "clusters.apps.kubeblocks.io/allow-inconsistent-resource" + LeaderAnnotationKey = "cs.apps.kubeblocks.io/leader" + DefaultBackupPolicyAnnotationKey = "dataprotection.kubeblocks.io/is-default-policy" // DefaultBackupPolicyAnnotationKey specifies the default backup policy. + DefaultBackupPolicyTemplateAnnotationKey = "dataprotection.kubeblocks.io/is-default-policy-template" // DefaultBackupPolicyTemplateAnnotationKey specifies the default backup policy template. + BackupDataPathPrefixAnnotationKey = "dataprotection.kubeblocks.io/path-prefix" // BackupDataPathPrefixAnnotationKey specifies the backup data path prefix. + ReconfigureRefAnnotationKey = "dataprotection.kubeblocks.io/reconfigure-ref" DisableUpgradeInsConfigurationAnnotationKey = "config.kubeblocks.io/disable-reconfigure" + LastAppliedConfigAnnotationKey = "config.kubeblocks.io/last-applied-configuration" + LastAppliedOpsCRAnnotationKey = "config.kubeblocks.io/last-applied-ops-name" UpgradePolicyAnnotationKey = "config.kubeblocks.io/reconfigure-policy" - UpgradeRestartAnnotationKey = "config.kubeblocks.io/restart" KBParameterUpdateSourceAnnotationKey = "config.kubeblocks.io/reconfigure-source" + UpgradeRestartAnnotationKey = "config.kubeblocks.io/restart" + KubeBlocksGenerationKey = "kubeblocks.io/generation" - // CMConfigurationTypeLabelKey configmap is config template type, e.g: "tpl", "instance" - CMConfigurationTypeLabelKey = "config.kubeblocks.io/config-type" - CMConfigurationTemplateNameLabelKey = "config.kubeblocks.io/config-template-name" - CMConfigurationConstraintsNameLabelKey = "config.kubeblocks.io/config-constraints-name" - CMInsConfigurationHashLabelKey = "config.kubeblocks.io/config-hash" - - // CMConfigurationSpecProviderLabelKey is ComponentConfigSpec name - CMConfigurationSpecProviderLabelKey = "config.kubeblocks.io/config-spec" - - // CMConfigurationCMKeysLabelKey Specify keys - CMConfigurationCMKeysLabelKey = "config.kubeblocks.io/configmap-keys" + // kubeblocks.io well-known finalizers + DBClusterFinalizerName = "cluster.kubeblocks.io/finalizer" + ConfigurationTemplateFinalizerName = "config.kubeblocks.io/finalizer" - // CMInsConfigurationLabelKey configmap is configuration file for component - // CMInsConfigurationLabelKey = "config.kubeblocks.io/ins-configure" + // ConfigurationTplLabelPrefixKey clusterVersion or clusterdefinition using tpl + ConfigurationTplLabelPrefixKey = "config.kubeblocks.io/tpl" + ConfigurationConstraintsLabelPrefixKey = "config.kubeblocks.io/constraints" // CMInsLastReconfigurePhaseKey defines the current phase CMInsLastReconfigurePhaseKey = "config.kubeblocks.io/last-applied-reconfigure-phase" - // configuration finalizer - ConfigurationTemplateFinalizerName = "config.kubeblocks.io/finalizer" + // CMInsEnableRerenderTemplateKey is used to enable rerender template + CMInsEnableRerenderTemplateKey = "config.kubeblocks.io/enable-rerender" // ClassAnnotationKey is used to specify the class of components ClassAnnotationKey = "cluster.kubeblocks.io/component-class" @@ -143,10 +172,11 @@ const ( PersistentVolumeClaimKind = "PersistentVolumeClaim" CronJobKind = "CronJob" JobKind = "Job" - ReplicaSetKind = "ReplicaSetKind" + ReplicaSetKind = "ReplicaSet" VolumeSnapshotKind = "VolumeSnapshot" ServiceKind = "Service" ConfigMapKind = "ConfigMap" + DaemonSetKind = "DaemonSet" ) const ( @@ -166,21 +196,12 @@ const ( ProbeGRPCPortName = "probe-grpc-port" RoleProbeContainerName = "kb-checkrole" StatusProbeContainerName = "kb-checkstatus" - RunningProbeContainerName = "kb-runningcheck" + RunningProbeContainerName = "kb-checkrunning" // the filedpath name used in event.InvolvedObject.FieldPath ProbeCheckRolePath = "spec.containers{" + RoleProbeContainerName + "}" ProbeCheckStatusPath = "spec.containers{" + StatusProbeContainerName + "}" ProbeCheckRunningPath = "spec.containers{" + RunningProbeContainerName + "}" - - // KubeBlocksDataNodeLabelKey is the node label key of the built-in data node label - KubeBlocksDataNodeLabelKey = "kb-data" - // KubeBlocksDataNodeLabelValue is the node label value of the built-in data node label - KubeBlocksDataNodeLabelValue = "true" - // KubeBlocksDataNodeTolerationKey is the taint label key of the built-in data node taint - KubeBlocksDataNodeTolerationKey = "kb-data" - // KubeBlocksDataNodeTolerationValue is the taint label value of the built-in data node taint - KubeBlocksDataNodeTolerationValue = "true" ) const ( @@ -200,3 +221,15 @@ const ( const ( KBReplicationSetPrimaryPodName = "KB_PRIMARY_POD_NAME" ) + +// username and password are keys in created secrets for others to refer to. +const ( + AccountNameForSecret = "username" + AccountPasswdForSecret = "password" +) + +const DefaultBackupPvcInitCapacity = "20Gi" + +const ( + ComponentStatusDefaultPodName = "Unknown" +) diff --git a/internal/controller/builder/builder.go b/internal/controller/builder/builder.go index a5bd768f7..04830f757 100644 --- a/internal/controller/builder/builder.go +++ b/internal/controller/builder/builder.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package builder @@ -33,14 +36,12 @@ import ( batchv1 "k8s.io/api/batch/v1" corev1 "k8s.io/api/core/v1" policyv1 "k8s.io/api/policy/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/rand" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - componentutil "github.com/apecloud/kubeblocks/controllers/apps/components/util" cfgcm "github.com/apecloud/kubeblocks/internal/configuration/config_manager" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/component" @@ -116,7 +117,8 @@ func buildFromCUE(tplName string, fillMap map[string]any, lookupKey string, targ } func processContainersInjection(reqCtx intctrlutil.RequestCtx, - params BuilderParams, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, envConfigName string, podSpec *corev1.PodSpec) error { for _, cc := range []*[]corev1.Container{ @@ -124,19 +126,20 @@ func processContainersInjection(reqCtx intctrlutil.RequestCtx, &podSpec.InitContainers, } { for i := range *cc { - injectEnvs(params, envConfigName, &(*cc)[i]) + injectEnvs(cluster, component, envConfigName, &(*cc)[i]) } } return nil } -func injectEnvs(params BuilderParams, envConfigName string, c *corev1.Container) { +func injectEnvs(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent, envConfigName string, c *corev1.Container) { // can not use map, it is unordered envFieldPathSlice := []struct { name string fieldPath string }{ {name: "KB_POD_NAME", fieldPath: "metadata.name"}, + {name: "KB_POD_UID", fieldPath: "metadata.uid"}, {name: "KB_NAMESPACE", fieldPath: "metadata.namespace"}, {name: "KB_SA_NAME", fieldPath: "spec.serviceAccountName"}, {name: "KB_NODENAME", fieldPath: "spec.nodeName"}, @@ -161,15 +164,22 @@ func injectEnvs(params BuilderParams, envConfigName string, c *corev1.Container) }) } + var kbClusterPostfix8 string + if len(cluster.UID) > 8 { + kbClusterPostfix8 = string(cluster.UID)[len(cluster.UID)-8:] + } else { + kbClusterPostfix8 = string(cluster.UID) + } toInjectEnvs = append(toInjectEnvs, []corev1.EnvVar{ - {Name: "KB_CLUSTER_NAME", Value: params.Cluster.Name}, - {Name: "KB_COMP_NAME", Value: params.Component.Name}, - {Name: "KB_CLUSTER_COMP_NAME", Value: params.Cluster.Name + "-" + params.Component.Name}, + {Name: "KB_CLUSTER_NAME", Value: cluster.Name}, + {Name: "KB_COMP_NAME", Value: component.Name}, + {Name: "KB_CLUSTER_COMP_NAME", Value: cluster.Name + "-" + component.Name}, + {Name: "KB_CLUSTER_UID_POSTFIX_8", Value: kbClusterPostfix8}, {Name: "KB_POD_FQDN", Value: fmt.Sprintf("%s.%s-headless.%s.svc", "$(KB_POD_NAME)", "$(KB_CLUSTER_COMP_NAME)", "$(KB_NAMESPACE)")}, }...) - if params.Component.TLS { + if component.TLS { toInjectEnvs = append(toInjectEnvs, []corev1.EnvVar{ {Name: "KB_TLS_CERT_PATH", Value: MountPath}, {Name: "KB_TLS_CA_FILE", Value: CAName}, @@ -195,7 +205,7 @@ func injectEnvs(params BuilderParams, envConfigName string, c *corev1.Container) }) } -// BuildPersistentVolumeClaimLabels builds a pvc name label, and synchronize the labels on the sts to the pvc labels. +// BuildPersistentVolumeClaimLabels builds a pvc name label, and synchronize the labels from sts to pvc. func BuildPersistentVolumeClaimLabels(sts *appsv1.StatefulSet, pvc *corev1.PersistentVolumeClaim, component *component.SynthesizedComponent, pvcTplName string) { // strict args checking. @@ -223,35 +233,50 @@ func BuildPersistentVolumeClaimLabels(sts *appsv1.StatefulSet, pvc *corev1.Persi } } -func BuildSvcList(params BuilderParams) ([]*corev1.Service, error) { - const tplFile = "service_template.cue" +func BuildSvcListWithCustomAttributes(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent, + customAttributeSetter func(*corev1.Service)) ([]*corev1.Service, error) { + services, err := BuildSvcListLow(cluster, component) + if err != nil { + return nil, err + } + if customAttributeSetter != nil { + for _, svc := range services { + customAttributeSetter(svc) + } + } + return services, nil +} - var result []*corev1.Service - for _, item := range params.Component.Services { +func BuildSvcListLow(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent) ([]*corev1.Service, error) { + const tplFile = "service_template.cue" + var result = make([]*corev1.Service, 0) + for _, item := range component.Services { if len(item.Spec.Ports) == 0 { continue } svc := corev1.Service{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, + "cluster": cluster, "service": item, - "component": params.Component, + "component": component, }, "svc", &svc); err != nil { return nil, err } result = append(result, &svc) } - return result, nil } func BuildHeadlessSvc(params BuilderParams) (*corev1.Service, error) { - const tplFile = "headless_service_template.cue" + return BuildHeadlessSvcLow(params.Cluster, params.Component) +} +func BuildHeadlessSvcLow(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent) (*corev1.Service, error) { + const tplFile = "headless_service_template.cue" service := corev1.Service{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, - "component": params.Component, + "cluster": cluster, + "component": component, }, "service", &service); err != nil { return nil, err } @@ -259,25 +284,34 @@ func BuildHeadlessSvc(params BuilderParams) (*corev1.Service, error) { } func BuildSts(reqCtx intctrlutil.RequestCtx, params BuilderParams, envConfigName string) (*appsv1.StatefulSet, error) { + return BuildStsLow(reqCtx, params.Cluster, params.Component, envConfigName) +} + +func BuildStsLow(reqCtx intctrlutil.RequestCtx, cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent, + envConfigName string) (*appsv1.StatefulSet, error) { const tplFile = "statefulset_template.cue" sts := appsv1.StatefulSet{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, - "component": params.Component, + "cluster": cluster, + "component": component, }, "statefulset", &sts); err != nil { return nil, err } + if component.StatefulSetWorkload != nil { + sts.Spec.PodManagementPolicy, sts.Spec.UpdateStrategy = component.StatefulSetWorkload.FinalStsUpdateStrategy() + } + // update sts.spec.volumeClaimTemplates[].metadata.labels if len(sts.Spec.VolumeClaimTemplates) > 0 && len(sts.GetLabels()) > 0 { for index, vct := range sts.Spec.VolumeClaimTemplates { - BuildPersistentVolumeClaimLabels(&sts, &vct, params.Component, vct.Name) + BuildPersistentVolumeClaimLabels(&sts, &vct, component, vct.Name) sts.Spec.VolumeClaimTemplates[index] = vct } } - if err := processContainersInjection(reqCtx, params, envConfigName, &sts.Spec.Template.Spec); err != nil { + if err := processContainersInjection(reqCtx, cluster, component, envConfigName, &sts.Spec.Template.Spec); err != nil { return nil, err } return &sts, nil @@ -288,12 +322,17 @@ func randomString(length int) string { } func BuildConnCredential(params BuilderParams) (*corev1.Secret, error) { + return BuildConnCredentialLow(params.ClusterDefinition, params.Cluster, params.Component) +} + +func BuildConnCredentialLow(clusterDefiniiton *appsv1alpha1.ClusterDefinition, cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) (*corev1.Secret, error) { const tplFile = "conn_credential_template.cue" connCredential := corev1.Secret{} if err := buildFromCUE(tplFile, map[string]any{ - "clusterdefinition": params.ClusterDefinition, - "cluster": params.Cluster, + "clusterdefinition": clusterDefiniiton, + "cluster": cluster, }, "secret", &connCredential); err != nil { return nil, err } @@ -343,15 +382,17 @@ func BuildConnCredential(params BuilderParams) (*corev1.Secret, error) { uuidStrB64 := base64.RawStdEncoding.EncodeToString([]byte(strings.ReplaceAll(uuidStr, "-", ""))) uuidHex := hex.EncodeToString(uuidBytes) m := map[string]string{ - "$(RANDOM_PASSWD)": randomString(8), - "$(UUID)": uuidStr, - "$(UUID_B64)": uuidB64, - "$(UUID_STR_B64)": uuidStrB64, - "$(UUID_HEX)": uuidHex, - "$(SVC_FQDN)": fmt.Sprintf("%s-%s.%s.svc", params.Cluster.Name, params.Component.Name, params.Cluster.Namespace), - } - if len(params.Component.Services) > 0 { - for _, p := range params.Component.Services[0].Spec.Ports { + "$(RANDOM_PASSWD)": randomString(8), + "$(UUID)": uuidStr, + "$(UUID_B64)": uuidB64, + "$(UUID_STR_B64)": uuidStrB64, + "$(UUID_HEX)": uuidHex, + "$(SVC_FQDN)": fmt.Sprintf("%s-%s.%s.svc", cluster.Name, component.Name, cluster.Namespace), + "$(KB_CLUSTER_COMP_NAME)": cluster.Name + "-" + component.Name, + "$(HEADLESS_SVC_FQDN)": fmt.Sprintf("%s-%s-headless.%s.svc", cluster.Name, component.Name, cluster.Namespace), + } + if len(component.Services) > 0 { + for _, p := range component.Services[0].Spec.Ports { m[fmt.Sprintf("$(SVC_PORT_%s)", p.Name)] = strconv.Itoa(int(p.Port)) } } @@ -359,40 +400,48 @@ func BuildConnCredential(params BuilderParams) (*corev1.Secret, error) { // 2nd pass replace $(CONN_CREDENTIAL) variables m = map[string]string{} - for k, v := range connCredential.StringData { m[fmt.Sprintf("$(CONN_CREDENTIAL).%s", k)] = v } - replaceData(m) return &connCredential, nil } func BuildPDB(params BuilderParams) (*policyv1.PodDisruptionBudget, error) { + return BuildPDBLow(params.Cluster, params.Component) +} + +func BuildPDBLow(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent) (*policyv1.PodDisruptionBudget, error) { const tplFile = "pdb_template.cue" pdb := policyv1.PodDisruptionBudget{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, - "component": params.Component, + "cluster": cluster, + "component": component, }, "pdb", &pdb); err != nil { return nil, err } - return &pdb, nil } func BuildDeploy(reqCtx intctrlutil.RequestCtx, params BuilderParams) (*appsv1.Deployment, error) { - const tplFile = "deployment_template.cue" + return BuildDeployLow(reqCtx, params.Cluster, params.Component) +} +func BuildDeployLow(reqCtx intctrlutil.RequestCtx, cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) (*appsv1.Deployment, error) { + const tplFile = "deployment_template.cue" deploy := appsv1.Deployment{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, - "component": params.Component, + "cluster": cluster, + "component": component, }, "deployment", &deploy); err != nil { return nil, err } - if err := processContainersInjection(reqCtx, params, "", &deploy.Spec.Template.Spec); err != nil { + if component.StatelessSpec != nil { + deploy.Spec.Strategy = component.StatelessSpec.UpdateStrategy + } + if err := processContainersInjection(reqCtx, cluster, component, "", &deploy.Spec.Template.Spec); err != nil { return nil, err } return &deploy, nil @@ -403,7 +452,6 @@ func BuildPVCFromSnapshot(sts *appsv1.StatefulSet, pvcKey types.NamespacedName, snapshotName string, component *component.SynthesizedComponent) (*corev1.PersistentVolumeClaim, error) { - pvc := corev1.PersistentVolumeClaim{} if err := buildFromCUE("pvc_template.cue", map[string]any{ "sts": sts, @@ -417,140 +465,81 @@ func BuildPVCFromSnapshot(sts *appsv1.StatefulSet, return &pvc, nil } -// BuildEnvConfig build cluster component context ConfigMap object, which is to be used in workload container's +// BuildEnvConfig builds cluster component context ConfigMap object, which is to be used in workload container's // envFrom.configMapRef with name of "$(cluster.metadata.name)-$(component.name)-env" pattern. func BuildEnvConfig(params BuilderParams, reqCtx intctrlutil.RequestCtx, cli client.Client) (*corev1.ConfigMap, error) { + return BuildEnvConfigLow(reqCtx, cli, params.Cluster, params.Component) +} - isRecreateFromExistingPVC := func() (bool, error) { - ml := client.MatchingLabels{ - constant.AppInstanceLabelKey: params.Cluster.Name, - constant.KBAppComponentLabelKey: params.Component.Name, - } - pvcList := corev1.PersistentVolumeClaimList{} - if err := cli.List(reqCtx.Ctx, &pvcList, ml); err != nil { - return false, err - } - // no pvc means it's not recreation - if len(pvcList.Items) == 0 { - return false, nil - } - // check sts existence - stsList := appsv1.StatefulSetList{} - if err := cli.List(reqCtx.Ctx, &stsList, ml); err != nil { - return false, err - } - // recreation will not have existing sts - if len(stsList.Items) > 0 { - return false, nil - } - // check pod existence - for _, pvc := range pvcList.Items { - vctName := pvc.Annotations[constant.VolumeClaimTemplateNameLabelKey] - podName := strings.TrimPrefix(pvc.Name, vctName+"-") - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Name: podName, Namespace: params.Cluster.Namespace}, &corev1.Pod{}); err != nil { - if apierrors.IsNotFound(err) { - continue - } - return false, err - } - // any pod exists means it's not a recreation - return false, nil - } - // passed all the above checks, so it's a recreation - return true, nil - } - +func BuildEnvConfigLow(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) (*corev1.ConfigMap, error) { const tplFile = "env_config_template.cue" + prefix := constant.KBPrefix + "_" - prefix := constant.KBPrefix + "_" + strings.ToUpper(params.Component.Type) + "_" - svcName := strings.Join([]string{params.Cluster.Name, params.Component.Name, "headless"}, "-") - envData := map[string]string{} - envData[prefix+"N"] = strconv.Itoa(int(params.Component.Replicas)) - for j := 0; j < int(params.Component.Replicas); j++ { - hostNameTplKey := prefix + strconv.Itoa(j) + "_HOSTNAME" - hostNameTplValue := params.Cluster.Name + "-" + params.Component.Name + "-" + strconv.Itoa(j) + svcName := strings.Join([]string{cluster.Name, component.Name, "headless"}, "-") + cnt := strconv.Itoa(int(component.Replicas)) + suffixes := make([]string, 0, 4+component.Replicas) + envData := map[string]string{ + prefix + "REPLICA_COUNT": cnt, + } - if params.Component.WorkloadType != appsv1alpha1.Replication { - envData[hostNameTplKey] = fmt.Sprintf("%s.%s", hostNameTplValue, svcName) - continue - } + for j := 0; j < int(component.Replicas); j++ { + toA := strconv.Itoa(j) + suffix := toA + "_HOSTNAME" + value := fmt.Sprintf("%s.%s", cluster.Name+"-"+component.Name+"-"+toA, svcName) + envData[prefix+suffix] = value + suffixes = append(suffixes, suffix) // build env for replication workload - // the 1st replica's hostname should not have suffix like '-0' - if j == 0 { - envData[hostNameTplKey] = fmt.Sprintf("%s.%s", hostNameTplValue, svcName) - } else { - envData[hostNameTplKey] = fmt.Sprintf("%s.%s", hostNameTplValue+"-0", svcName) - } - // if primaryIndex is 0, the pod name have to be no suffix '-0' - primaryIndex := params.Component.GetPrimaryIndex() - if primaryIndex == 0 { - envData[constant.KBReplicationSetPrimaryPodName] = fmt.Sprintf("%s-%s-%d.%s", params.Cluster.Name, params.Component.Name, primaryIndex, svcName) - } else { - envData[constant.KBReplicationSetPrimaryPodName] = fmt.Sprintf("%s-%s-%d-%d.%s", params.Cluster.Name, params.Component.Name, primaryIndex, 0, svcName) + if component.WorkloadType == appsv1alpha1.Replication { + envData[constant.KBReplicationSetPrimaryPodName] = + fmt.Sprintf("%s-%s-%d.%s", cluster.Name, component.Name, component.GetPrimaryIndex(), svcName) } } // TODO following code seems to be redundant with updateConsensusRoleInfo in consensus_set_utils.go // build consensus env from cluster.status - if params.Cluster.Status.Components != nil { - if v, ok := params.Cluster.Status.Components[params.Component.Name]; ok { - consensusSetStatus := v.ConsensusSetStatus - if consensusSetStatus != nil { - if consensusSetStatus.Leader.Pod != componentutil.ComponentStatusDefaultPodName { - envData[prefix+"LEADER"] = consensusSetStatus.Leader.Pod - } - - followers := "" - for _, follower := range consensusSetStatus.Followers { - if follower.Pod == componentutil.ComponentStatusDefaultPodName { - continue - } - if len(followers) > 0 { - followers += "," - } - followers += follower.Pod - } - envData[prefix+"FOLLOWERS"] = followers + if v, ok := cluster.Status.Components[component.Name]; ok && v.ConsensusSetStatus != nil { + consensusSetStatus := v.ConsensusSetStatus + if consensusSetStatus.Leader.Pod != constant.ComponentStatusDefaultPodName { + envData[prefix+"LEADER"] = consensusSetStatus.Leader.Pod + suffixes = append(suffixes, "LEADER") + } + followers := make([]string, 0, len(consensusSetStatus.Followers)) + for _, follower := range consensusSetStatus.Followers { + if follower.Pod == constant.ComponentStatusDefaultPodName { + continue } + followers = append(followers, follower.Pod) } + envData[prefix+"FOLLOWERS"] = strings.Join(followers, ",") + suffixes = append(suffixes, "FOLLOWERS") } - // if created from existing pvc, set env - isRecreate, err := isRecreateFromExistingPVC() - if err != nil { - return nil, err + // set cluster uid to let pod know if the cluster is recreated + envData[prefix+"CLUSTER_UID"] = string(cluster.UID) + suffixes = append(suffixes, "CLUSTER_UID") + + // have backward compatible handling for CM key with 'compDefName' being part of the key name + // TODO: need to deprecate 'compDefName' being part of variable name, as it's redundant + // and introduce env/cm key naming reference complexity + prefixWithCompDefName := prefix + strings.ToUpper(component.CompDefName) + "_" + for _, s := range suffixes { + envData[prefixWithCompDefName+s] = envData[prefix+s] } - envData[prefix+"RECREATE"] = strconv.FormatBool(isRecreate) + envData[prefixWithCompDefName+"N"] = envData[prefix+"REPLICA_COUNT"] config := corev1.ConfigMap{} if err := buildFromCUE(tplFile, map[string]any{ - "cluster": params.Cluster, - "component": params.Component, + "cluster": cluster, + "component": component, "config.data": envData, }, "config", &config); err != nil { return nil, err } - return &config, nil } -func BuildBackupPolicy(sts *appsv1.StatefulSet, - template *dataprotectionv1alpha1.BackupPolicyTemplate, - backupKey types.NamespacedName) (*dataprotectionv1alpha1.BackupPolicy, error) { - backupKey.Name = backupKey.Name + "-" + randomString(6) - backupPolicy := dataprotectionv1alpha1.BackupPolicy{} - if err := buildFromCUE("backup_policy_template.cue", map[string]any{ - "sts": sts, - "backup_key": backupKey, - "template": template.Name, - }, "backup_policy", &backupPolicy); err != nil { - return nil, err - } - - return &backupPolicy, nil -} - func BuildBackup(sts *appsv1.StatefulSet, backupPolicyName string, backupKey types.NamespacedName) (*dataprotectionv1alpha1.Backup, error) { @@ -562,7 +551,6 @@ func BuildBackup(sts *appsv1.StatefulSet, }, "backup_job", &backup); err != nil { return nil, err } - return &backup, nil } @@ -577,16 +565,13 @@ func BuildVolumeSnapshot(snapshotKey types.NamespacedName, }, "snapshot", &snapshot); err != nil { return nil, err } - return &snapshot, nil } func BuildCronJob(pvcKey types.NamespacedName, schedule string, sts *appsv1.StatefulSet) (*batchv1.CronJob, error) { - serviceAccount := viper.GetString("KUBEBLOCKS_SERVICEACCOUNT_NAME") - cronJob := batchv1.CronJob{} if err := buildFromCUE("delete_pvc_cron_job_template.cue", map[string]any{ "pvc": pvcKey, @@ -596,16 +581,23 @@ func BuildCronJob(pvcKey types.NamespacedName, }, "cronjob", &cronJob); err != nil { return nil, err } - return &cronJob, nil } -func BuildConfigMapWithTemplate( - configs map[string]string, +func BuildConfigMapWithTemplate(configs map[string]string, params BuilderParams, cmName string, configConstraintName string, tplCfg appsv1alpha1.ComponentTemplateSpec) (*corev1.ConfigMap, error) { + return BuildConfigMapWithTemplateLow(params.Cluster, params.Component, configs, cmName, configConstraintName, tplCfg) +} + +func BuildConfigMapWithTemplateLow(cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, + configs map[string]string, + cmName string, + configConstraintName string, + tplCfg appsv1alpha1.ComponentTemplateSpec) (*corev1.ConfigMap, error) { const tplFile = "config_template.cue" cueFS, _ := debme.FS(cueTemplates, "cue") cueTpl, err := getCacheCUETplValue(tplFile, func() (*intctrlutil.CUETpl, error) { @@ -619,16 +611,16 @@ func BuildConfigMapWithTemplate( // prepare cue data configMeta := map[string]map[string]string{ "clusterDefinition": { - "name": params.ClusterDefinition.GetName(), + "name": cluster.Spec.ClusterDefRef, }, "cluster": { - "name": params.Cluster.GetName(), - "namespace": params.Cluster.GetNamespace(), + "name": cluster.GetName(), + "namespace": cluster.GetNamespace(), }, "component": { - "name": params.Component.Name, - "type": params.Component.Type, - "characterType": params.Component.CharacterType, + "name": component.Name, + "compDefName": component.CompDefName, + "characterType": component.CharacterType, "configName": cmName, "templateName": tplCfg.TemplateRef, "configConstraintsName": configConstraintName, @@ -640,7 +632,7 @@ func BuildConfigMapWithTemplate( return nil, err } - // Generate config files context by render cue template + // Generate config files context by rendering cue template if err = cueValue.Fill("meta", configBytes); err != nil { return nil, err } @@ -693,7 +685,6 @@ func BuildCfgManagerContainer(sidecarRenderedParam *cfgcm.CfgManagerBuildParams) func BuildTLSSecret(namespace, clusterName, componentName string) (*corev1.Secret, error) { const tplFile = "tls_certs_secret_template.cue" - secret := &corev1.Secret{} pathedName := componentPathedName{ Namespace: namespace, @@ -708,7 +699,6 @@ func BuildTLSSecret(namespace, clusterName, componentName string) (*corev1.Secre func BuildBackupManifestsJob(key types.NamespacedName, backup *dataprotectionv1alpha1.Backup, podSpec *corev1.PodSpec) (*batchv1.Job, error) { const tplFile = "backup_manifests_template.cue" - job := &batchv1.Job{} if err := buildFromCUE(tplFile, map[string]any{ @@ -722,3 +712,27 @@ func BuildBackupManifestsJob(key types.NamespacedName, backup *dataprotectionv1a } return job, nil } + +func BuildRestoreJob(name, namespace string, image string, command []string, args []string, + volumes []corev1.Volume, volumeMounts []corev1.VolumeMount, env []corev1.EnvVar, resources *corev1.ResourceRequirements) (*batchv1.Job, error) { + const tplFile = "restore_job_template.cue" + job := &batchv1.Job{} + fillMaps := map[string]any{ + "job.metadata.name": name, + "job.metadata.namespace": namespace, + "job.spec.template.spec.volumes": volumes, + "container.image": image, + "container.command": command, + "container.args": args, + "container.volumeMounts": volumeMounts, + "container.env": env, + } + if resources != nil { + fillMaps["container.resources"] = *resources + } + + if err := buildFromCUE(tplFile, fillMaps, "job", job); err != nil { + return nil, err + } + return job, nil +} diff --git a/internal/controller/builder/builder_base.go b/internal/controller/builder/builder_base.go new file mode 100644 index 000000000..95e12fdc6 --- /dev/null +++ b/internal/controller/builder/builder_base.go @@ -0,0 +1,122 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + "reflect" + + appsv1 "k8s.io/api/apps/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + intctrlutil "github.com/apecloud/kubeblocks/internal/generics" +) + +// TODO: a copy of testutil.apps.base_factory, should make this as a common util both used by builder and testing +// Manipulate common attributes here to save boilerplate code + +type BaseBuilder[T intctrlutil.Object, PT intctrlutil.PObject[T], B any] struct { + object PT + concreteBuilder *B +} + +func (builder *BaseBuilder[T, PT, B]) init(namespace, name string, obj PT, b *B) { + obj.SetNamespace(namespace) + obj.SetName(name) + if obj.GetLabels() == nil { + obj.SetLabels(map[string]string{}) + } + if obj.GetAnnotations() == nil { + obj.SetAnnotations(map[string]string{}) + } + builder.object = obj + builder.concreteBuilder = b +} + +func (builder *BaseBuilder[T, PT, B]) get() PT { + return builder.object +} + +func (builder *BaseBuilder[T, PT, B]) SetName(name string) *B { + builder.object.SetName(name) + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) AddLabels(keysAndValues ...string) *B { + builder.AddLabelsInMap(WithMap(keysAndValues...)) + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) AddLabelsInMap(labels map[string]string) *B { + l := builder.object.GetLabels() + for k, v := range labels { + l[k] = v + } + builder.object.SetLabels(l) + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) AddAnnotations(keysAndValues ...string) *B { + builder.AddAnnotationsInMap(WithMap(keysAndValues...)) + return builder.concreteBuilder +} +func (builder *BaseBuilder[T, PT, B]) AddAnnotationsInMap(annotations map[string]string) *B { + a := builder.object.GetAnnotations() + for k, v := range annotations { + a[k] = v + } + builder.object.SetAnnotations(a) + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) AddControllerRevisionHashLabel(value string) *B { + return builder.AddLabels(appsv1.ControllerRevisionHashLabelKey, value) +} + +func (builder *BaseBuilder[T, PT, B]) SetOwnerReferences(ownerAPIVersion string, ownerKind string, owner client.Object) *B { + // interface object needs to determine whether the value is nil. + // otherwise, nil pointer error may be reported. + if owner != nil && !reflect.ValueOf(owner).IsNil() { + t := true + builder.object.SetOwnerReferences([]metav1.OwnerReference{ + {APIVersion: ownerAPIVersion, Kind: ownerKind, Controller: &t, + BlockOwnerDeletion: &t, Name: owner.GetName(), UID: owner.GetUID()}, + }) + } + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) AddFinalizers(finalizers []string) *B { + builder.object.SetFinalizers(finalizers) + return builder.concreteBuilder +} + +func (builder *BaseBuilder[T, PT, B]) GetObject() PT { + return builder.object +} + +func WithMap(keysAndValues ...string) map[string]string { + // ignore mismatching for kvs + m := make(map[string]string, len(keysAndValues)/2) + for i := 0; i+1 < len(keysAndValues); i += 2 { + m[keysAndValues[i]] = keysAndValues[i+1] + } + return m +} diff --git a/internal/controller/builder/builder_base_test.go b/internal/controller/builder/builder_base_test.go new file mode 100644 index 000000000..26a6a42ba --- /dev/null +++ b/internal/controller/builder/builder_base_test.go @@ -0,0 +1,78 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + appsv1 "k8s.io/api/apps/v1" +) + +var _ = Describe("base builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + labelKey1, labelValue1 = "foo-1", "bar-1" + labelKey2, labelValue2 = "foo-2", "bar-2" + labelKey3, labelValue3 = "foo-3", "bar-3" + annotationKey1, annotationValue1 = "foo-1", "bar-1" + annotationKey2, annotationValue2 = "foo-2", "bar-2" + annotationKey3, annotationValue3 = "foo-3", "bar-3" + ) + labels := map[string]string{labelKey3: labelValue3} + annotations := map[string]string{annotationKey3: annotationValue3} + controllerRevision := "wer-23e23-sedfwe--34r23" + finalizer := "foo-bar" + owner := NewConsensusSetBuilder(ns, name).GetObject() + owner.UID = "sdfwsedqw-swed-sdswe" + ownerAPIVersion := "workloads.kubeblocks.io/v1alpha1" + ownerKind := "ConsensusSet" + obj := NewConfigMapBuilder(ns, name). + AddLabels(labelKey1, labelValue1, labelKey2, labelValue2). + AddLabelsInMap(labels). + AddAnnotations(annotationKey1, annotationValue1, annotationKey2, annotationValue2). + AddAnnotationsInMap(annotations). + AddControllerRevisionHashLabel(controllerRevision). + AddFinalizers([]string{finalizer}). + SetOwnerReferences(ownerAPIVersion, ownerKind, owner). + GetObject() + + Expect(obj.Name).Should(Equal(name)) + Expect(obj.Namespace).Should(Equal(ns)) + Expect(len(obj.Labels)).Should(Equal(4)) + Expect(obj.Labels[labelKey1]).Should(Equal(labelValue1)) + Expect(obj.Labels[labelKey2]).Should(Equal(labelValue2)) + Expect(obj.Labels[labelKey3]).Should(Equal(labelValue3)) + Expect(obj.Labels[appsv1.ControllerRevisionHashLabelKey]).Should(Equal(controllerRevision)) + Expect(len(obj.Annotations)).Should(Equal(3)) + Expect(obj.Annotations[annotationKey1]).Should(Equal(annotationValue1)) + Expect(obj.Annotations[annotationKey2]).Should(Equal(annotationValue2)) + Expect(obj.Annotations[annotationKey3]).Should(Equal(annotationValue3)) + Expect(len(obj.Finalizers)).Should(Equal(1)) + Expect(obj.Finalizers[0]).Should(Equal(finalizer)) + Expect(len(obj.OwnerReferences)).Should(Equal(1)) + Expect(obj.OwnerReferences[0].APIVersion).Should(Equal(ownerAPIVersion)) + Expect(obj.OwnerReferences[0].Kind).Should(Equal(ownerKind)) + Expect(obj.OwnerReferences[0].Name).Should(Equal(owner.Name)) + Expect(obj.OwnerReferences[0].UID).Should(Equal(owner.UID)) + }) +}) diff --git a/internal/controller/builder/builder_configmap.go b/internal/controller/builder/builder_configmap.go new file mode 100644 index 000000000..d58c61723 --- /dev/null +++ b/internal/controller/builder/builder_configmap.go @@ -0,0 +1,65 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import corev1 "k8s.io/api/core/v1" + +type ConfigMapBuilder struct { + BaseBuilder[corev1.ConfigMap, *corev1.ConfigMap, ConfigMapBuilder] +} + +func NewConfigMapBuilder(namespace, name string) *ConfigMapBuilder { + builder := &ConfigMapBuilder{} + builder.init(namespace, name, &corev1.ConfigMap{}, builder) + return builder +} + +func (builder *ConfigMapBuilder) SetImmutable(immutable bool) *ConfigMapBuilder { + builder.get().Immutable = &immutable + return builder +} + +func (builder *ConfigMapBuilder) PutData(key, value string) *ConfigMapBuilder { + data := builder.get().Data + if data == nil { + data = make(map[string]string, 1) + } + data[key] = value + return builder +} + +func (builder *ConfigMapBuilder) SetData(data map[string]string) *ConfigMapBuilder { + builder.get().Data = data + return builder +} + +func (builder *ConfigMapBuilder) PutBinaryData(key string, value []byte) *ConfigMapBuilder { + data := builder.get().BinaryData + if data == nil { + data = make(map[string][]byte, 1) + } + data[key] = value + return builder +} + +func (builder *ConfigMapBuilder) SetBinaryData(binaryData map[string][]byte) *ConfigMapBuilder { + builder.get().BinaryData = binaryData + return builder +} diff --git a/internal/controller/builder/builder_configmap_test.go b/internal/controller/builder/builder_configmap_test.go new file mode 100644 index 000000000..0c4bb223e --- /dev/null +++ b/internal/controller/builder/builder_configmap_test.go @@ -0,0 +1,56 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("configmap builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + ) + cm := NewConfigMapBuilder(ns, name). + SetData(map[string]string{ + "foo-1": "bar-1", + }). + PutData("foo-2", "bar-2"). + SetBinaryData(map[string][]byte{ + "foo-3": []byte("bar-3"), + }). + PutBinaryData("foo-4", []byte("bar-4")). + SetImmutable(true). + GetObject() + + Expect(cm.Name).Should(Equal(name)) + Expect(cm.Namespace).Should(Equal(ns)) + Expect(cm.Data).ShouldNot(BeNil()) + Expect(cm.Data["foo-1"]).Should(Equal("bar-1")) + Expect(cm.Data["foo-2"]).Should(Equal("bar-2")) + Expect(cm.BinaryData).ShouldNot(BeNil()) + Expect(cm.BinaryData["foo-3"]).Should(Equal([]byte("bar-3"))) + Expect(cm.BinaryData["foo-4"]).Should(Equal([]byte("bar-4"))) + Expect(cm.Immutable).ShouldNot(BeNil()) + Expect(*cm.Immutable).Should(BeTrue()) + }) +}) diff --git a/internal/controller/builder/builder_consensus_set.go b/internal/controller/builder/builder_consensus_set.go new file mode 100644 index 000000000..cb73be60f --- /dev/null +++ b/internal/controller/builder/builder_consensus_set.go @@ -0,0 +1,82 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + corev1 "k8s.io/api/core/v1" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" +) + +type ConsensusSetBuilder struct { + BaseBuilder[workloads.ConsensusSet, *workloads.ConsensusSet, ConsensusSetBuilder] +} + +func NewConsensusSetBuilder(namespace, name string) *ConsensusSetBuilder { + builder := &ConsensusSetBuilder{} + builder.init(namespace, name, + &workloads.ConsensusSet{ + Spec: workloads.ConsensusSetSpec{ + Replicas: 1, + Roles: []workloads.ConsensusRole{ + { + Name: "leader", + AccessMode: workloads.ReadWriteMode, + IsLeader: true, + CanVote: true, + }, + }, + UpdateStrategy: workloads.SerialUpdateStrategy, + }, + }, builder) + return builder +} + +func (builder *ConsensusSetBuilder) SetReplicas(replicas int32) *ConsensusSetBuilder { + builder.get().Spec.Replicas = replicas + return builder +} + +func (builder *ConsensusSetBuilder) SetRoles(roles []workloads.ConsensusRole) *ConsensusSetBuilder { + builder.get().Spec.Roles = roles + return builder +} + +func (builder *ConsensusSetBuilder) SetTemplate(template corev1.PodTemplateSpec) *ConsensusSetBuilder { + builder.get().Spec.Template = template + return builder +} + +func (builder *ConsensusSetBuilder) SetObservationActions(actions []workloads.Action) *ConsensusSetBuilder { + builder.get().Spec.RoleObservation.ObservationActions = actions + return builder +} + +func (builder *ConsensusSetBuilder) AddObservationAction(action workloads.Action) *ConsensusSetBuilder { + actions := builder.get().Spec.RoleObservation.ObservationActions + actions = append(actions, action) + builder.get().Spec.RoleObservation.ObservationActions = actions + return builder +} + +func (builder *ConsensusSetBuilder) SetService(service corev1.ServiceSpec) *ConsensusSetBuilder { + builder.get().Spec.Service = service + return builder +} diff --git a/internal/controller/builder/builder_consensus_set_test.go b/internal/controller/builder/builder_consensus_set_test.go new file mode 100644 index 000000000..bd3ec9d2f --- /dev/null +++ b/internal/controller/builder/builder_consensus_set_test.go @@ -0,0 +1,100 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" +) + +var _ = Describe("consensus_set builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + replicas = int32(5) + port = int32(12345) + ) + role := workloads.ConsensusRole{ + Name: "foo", + AccessMode: workloads.ReadWriteMode, + IsLeader: true, + CanVote: true, + } + pod := NewPodBuilder(ns, "foo"). + AddContainer(corev1.Container{ + Name: "foo", + Image: "bar", + Ports: []corev1.ContainerPort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + }).GetObject() + template := corev1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + Spec: pod.Spec, + } + actions := []workloads.Action{ + { + Image: "foo-1", + Command: []string{"bar-1"}, + }, + } + action := workloads.Action{ + Image: "foo-2", + Command: []string{"bar-2"}, + } + service := corev1.ServiceSpec{ + Ports: []corev1.ServicePort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + Port: port, + }, + }, + } + csSet := NewConsensusSetBuilder(ns, name). + SetReplicas(replicas). + SetRoles([]workloads.ConsensusRole{role}). + SetTemplate(template). + SetObservationActions(actions). + AddObservationAction(action). + SetService(service). + GetObject() + + Expect(csSet.Name).Should(Equal(name)) + Expect(csSet.Namespace).Should(Equal(ns)) + Expect(csSet.Spec.Replicas).Should(Equal(replicas)) + Expect(len(csSet.Spec.Roles)).Should(Equal(1)) + Expect(csSet.Spec.Roles[0]).Should(Equal(role)) + Expect(csSet.Spec.Template).Should(Equal(template)) + Expect(len(csSet.Spec.RoleObservation.ObservationActions)).Should(Equal(2)) + Expect(csSet.Spec.RoleObservation.ObservationActions[0]).Should(Equal(actions[0])) + Expect(csSet.Spec.RoleObservation.ObservationActions[1]).Should(Equal(action)) + Expect(csSet.Spec.Service).Should(Equal(service)) + }) +}) diff --git a/internal/controller/builder/builder_job.go b/internal/controller/builder/builder_job.go new file mode 100644 index 000000000..8c8d93dd1 --- /dev/null +++ b/internal/controller/builder/builder_job.go @@ -0,0 +1,58 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type JobBuilder struct { + BaseBuilder[batchv1.Job, *batchv1.Job, JobBuilder] +} + +func NewJobBuilder(namespace, name string) *JobBuilder { + builder := &JobBuilder{} + builder.init(namespace, name, &batchv1.Job{}, builder) + return builder +} + +func (builder *JobBuilder) SetPodTemplateSpec(template corev1.PodTemplateSpec) *JobBuilder { + builder.get().Spec.Template = template + return builder +} + +func (builder *JobBuilder) AddSelector(key, value string) *JobBuilder { + selector := builder.get().Spec.Selector + if selector == nil { + selector = &metav1.LabelSelector{ + MatchLabels: map[string]string{}, + } + } + selector.MatchLabels[key] = value + builder.get().Spec.Selector = selector + return builder +} + +func (builder *JobBuilder) SetSuspend(suspend bool) *JobBuilder { + builder.get().Spec.Suspend = &suspend + return builder +} diff --git a/internal/controller/builder/builder_job_test.go b/internal/controller/builder/builder_job_test.go new file mode 100644 index 000000000..fcc48fd42 --- /dev/null +++ b/internal/controller/builder/builder_job_test.go @@ -0,0 +1,69 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("job builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + port = int32(12345) + ) + pod := NewPodBuilder(ns, "foo"). + AddContainer(corev1.Container{ + Name: "foo", + Image: "bar", + Ports: []corev1.ContainerPort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + }).GetObject() + template := corev1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + Spec: pod.Spec, + } + selectorKey, selectorValue := "foo", "bar" + suspend := true + job := NewJobBuilder(ns, name). + SetPodTemplateSpec(template). + AddSelector(selectorKey, selectorValue). + SetSuspend(suspend). + GetObject() + + Expect(job.Name).Should(Equal(name)) + Expect(job.Namespace).Should(Equal(ns)) + Expect(job.Spec.Template).Should(Equal(template)) + Expect(job.Spec.Selector).ShouldNot(BeNil()) + Expect(job.Spec.Selector.MatchLabels).ShouldNot(BeNil()) + Expect(job.Spec.Selector.MatchLabels[selectorKey]).Should(Equal(selectorValue)) + Expect(job.Spec.Suspend).ShouldNot(BeNil()) + Expect(*job.Spec.Suspend).Should(Equal(suspend)) + }) +}) diff --git a/internal/controller/builder/builder_pod.go b/internal/controller/builder/builder_pod.go new file mode 100644 index 000000000..04b32f31c --- /dev/null +++ b/internal/controller/builder/builder_pod.go @@ -0,0 +1,44 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import corev1 "k8s.io/api/core/v1" + +type PodBuilder struct { + BaseBuilder[corev1.Pod, *corev1.Pod, PodBuilder] +} + +func NewPodBuilder(namespace, name string) *PodBuilder { + builder := &PodBuilder{} + builder.init(namespace, name, &corev1.Pod{}, builder) + return builder +} + +func (builder *PodBuilder) SetContainers(containers []corev1.Container) *PodBuilder { + builder.get().Spec.Containers = containers + return builder +} + +func (builder *PodBuilder) AddContainer(container corev1.Container) *PodBuilder { + containers := builder.get().Spec.Containers + containers = append(containers, container) + builder.get().Spec.Containers = containers + return builder +} diff --git a/internal/controller/builder/builder_pod_test.go b/internal/controller/builder/builder_pod_test.go new file mode 100644 index 000000000..9fa8c7d00 --- /dev/null +++ b/internal/controller/builder/builder_pod_test.go @@ -0,0 +1,69 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("pod builder", func() { + It("should work well", func() { + name := "foo" + ns := "default" + port := int32(12345) + container := corev1.Container{ + Name: "foo-1", + Image: "bar-2", + Ports: []corev1.ContainerPort{ + { + Name: "foo-1", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + } + containers := []corev1.Container{ + { + Name: "foo-2", + Image: "bar-2", + Ports: []corev1.ContainerPort{ + { + Name: "foo-2", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + }, + } + pod := NewPodBuilder(ns, name). + SetContainers(containers). + AddContainer(container). + GetObject() + + Expect(pod.Name).Should(Equal(name)) + Expect(pod.Namespace).Should(Equal(ns)) + Expect(len(pod.Spec.Containers)).Should(Equal(2)) + Expect(pod.Spec.Containers[0]).Should(Equal(containers[0])) + Expect(pod.Spec.Containers[1]).Should(Equal(container)) + }) +}) diff --git a/internal/controller/builder/builder_service.go b/internal/controller/builder/builder_service.go new file mode 100644 index 000000000..af4f4ea82 --- /dev/null +++ b/internal/controller/builder/builder_service.go @@ -0,0 +1,103 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +type ServiceBuilder struct { + BaseBuilder[corev1.Service, *corev1.Service, ServiceBuilder] +} + +func NewServiceBuilder(namespace, name string) *ServiceBuilder { + builder := &ServiceBuilder{} + builder.init(namespace, name, &corev1.Service{}, builder) + return builder +} + +func NewHeadlessServiceBuilder(namespace, name string) *ServiceBuilder { + builder := &ServiceBuilder{} + builder.init(namespace, name, &corev1.Service{}, builder) + builder.SetType(corev1.ServiceTypeClusterIP) + builder.get().Spec.ClusterIP = corev1.ClusterIPNone + return builder +} + +func (builder *ServiceBuilder) AddSelector(key, value string) *ServiceBuilder { + keyValues := make(map[string]string, 1) + keyValues[key] = value + return builder.AddSelectorsInMap(keyValues) +} + +func (builder *ServiceBuilder) AddSelectors(keyValues ...string) *ServiceBuilder { + return builder.AddSelectorsInMap(WithMap(keyValues...)) +} + +func (builder *ServiceBuilder) AddSelectorsInMap(keyValues map[string]string) *ServiceBuilder { + selectors := builder.get().Spec.Selector + if selectors == nil { + selectors = make(map[string]string, 0) + } + for k, v := range keyValues { + selectors[k] = v + } + builder.get().Spec.Selector = selectors + return builder +} + +func (builder *ServiceBuilder) AddPorts(ports ...corev1.ServicePort) *ServiceBuilder { + portList := builder.get().Spec.Ports + if portList == nil { + portList = make([]corev1.ServicePort, 0) + } + portList = append(portList, ports...) + builder.get().Spec.Ports = portList + return builder +} + +func (builder *ServiceBuilder) AddContainerPorts(ports ...corev1.ContainerPort) *ServiceBuilder { + servicePorts := make([]corev1.ServicePort, 0) + for _, containerPort := range ports { + servicePort := corev1.ServicePort{ + Name: containerPort.Name, + Protocol: containerPort.Protocol, + Port: containerPort.ContainerPort, + TargetPort: intstr.FromString(containerPort.Name), + } + servicePorts = append(servicePorts, servicePort) + } + return builder.AddPorts(servicePorts...) +} + +func (builder *ServiceBuilder) SetType(serviceType corev1.ServiceType) *ServiceBuilder { + if serviceType == "" { + return builder + } + builder.get().Spec.Type = serviceType + if serviceType == corev1.ServiceTypeLoadBalancer { + // Set externalTrafficPolicy to Local has two benefits: + // 1. preserve client IP + // 2. improve network performance by reducing one hop + builder.get().Spec.ExternalTrafficPolicy = corev1.ServiceExternalTrafficPolicyTypeLocal + } + return builder +} diff --git a/internal/controller/builder/builder_service_test.go b/internal/controller/builder/builder_service_test.go new file mode 100644 index 000000000..02da21d02 --- /dev/null +++ b/internal/controller/builder/builder_service_test.go @@ -0,0 +1,92 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" +) + +var _ = Describe("service builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + selectorKey1, selectorValue1 = "foo-1", "bar-1" + selectorKey2, selectorValue2 = "foo-2", "bar-2" + selectorKey3, selectorValue3 = "foo-3", "bar-3" + selectorKey4, selectorValue4 = "foo-4", "bar-4" + port = int32(12345) + ) + selectors := map[string]string{selectorKey4: selectorValue4} + ports := []corev1.ServicePort{ + { + Name: "foo-1", + Protocol: corev1.ProtocolTCP, + Port: port, + }, + } + containerPorts := []corev1.ContainerPort{ + { + Name: "foo-2", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + } + serviceType := corev1.ServiceTypeLoadBalancer + svc := NewHeadlessServiceBuilder(ns, name). + AddSelector(selectorKey1, selectorValue1). + AddSelectors(selectorKey2, selectorValue2, selectorKey3, selectorValue3). + AddSelectorsInMap(selectors). + AddPorts(ports...). + AddContainerPorts(containerPorts...). + SetType(serviceType). + GetObject() + + Expect(svc.Name).Should(Equal(name)) + Expect(svc.Namespace).Should(Equal(ns)) + Expect(svc.Spec.Selector).ShouldNot(BeNil()) + Expect(len(svc.Spec.Selector)).Should(Equal(4)) + Expect(svc.Spec.Selector[selectorKey1]).Should(Equal(selectorValue1)) + Expect(svc.Spec.Selector[selectorKey2]).Should(Equal(selectorValue2)) + Expect(svc.Spec.Selector[selectorKey3]).Should(Equal(selectorValue3)) + Expect(svc.Spec.Selector[selectorKey4]).Should(Equal(selectorValue4)) + Expect(svc.Spec.Ports).ShouldNot(BeNil()) + Expect(len(svc.Spec.Ports)).Should(Equal(2)) + Expect(svc.Spec.Ports[0]).Should(Equal(ports[0])) + Expect(svc.Spec.Type).Should(Equal(serviceType)) + Expect(svc.Spec.ExternalTrafficPolicy).Should(Equal(corev1.ServiceExternalTrafficPolicyTypeLocal)) + hasPort := func(containerPort corev1.ContainerPort, servicePorts []corev1.ServicePort) bool { + for _, servicePort := range servicePorts { + if containerPort.Protocol == servicePort.Protocol && + intstr.FromString(containerPort.Name) == servicePort.TargetPort { + return true + } + } + return false + } + for _, containerPort := range containerPorts { + Expect(hasPort(containerPort, svc.Spec.Ports)).Should(BeTrue()) + } + }) +}) diff --git a/internal/controller/builder/builder_stateful_set.go b/internal/controller/builder/builder_stateful_set.go new file mode 100644 index 000000000..5453bace6 --- /dev/null +++ b/internal/controller/builder/builder_stateful_set.go @@ -0,0 +1,105 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type StatefulSetBuilder struct { + BaseBuilder[apps.StatefulSet, *apps.StatefulSet, StatefulSetBuilder] +} + +func NewStatefulSetBuilder(namespace, name string) *StatefulSetBuilder { + builder := &StatefulSetBuilder{} + builder.init(namespace, name, &apps.StatefulSet{}, builder) + return builder +} + +func (builder *StatefulSetBuilder) AddMatchLabel(key, value string) *StatefulSetBuilder { + labels := make(map[string]string, 1) + labels[key] = value + return builder.AddMatchLabelsInMap(labels) +} + +func (builder *StatefulSetBuilder) AddMatchLabels(keyValues ...string) *StatefulSetBuilder { + return builder.AddMatchLabelsInMap(WithMap(keyValues...)) +} + +func (builder *StatefulSetBuilder) AddMatchLabelsInMap(labels map[string]string) *StatefulSetBuilder { + selector := builder.get().Spec.Selector + if selector == nil { + selector = &metav1.LabelSelector{} + builder.get().Spec.Selector = selector + } + matchLabels := builder.get().Spec.Selector.MatchLabels + if matchLabels == nil { + matchLabels = make(map[string]string, len(labels)) + } + for k, v := range labels { + matchLabels[k] = v + } + builder.get().Spec.Selector.MatchLabels = matchLabels + return builder +} + +func (builder *StatefulSetBuilder) SetServiceName(serviceName string) *StatefulSetBuilder { + builder.get().Spec.ServiceName = serviceName + return builder +} + +func (builder *StatefulSetBuilder) SetReplicas(replicas int32) *StatefulSetBuilder { + builder.get().Spec.Replicas = &replicas + return builder +} + +func (builder *StatefulSetBuilder) SetMinReadySeconds(minReadySeconds int32) *StatefulSetBuilder { + builder.get().Spec.MinReadySeconds = minReadySeconds + return builder +} + +func (builder *StatefulSetBuilder) SetPodManagementPolicy(policy apps.PodManagementPolicyType) *StatefulSetBuilder { + builder.get().Spec.PodManagementPolicy = policy + return builder +} + +func (builder *StatefulSetBuilder) SetTemplate(template corev1.PodTemplateSpec) *StatefulSetBuilder { + builder.get().Spec.Template = template + return builder +} + +func (builder *StatefulSetBuilder) AddVolumeClaimTemplates(templates ...corev1.PersistentVolumeClaim) *StatefulSetBuilder { + templateList := builder.get().Spec.VolumeClaimTemplates + templateList = append(templateList, templates...) + builder.get().Spec.VolumeClaimTemplates = templateList + return builder +} + +func (builder *StatefulSetBuilder) SetVolumeClaimTemplates(templates ...corev1.PersistentVolumeClaim) *StatefulSetBuilder { + builder.get().Spec.VolumeClaimTemplates = templates + return builder +} + +func (builder *StatefulSetBuilder) SetUpdateStrategyType(strategyType apps.StatefulSetUpdateStrategyType) *StatefulSetBuilder { + builder.get().Spec.UpdateStrategy.Type = strategyType + return builder +} diff --git a/internal/controller/builder/builder_stateful_set_test.go b/internal/controller/builder/builder_stateful_set_test.go new file mode 100644 index 000000000..4b1f11a26 --- /dev/null +++ b/internal/controller/builder/builder_stateful_set_test.go @@ -0,0 +1,128 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package builder + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +var _ = Describe("stateful_set builder", func() { + It("should work well", func() { + const ( + name = "foo" + ns = "default" + selectorKey1, selectorValue1 = "foo-1", "bar-1" + selectorKey2, selectorValue2 = "foo-2", "bar-2" + selectorKey3, selectorValue3 = "foo-3", "bar-3" + selectorKey4, selectorValue4 = "foo-4", "bar-4" + port = int32(12345) + serviceName = "foo" + replicas = int32(5) + minReadySeconds = int32(37) + policy = apps.OrderedReadyPodManagement + ) + selectors := map[string]string{selectorKey4: selectorValue4} + pod := NewPodBuilder(ns, "foo"). + AddContainer(corev1.Container{ + Name: "foo", + Image: "bar", + Ports: []corev1.ContainerPort{ + { + Name: "foo", + Protocol: corev1.ProtocolTCP, + ContainerPort: port, + }, + }, + }).GetObject() + template := corev1.PodTemplateSpec{ + ObjectMeta: pod.ObjectMeta, + Spec: pod.Spec, + } + vcs := []corev1.PersistentVolumeClaim{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-1", + Namespace: ns, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "foo-1", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + }, + }, + }, + }, + } + vc := corev1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo-2", + Namespace: ns, + }, + Spec: corev1.PersistentVolumeClaimSpec{ + VolumeName: "foo-2", + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("600m"), + }, + }, + }, + } + strategyType := apps.OnDeleteStatefulSetStrategyType + sts := NewStatefulSetBuilder(ns, name). + AddMatchLabel(selectorKey1, selectorValue1). + AddMatchLabels(selectorKey2, selectorValue2, selectorKey3, selectorValue3). + AddMatchLabelsInMap(selectors). + SetServiceName(serviceName). + SetReplicas(replicas). + SetMinReadySeconds(minReadySeconds). + SetPodManagementPolicy(policy). + SetTemplate(template). + SetVolumeClaimTemplates(vcs...). + AddVolumeClaimTemplates(vc). + SetUpdateStrategyType(strategyType). + GetObject() + + Expect(sts.Name).Should(Equal(name)) + Expect(sts.Namespace).Should(Equal(ns)) + Expect(sts.Spec.Selector).ShouldNot(BeNil()) + Expect(len(sts.Spec.Selector.MatchLabels)).Should(Equal(4)) + Expect(sts.Spec.Selector.MatchLabels[selectorKey1]).Should(Equal(selectorValue1)) + Expect(sts.Spec.Selector.MatchLabels[selectorKey2]).Should(Equal(selectorValue2)) + Expect(sts.Spec.Selector.MatchLabels[selectorKey3]).Should(Equal(selectorValue3)) + Expect(sts.Spec.Selector.MatchLabels[selectorKey4]).Should(Equal(selectorValue4)) + Expect(sts.Spec.ServiceName).Should(Equal(serviceName)) + Expect(sts.Spec.Replicas).ShouldNot(BeNil()) + Expect(*sts.Spec.Replicas).Should(Equal(replicas)) + Expect(sts.Spec.PodManagementPolicy).Should(Equal(policy)) + Expect(sts.Spec.Template).Should(Equal(template)) + Expect(len(sts.Spec.VolumeClaimTemplates)).Should(Equal(2)) + Expect(sts.Spec.VolumeClaimTemplates[0]).Should(Equal(vcs[0])) + Expect(sts.Spec.VolumeClaimTemplates[1]).Should(Equal(vc)) + Expect(sts.Spec.UpdateStrategy.Type).Should(Equal(strategyType)) + + }) +}) diff --git a/internal/controller/builder/builder_test.go b/internal/controller/builder/builder_test.go index fc1830815..976e9c5c5 100644 --- a/internal/controller/builder/builder_test.go +++ b/internal/controller/builder/builder_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package builder @@ -19,7 +22,6 @@ package builder import ( "fmt" "strconv" - "strings" "testing" . "github.com/onsi/ginkgo/v2" @@ -34,7 +36,6 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" cfgcm "github.com/apecloud/kubeblocks/internal/configuration/config_manager" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/component" @@ -60,17 +61,20 @@ var _ = Describe("builder", func() { const clusterDefName = "test-clusterdef" const clusterVersionName = "test-clusterversion" const clusterName = "test-cluster" - - const mysqlCompType = "replicasets" + const mysqlCompDefName = "replicasets" const mysqlCompName = "mysql" - - const nginxCompType = "proxy" + const proxyCompDefName = "proxy" + var requiredKeys = []string{ + "KB_REPLICA_COUNT", + "KB_0_HOSTNAME", + "KB_CLUSTER_UID", + } allFieldsClusterDefObj := func(needCreate bool) *appsv1alpha1.ClusterDefinition { By("By assure an clusterDefinition obj") clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). + AddComponentDef(testapps.StatelessNginxComponent, proxyCompDefName). GetObject() if needCreate { Expect(testCtx.CreateObj(testCtx.Ctx, clusterDefObj)).Should(Succeed()) @@ -81,9 +85,9 @@ var _ = Describe("builder", func() { allFieldsClusterVersionObj := func(needCreate bool) *appsv1alpha1.ClusterVersion { By("By assure an clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - AddComponent(nginxCompType). + AddComponentVersion(proxyCompDefName). AddInitContainerShort("nginx-init", testapps.NginxImage). AddContainerShort("nginx", testapps.NginxImage). GetObject() @@ -98,18 +102,17 @@ var _ = Describe("builder", func() { clusterVersionObj *appsv1alpha1.ClusterVersion, needCreate bool, ) (*appsv1alpha1.Cluster, *appsv1alpha1.ClusterDefinition, *appsv1alpha1.ClusterVersion, types.NamespacedName) { - // setup Cluster obj required default ClusterDefinition and ClusterVersion objects if not provided + // setup Cluster obj requires default ClusterDefinition and ClusterVersion objects if clusterDefObj == nil { clusterDefObj = allFieldsClusterDefObj(needCreate) } if clusterVersionObj == nil { clusterVersionObj = allFieldsClusterVersionObj(needCreate) } - pvcSpec := testapps.NewPVCSpec("1Gi") clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType).SetReplicas(1). + AddComponent(mysqlCompName, mysqlCompDefName).SetReplicas(1). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). AddService(testapps.ServiceVPCName, corev1.ServiceTypeLoadBalancer). AddService(testapps.ServiceInternetName, corev1.ServiceTypeLoadBalancer). @@ -118,7 +121,6 @@ var _ = Describe("builder", func() { if needCreate { Expect(testCtx.CreateObj(testCtx.Ctx, clusterObj)).Should(Succeed()) } - return clusterObj, clusterDefObj, clusterVersionObj, key } @@ -152,13 +154,14 @@ var _ = Describe("builder", func() { cluster, clusterDef, clusterVersion, _ := newAllFieldsClusterObj(clusterDef, clusterVersion, false) reqCtx := newReqCtx() By("assign every available fields") - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) + Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) return component } @@ -184,16 +187,6 @@ var _ = Describe("builder", func() { return ¶ms } - newBackupPolicyTemplate := func() *dataprotectionv1alpha1.BackupPolicyTemplate { - return testapps.NewBackupPolicyTemplateFactory("backup-policy-template-mysql"). - SetBackupToolName("mysql-xtrabackup"). - SetSchedule("0 2 * * *"). - SetTTL("168h0m0s"). - AddHookPreCommand("touch /data/mysql/.restore;sync"). - AddHookPostCommand("rm -f /data/mysql/.restore;sync"). - Create(&testCtx).GetObject() - } - Context("has helper function which builds specific object from cue template", func() { It("builds PVC correctly", func() { snapshotName := "test-snapshot-name" @@ -213,7 +206,7 @@ var _ = Describe("builder", func() { It("builds Service correctly", func() { params := newParams() - svcList, err := BuildSvcList(*params) + svcList, err := BuildSvcListWithCustomAttributes(params.Cluster, params.Component, nil) Expect(err).Should(BeNil()) Expect(svcList).ShouldNot(BeEmpty()) }) @@ -250,12 +243,15 @@ var _ = Describe("builder", func() { "UUID_B64", "UUID_STR_B64", "UUID_HEX", + "HEADLESS_SVC_FQDN", } { Expect(credential.StringData[v]).ShouldNot(BeEquivalentTo(fmt.Sprintf("$(%s)", v))) } Expect(credential.StringData["RANDOM_PASSWD"]).Should(HaveLen(8)) svcFQDN := fmt.Sprintf("%s-%s.%s.svc", params.Cluster.Name, params.Component.Name, params.Cluster.Namespace) + headlessSvcFQDN := fmt.Sprintf("%s-%s-headless.%s.svc", params.Cluster.Name, params.Component.Name, + params.Cluster.Namespace) var mysqlPort corev1.ServicePort var paxosPort corev1.ServicePort for _, s := range params.Component.Services[0].Spec.Ports { @@ -267,6 +263,7 @@ var _ = Describe("builder", func() { } } Expect(credential.StringData["SVC_FQDN"]).Should(Equal(svcFQDN)) + Expect(credential.StringData["HEADLESS_SVC_FQDN"]).Should(Equal(headlessSvcFQDN)) Expect(credential.StringData["tcpEndpoint"]).Should(Equal(fmt.Sprintf("tcp:%s:%d", svcFQDN, mysqlPort.Port))) Expect(credential.StringData["paxosEndpoint"]).Should(Equal(fmt.Sprintf("paxos:%s:%d", svcFQDN, paxosPort.Port))) @@ -277,7 +274,6 @@ var _ = Describe("builder", func() { params := newParams() envConfigName := "test-env-config-name" newParams := params - sts, err := BuildSts(reqCtx, *params, envConfigName) Expect(err).Should(BeNil()) Expect(sts).ShouldNot(BeNil()) @@ -299,7 +295,7 @@ var _ = Describe("builder", func() { sts, err = BuildSts(reqCtx, *newParams, envConfigName) Expect(err).Should(BeNil()) Expect(sts).ShouldNot(BeNil()) - Expect(*sts.Spec.Replicas).Should(Equal(int32(1))) + Expect(*sts.Spec.Replicas).Should(BeEquivalentTo(2)) }) It("builds Deploy correctly", func() { @@ -323,22 +319,22 @@ var _ = Describe("builder", func() { cfg, err := BuildEnvConfig(*params, reqCtx, k8sClient) Expect(err).Should(BeNil()) Expect(cfg).ShouldNot(BeNil()) - Expect(len(cfg.Data) == 3).Should(BeTrue()) + for _, k := range requiredKeys { + _, ok := cfg.Data[k] + Expect(ok).Should(BeTrue()) + } }) It("builds env config with resources recreate", func() { reqCtx := newReqCtx() params := newParams() - - By("creating pvc to make it looks like recreation") - pvcName := "test-pvc" - testapps.NewPersistentVolumeClaimFactory(testCtx.DefaultNamespace, pvcName, clusterName, - params.Component.Name, testapps.DataVolumeName).SetStorage("1Gi").CheckedCreate(&testCtx) - + uuid := "12345" + By("mock a cluster uuid") + params.Cluster.UID = types.UID(uuid) cfg, err := BuildEnvConfig(*params, reqCtx, k8sClient) Expect(err).Should(BeNil()) Expect(cfg).ShouldNot(BeNil()) - Expect(cfg.Data["KB_"+strings.ToUpper(params.Component.Type)+"_RECREATE"]).Should(Equal("true")) + Expect(cfg.Data["KB_CLUSTER_UID"]).Should(Equal(uuid)) }) It("builds Env Config with ConsensusSet status correctly", func() { @@ -360,42 +356,50 @@ var _ = Describe("builder", func() { cfg, err := BuildEnvConfig(*params, reqCtx, k8sClient) Expect(err).Should(BeNil()) Expect(cfg).ShouldNot(BeNil()) - Expect(len(cfg.Data) == 5).Should(BeTrue()) + toCheckKeys := append(requiredKeys, []string{ + "KB_LEADER", + "KB_FOLLOWERS", + }...) + for _, k := range toCheckKeys { + _, ok := cfg.Data[k] + Expect(ok).Should(BeTrue()) + } }) It("builds Env Config with Replication component correctly", func() { + var cfg *corev1.ConfigMap + var err error + reqCtx := newReqCtx() params := newParams() params.Component.WorkloadType = appsv1alpha1.Replication - var cfg *corev1.ConfigMap - var err error - checkEnvValues := func() { cfg, err = BuildEnvConfig(*params, reqCtx, k8sClient) Expect(err).Should(BeNil()) Expect(cfg).ShouldNot(BeNil()) - Expect(len(cfg.Data) == int(3+params.Component.Replicas)).Should(BeTrue()) - Expect(cfg.Data["KB_"+strings.ToUpper(params.Component.Type)+"_N"]). + toCheckKeys := append(requiredKeys, []string{ + "KB_PRIMARY_POD_NAME", + }...) + for _, k := range toCheckKeys { + _, ok := cfg.Data[k] + Expect(ok).Should(BeTrue()) + } + Expect(cfg.Data["KB_REPLICA_COUNT"]). Should(Equal(strconv.Itoa(int(params.Component.Replicas)))) stsName := fmt.Sprintf("%s-%s", params.Cluster.Name, params.Component.Name) svcName := fmt.Sprintf("%s-headless", stsName) By("Checking KB_PRIMARY_POD_NAME value be right") - if int(params.Component.GetPrimaryIndex()) == 0 { - Expect(cfg.Data["KB_PRIMARY_POD_NAME"]). - Should(Equal(stsName + "-" + strconv.Itoa(int(params.Component.GetPrimaryIndex())) + "." + svcName)) - } else { - Expect(cfg.Data["KB_PRIMARY_POD_NAME"]). - Should(Equal(stsName + "-" + strconv.Itoa(int(params.Component.GetPrimaryIndex())) + "-0." + svcName)) - } + Expect(cfg.Data["KB_PRIMARY_POD_NAME"]). + Should(Equal(stsName + "-" + strconv.Itoa(int(params.Component.GetPrimaryIndex())) + "." + svcName)) for i := 0; i < int(params.Component.Replicas); i++ { if i == 0 { By("Checking the 1st replica's hostname should not have suffix '-0'") - Expect(cfg.Data["KB_"+strings.ToUpper(params.Component.Type)+"_"+strconv.Itoa(i)+"_HOSTNAME"]). + Expect(cfg.Data["KB_"+strconv.Itoa(i)+"_HOSTNAME"]). Should(Equal(stsName + "-" + strconv.Itoa(0) + "." + svcName)) } else { - Expect(cfg.Data["KB_"+strings.ToUpper(params.Component.Type)+"_"+strconv.Itoa(i)+"_HOSTNAME"]). - Should(Equal(stsName + "-" + strconv.Itoa(int(params.Component.GetPrimaryIndex())) + "-0." + svcName)) + Expect(cfg.Data["KB_"+strconv.Itoa(i)+"_HOSTNAME"]). + Should(Equal(stsName + "-" + strconv.Itoa(int(params.Component.GetPrimaryIndex())) + "." + svcName)) } } } @@ -412,18 +416,6 @@ var _ = Describe("builder", func() { checkEnvValues() }) - It("builds BackupPolicy correctly", func() { - sts := newStsObj() - backupPolicyTemplate := newBackupPolicyTemplate() - backupKey := types.NamespacedName{ - Namespace: "default", - Name: "test-backup", - } - policy, err := BuildBackupPolicy(sts, backupPolicyTemplate, backupKey) - Expect(err).Should(BeNil()) - Expect(policy).ShouldNot(BeNil()) - }) - It("builds BackupJob correctly", func() { sts := newStsObj() backupJobKey := types.NamespacedName{ diff --git a/internal/controller/builder/cue/backup_job_template.cue b/internal/controller/builder/cue/backup_job_template.cue index 9caaafb45..5369a660c 100644 --- a/internal/controller/builder/cue/backup_job_template.cue +++ b/internal/controller/builder/cue/backup_job_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . sts: { metadata: { diff --git a/internal/controller/builder/cue/backup_manifests_template.cue b/internal/controller/builder/cue/backup_manifests_template.cue index 852bed36f..cd41196c0 100644 --- a/internal/controller/builder/cue/backup_manifests_template.cue +++ b/internal/controller/builder/cue/backup_manifests_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . backup: { metadata: { diff --git a/internal/controller/builder/cue/backup_policy_template.cue b/internal/controller/builder/cue/backup_policy_template.cue deleted file mode 100644 index 89d01f75c..000000000 --- a/internal/controller/builder/cue/backup_policy_template.cue +++ /dev/null @@ -1,70 +0,0 @@ -// Copyright ApeCloud, Inc. -// -// 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. - -sts: { - metadata: { - labels: { - "app.kubernetes.io/instance": string - } - namespace: string - } -} -backup_key: { - Name: string - Namespace: string -} -template: string -backup_policy: { - apiVersion: "dataprotection.kubeblocks.io/v1alpha1" - kind: "BackupPolicy" - metadata: { - name: backup_key.Name - // generateName: "\(backup_key.Name)-" - namespace: backup_key.Namespace - labels: { - "apps.kubeblocks.io/managed-by": "cluster" - for k, v in sts.metadata.labels { - "\(k)": "\(v)" - } - } - } - spec: { - "backupPolicyTemplateName": template - "target": { - "labelsSelector": { - "matchLabels": { - "app.kubernetes.io/instance": sts.metadata.labels["app.kubernetes.io/instance"] - "apps.kubeblocks.io/component-name": sts.metadata.labels["apps.kubeblocks.io/component-name"] - } - } - "secret": { - "name": "wesql-cluster" - } - } - "remoteVolume": { - "name": "backup-remote-volume" - "persistentVolumeClaim": { - "claimName": "backup-s3-pvc" - } - } - "hooks": { - "preCommands": [ - "touch /data/mysql/data/.restore; sync", - ] - "postCommands": [ - ] - } - "onFailAttempted": 3 - } -} diff --git a/internal/controller/builder/cue/config_manager_sidecar.cue b/internal/controller/builder/cue/config_manager_sidecar.cue index 3c05392aa..949b79a34 100644 --- a/internal/controller/builder/cue/config_manager_sidecar.cue +++ b/internal/controller/builder/cue/config_manager_sidecar.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . template: { name: parameter.name diff --git a/internal/controller/builder/cue/config_template.cue b/internal/controller/builder/cue/config_template.cue index 22c88c9df..f07dad738 100644 --- a/internal/controller/builder/cue/config_template.cue +++ b/internal/controller/builder/cue/config_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . meta: { clusterDefinition: { @@ -28,6 +31,7 @@ meta: { templateName: string configConstraintsName: string configTemplateName: string + compDefName: string } } @@ -41,6 +45,7 @@ config: { "app.kubernetes.io/name": "\(meta.clusterDefinition.name)" "app.kubernetes.io/instance": meta.cluster.name "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(meta.component.compDefName)" "apps.kubeblocks.io/component-name": "\(meta.component.name)" // configmap selector for ConfigureController diff --git a/internal/controller/builder/cue/conn_credential_template.cue b/internal/controller/builder/cue/conn_credential_template.cue index 6f8508711..237012a49 100644 --- a/internal/controller/builder/cue/conn_credential_template.cue +++ b/internal/controller/builder/cue/conn_credential_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . clusterdefinition: { metadata: { @@ -39,7 +42,7 @@ secret: { "app.kubernetes.io/instance": cluster.metadata.name "app.kubernetes.io/managed-by": "kubeblocks" if clusterdefinition.spec.type != _|_ { - "apps.kubeblocks.io/cluster-type": clusterdefinition.spec.type + "apps.kubeblocks.io/cluster-type": clusterdefinition.spec.type } } } diff --git a/internal/controller/builder/cue/delete_pvc_cron_job_template.cue b/internal/controller/builder/cue/delete_pvc_cron_job_template.cue index 10311fbe8..b0f325680 100644 --- a/internal/controller/builder/cue/delete_pvc_cron_job_template.cue +++ b/internal/controller/builder/cue/delete_pvc_cron_job_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . pvc: { Name: string @@ -35,6 +38,9 @@ cronjob: { spec: { schedule: string jobTemplate: { + metadata: { + labels: sts.metadata.labels + } spec: { template: { spec: { diff --git a/internal/controller/builder/cue/deployment_template.cue b/internal/controller/builder/cue/deployment_template.cue index 4804a1fbc..e1c99d297 100644 --- a/internal/controller/builder/cue/deployment_template.cue +++ b/internal/controller/builder/cue/deployment_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -24,7 +27,7 @@ cluster: { component: { clusterDefName: string name: string - type: string + compDefName: string workloadType: string replicas: int podSpec: containers: [...] @@ -41,6 +44,7 @@ deployment: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": cluster.metadata.name "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" "apps.kubeblocks.io/component-name": "\(component.name)" } @@ -63,7 +67,7 @@ deployment: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": "\(cluster.metadata.name)" "app.kubernetes.io/managed-by": "kubeblocks" - "app.kubernetes.io/component": "\(component.type)" + "app.kubernetes.io/component": "\(component.compDefName)" if cluster.spec.clusterVersionRef != _|_ { "app.kubernetes.io/version": "\(cluster.spec.clusterVersionRef)" } diff --git a/internal/controller/builder/cue/env_config_template.cue b/internal/controller/builder/cue/env_config_template.cue index 4219f84a6..f2babc484 100644 --- a/internal/controller/builder/cue/env_config_template.cue +++ b/internal/controller/builder/cue/env_config_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -21,21 +24,26 @@ cluster: { component: { name: string clusterDefName: string + compDefName: string } config: { apiVersion: "v1" kind: "ConfigMap" metadata: { + // this naming pattern has been referenced elsewhere, complete code scan is + // required if this naming pattern is going be changed. name: "\(cluster.metadata.name)-\(component.name)-env" namespace: cluster.metadata.namespace labels: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": cluster.metadata.name "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" + // configmap selector for env update "apps.kubeblocks.io/config-type": "kubeblocks-env" - "apps.kubeblocks.io/component-name": component.name + "apps.kubeblocks.io/component-name": "\(component.name)" } } data: [string]: string diff --git a/internal/controller/builder/cue/headless_service_template.cue b/internal/controller/builder/cue/headless_service_template.cue index 29ff0078e..e81066a2b 100644 --- a/internal/controller/builder/cue/headless_service_template.cue +++ b/internal/controller/builder/cue/headless_service_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -20,9 +23,11 @@ cluster: { } component: { clusterDefName: string + compDefName: string name: string monitor: { enable: bool + builtIn: bool scrapePort: int scrapePath: string } @@ -39,15 +44,25 @@ service: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": cluster.metadata.name "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" "apps.kubeblocks.io/component-name": "\(component.name)" } annotations: { - "prometheus.io/scrape": "\(component.monitor.enable)" - if component.monitor.enable == true { - "prometheus.io/path": component.monitor.scrapePath - "prometheus.io/port": "\(component.monitor.scrapePort)" - "prometheus.io/scheme": "http" + if component.monitor.enable == false { + "prometheus.io/scrape": "false" + "apps.kubeblocks.io/monitor": "false" + } + if component.monitor.enable == true && component.monitor.builtIn == false { + "prometheus.io/scrape": "true" + "prometheus.io/path": component.monitor.scrapePath + "prometheus.io/port": "\(component.monitor.scrapePort)" + "prometheus.io/scheme": "http" + "apps.kubeblocks.io/monitor": "false" + } + if component.monitor.enable == true && component.monitor.builtIn == true { + "prometheus.io/scrape": "false" + "apps.kubeblocks.io/monitor": "true" } } } diff --git a/internal/controller/builder/cue/pdb_template.cue b/internal/controller/builder/cue/pdb_template.cue index e658838f1..1c89fa6f3 100644 --- a/internal/controller/builder/cue/pdb_template.cue +++ b/internal/controller/builder/cue/pdb_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -20,11 +23,9 @@ cluster: { } component: { clusterDefName: string + compDefName: string name: string - // FIXME not defined in apis - maxUnavailable: string - podSpec: containers: [...] - volumeClaimTemplates: [...] + minAvailable: string | int } pdb: { @@ -37,13 +38,14 @@ pdb: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": cluster.metadata.name "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" "apps.kubeblocks.io/component-name": "\(component.name)" } } "spec": { - if component.maxUnavailable != _|_ { - maxUnavailable: component.maxUnavailable + if component.minAvailable != _|_ { + minAvailable: component.minAvailable } selector: { matchLabels: { diff --git a/internal/controller/builder/cue/pvc_template.cue b/internal/controller/builder/cue/pvc_template.cue index 848dde657..7dee66ea2 100644 --- a/internal/controller/builder/cue/pvc_template.cue +++ b/internal/controller/builder/cue/pvc_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . sts: { metadata: { @@ -38,7 +41,7 @@ pvc: { name: pvc_key.Name namespace: pvc_key.Namespace labels: { - "vct.kubeblocks.io/name": volumeClaimTemplate.metadata.name + "apps.kubeblocks.io/vct-name": volumeClaimTemplate.metadata.name for k, v in sts.metadata.labels { "\(k)": "\(v)" } @@ -47,10 +50,12 @@ pvc: { spec: { accessModes: volumeClaimTemplate.spec.accessModes resources: volumeClaimTemplate.spec.resources - dataSource: { - "name": snapshot_name - "kind": "VolumeSnapshot" - "apiGroup": "snapshot.storage.k8s.io" + if len(snapshot_name) > 0 { + dataSource: { + "name": snapshot_name + "kind": "VolumeSnapshot" + "apiGroup": "snapshot.storage.k8s.io" + } } } } diff --git a/internal/controller/builder/cue/restore_job_template.cue b/internal/controller/builder/cue/restore_job_template.cue new file mode 100644 index 000000000..8c27d3207 --- /dev/null +++ b/internal/controller/builder/cue/restore_job_template.cue @@ -0,0 +1,50 @@ +// Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +// This file is part of KubeBlocks project +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +container: { + name: "restore" + image: string + imagePullPolicy: "IfNotPresent" + command: [...] + args: [...] + volumeMounts: [...] + env: [...] + resources: {} +} + +job: { + apiVersion: "batch/v1" + kind: "Job" + metadata: { + name: string + namespace: string + labels: { + "app.kubernetes.io/managed-by": "kubeblocks" + } + } + spec: { + template: { + spec: { + containers: [container] + volumes: [...] + restartPolicy: "OnFailure" + securityContext: + runAsUser: 0 + } + } + } +} diff --git a/internal/controller/builder/cue/service_template.cue b/internal/controller/builder/cue/service_template.cue index 530f3a9f1..1359763b1 100644 --- a/internal/controller/builder/cue/service_template.cue +++ b/internal/controller/builder/cue/service_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -21,6 +24,7 @@ cluster: { component: { clusterDefName: string + compDefName: string name: string } @@ -47,17 +51,20 @@ svc: { name: "\(cluster.metadata.name)-\(component.name)" } labels: { - "app.kubernetes.io/name": "\(component.clusterDefName)" - "app.kubernetes.io/instance": cluster.metadata.name - "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/name": "\(component.clusterDefName)" + "app.kubernetes.io/instance": cluster.metadata.name + "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" + "apps.kubeblocks.io/component-name": "\(component.name)" } annotations: service.metadata.annotations } "spec": { "selector": { - "app.kubernetes.io/instance": "\(cluster.metadata.name)" - "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/instance": "\(cluster.metadata.name)" + "app.kubernetes.io/managed-by": "kubeblocks" + "apps.kubeblocks.io/component-name": "\(component.name)" } ports: service.spec.ports diff --git a/internal/controller/builder/cue/snapshot_template.cue b/internal/controller/builder/cue/snapshot_template.cue index 729e73d36..270a01bf9 100644 --- a/internal/controller/builder/cue/snapshot_template.cue +++ b/internal/controller/builder/cue/snapshot_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . snapshot_key: { Name: string diff --git a/internal/controller/builder/cue/statefulset_template.cue b/internal/controller/builder/cue/statefulset_template.cue index d69eb5d4d..826e566a0 100644 --- a/internal/controller/builder/cue/statefulset_template.cue +++ b/internal/controller/builder/cue/statefulset_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . cluster: { metadata: { @@ -23,7 +26,7 @@ cluster: { } component: { clusterDefName: string - type: string + compDefName: string name: string workloadType: string replicas: int @@ -38,47 +41,42 @@ statefulset: { namespace: cluster.metadata.namespace name: "\(cluster.metadata.name)-\(component.name)" labels: { - "app.kubernetes.io/name": "\(component.clusterDefName)" - "app.kubernetes.io/instance": cluster.metadata.name - "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/name": "\(component.clusterDefName)" + "app.kubernetes.io/instance": cluster.metadata.name + "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/component": "\(component.compDefName)" + "apps.kubeblocks.io/component-name": "\(component.name)" } } spec: { selector: matchLabels: { - "app.kubernetes.io/name": "\(component.clusterDefName)" - "app.kubernetes.io/instance": "\(cluster.metadata.name)" - "app.kubernetes.io/managed-by": "kubeblocks" + "app.kubernetes.io/name": "\(component.clusterDefName)" + "app.kubernetes.io/instance": "\(cluster.metadata.name)" + "app.kubernetes.io/managed-by": "kubeblocks" + "apps.kubeblocks.io/component-name": "\(component.name)" } serviceName: "\(cluster.metadata.name)-\(component.name)-headless" - if component.workloadType != "Replication" { - replicas: component.replicas - } - if component.workloadType == "Replication" { - replicas: 1 - } - minReadySeconds: 10 - podManagementPolicy: "Parallel" + replicas: component.replicas template: { metadata: { labels: { "app.kubernetes.io/name": "\(component.clusterDefName)" "app.kubernetes.io/instance": "\(cluster.metadata.name)" "app.kubernetes.io/managed-by": "kubeblocks" - "app.kubernetes.io/component": "\(component.type)" + "app.kubernetes.io/component": "\(component.compDefName)" if cluster.spec.clusterVersionRef != _|_ { "app.kubernetes.io/version": "\(cluster.spec.clusterVersionRef)" } + "apps.kubeblocks.io/component-name": "\(component.name)" "apps.kubeblocks.io/workload-type": "\(component.workloadType)" } } spec: component.podSpec } - if component.workloadType != "Replication" { - volumeClaimTemplates: component.volumeClaimTemplates - } + volumeClaimTemplates: component.volumeClaimTemplates } } diff --git a/internal/controller/builder/cue/tls_certs_secret_template.cue b/internal/controller/builder/cue/tls_certs_secret_template.cue index 5faf8f4fc..e5a4df682 100644 --- a/internal/controller/builder/cue/tls_certs_secret_template.cue +++ b/internal/controller/builder/cue/tls_certs_secret_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +// Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +// This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . pathedName: { namespace: string diff --git a/internal/controller/builder/suite_test.go b/internal/controller/builder/suite_test.go index e212b36d0..1cfaa18e4 100644 --- a/internal/controller/builder/suite_test.go +++ b/internal/controller/builder/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package builder diff --git a/internal/controller/client/readonly_client.go b/internal/controller/client/readonly_client.go index cba7c2b94..9a282f404 100644 --- a/internal/controller/client/readonly_client.go +++ b/internal/controller/client/readonly_client.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package client diff --git a/internal/controller/component/affinity_utils.go b/internal/controller/component/affinity_utils.go index 8b232f4ee..b8d96848d 100644 --- a/internal/controller/component/affinity_utils.go +++ b/internal/controller/component/affinity_utils.go @@ -1,29 +1,34 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component import ( + "encoding/json" "strings" + "github.com/spf13/viper" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - intctrlutil "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/constant" ) func buildPodTopologySpreadConstraints( @@ -50,8 +55,8 @@ func buildPodTopologySpreadConstraints( TopologyKey: topologyKey, LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - intctrlutil.AppInstanceLabelKey: cluster.Name, - intctrlutil.KBAppComponentLabelKey: component.Name, + constant.AppInstanceLabelKey: cluster.Name, + constant.KBAppComponentLabelKey: component.Name, }, }, }) @@ -63,6 +68,23 @@ func buildPodAffinity( cluster *appsv1alpha1.Cluster, clusterOrCompAffinity *appsv1alpha1.Affinity, component *SynthesizedComponent, +) (*corev1.Affinity, error) { + affinity := buildNewAffinity(cluster, clusterOrCompAffinity, component) + + // read data plane affinity from config and merge it + dpAffinity := new(corev1.Affinity) + if val := viper.GetString(constant.CfgKeyDataPlaneAffinity); val != "" { + if err := json.Unmarshal([]byte(val), &dpAffinity); err != nil { + return nil, err + } + } + return mergeAffinity(affinity, dpAffinity) +} + +func buildNewAffinity( + cluster *appsv1alpha1.Cluster, + clusterOrCompAffinity *appsv1alpha1.Affinity, + component *SynthesizedComponent, ) *corev1.Affinity { if clusterOrCompAffinity == nil { return nil @@ -96,8 +118,8 @@ func buildPodAffinity( TopologyKey: topologyKey, LabelSelector: &metav1.LabelSelector{ MatchLabels: map[string]string{ - intctrlutil.AppInstanceLabelKey: cluster.Name, - intctrlutil.KBAppComponentLabelKey: component.Name, + constant.AppInstanceLabelKey: cluster.Name, + constant.KBAppComponentLabelKey: component.Name, }, }, }) @@ -122,7 +144,7 @@ func buildPodAffinity( if clusterOrCompAffinity.Tenancy == appsv1alpha1.DedicatedNode { var labelSelectorReqs []metav1.LabelSelectorRequirement labelSelectorReqs = append(labelSelectorReqs, metav1.LabelSelectorRequirement{ - Key: intctrlutil.WorkloadTypeLabelKey, + Key: constant.WorkloadTypeLabelKey, Operator: metav1.LabelSelectorOpIn, Values: appsv1alpha1.WorkloadTypes, }) @@ -138,42 +160,86 @@ func buildPodAffinity( return affinity } -// patchBuiltInAffinity patches built-in affinity configuration -func patchBuiltInAffinity(affinity *corev1.Affinity) *corev1.Affinity { - var matchExpressions []corev1.NodeSelectorRequirement - matchExpressions = append(matchExpressions, corev1.NodeSelectorRequirement{ - Key: intctrlutil.KubeBlocksDataNodeLabelKey, - Operator: corev1.NodeSelectorOpIn, - Values: []string{intctrlutil.KubeBlocksDataNodeLabelValue}, - }) - preferredSchedulingTerm := corev1.PreferredSchedulingTerm{ - Preference: corev1.NodeSelectorTerm{ - MatchExpressions: matchExpressions, - }, - Weight: 100, +// mergeAffinity merges affinity from src to dest +func mergeAffinity(dest, src *corev1.Affinity) (*corev1.Affinity, error) { + if src == nil { + return dest, nil } - if affinity != nil && affinity.NodeAffinity != nil { - affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution = append( - affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution, preferredSchedulingTerm) - } else { - if affinity == nil { - affinity = new(corev1.Affinity) + + if dest == nil { + return src.DeepCopy(), nil + } + + rst := dest.DeepCopy() + skipPodAffinity := src.PodAffinity == nil + skipPodAntiAffinity := src.PodAntiAffinity == nil + skipNodeAffinity := src.NodeAffinity == nil + + if rst.PodAffinity == nil && !skipPodAffinity { + rst.PodAffinity = src.PodAffinity + skipPodAffinity = true + } + if rst.PodAntiAffinity == nil && !skipPodAntiAffinity { + rst.PodAntiAffinity = src.PodAntiAffinity + skipPodAntiAffinity = true + } + if rst.NodeAffinity == nil && !skipNodeAffinity { + rst.NodeAffinity = src.NodeAffinity + skipNodeAffinity = true + } + + // if not skip, both are not nil + if !skipPodAffinity { + rst.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution = append( + rst.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution, + src.PodAffinity.PreferredDuringSchedulingIgnoredDuringExecution...) + + rst.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution = append( + rst.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution, + src.PodAffinity.RequiredDuringSchedulingIgnoredDuringExecution...) + } + if !skipPodAntiAffinity { + rst.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution = append( + rst.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution, + src.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution...) + + rst.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution = append( + rst.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution, + src.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution...) + } + if !skipNodeAffinity { + rst.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution = append( + rst.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution, + src.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution...) + + skip := src.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil + if rst.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution == nil && !skip { + rst.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution = src.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution + skip = true } - affinity.NodeAffinity = &corev1.NodeAffinity{ - PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{preferredSchedulingTerm}, + if !skip { + rst.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms = append( + rst.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms, + src.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms...) } } - - return affinity + return rst, nil } -// PatchBuiltInToleration patches built-in tolerations configuration -func PatchBuiltInToleration(tolerations []corev1.Toleration) []corev1.Toleration { - tolerations = append(tolerations, corev1.Toleration{ - Key: intctrlutil.KubeBlocksDataNodeTolerationKey, - Operator: corev1.TolerationOpEqual, - Value: intctrlutil.KubeBlocksDataNodeTolerationValue, - Effect: corev1.TaintEffectNoSchedule, - }) - return tolerations +// BuildTolerations builds tolerations from config +func BuildTolerations(cluster *appsv1alpha1.Cluster, clusterCompSpec *appsv1alpha1.ClusterComponentSpec) ([]corev1.Toleration, error) { + tolerations := cluster.Spec.Tolerations + if clusterCompSpec != nil && len(clusterCompSpec.Tolerations) != 0 { + tolerations = clusterCompSpec.Tolerations + } + + // build data plane tolerations from config + var dpTolerations []corev1.Toleration + if val := viper.GetString(constant.CfgKeyDataPlaneTolerations); val != "" { + if err := json.Unmarshal([]byte(val), &dpTolerations); err != nil { + return nil, err + } + } + + return append(tolerations, dpTolerations...), nil } diff --git a/internal/controller/component/affinity_utils_test.go b/internal/controller/component/affinity_utils_test.go index 3cbe08b53..c261ea6c0 100644 --- a/internal/controller/component/affinity_utils_test.go +++ b/internal/controller/component/affinity_utils_test.go @@ -1,25 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component import ( + "fmt" + . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "github.com/spf13/viper" corev1 "k8s.io/api/core/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -33,66 +39,95 @@ var _ = Describe("affinity utils", func() { clusterDefName = "test-clusterdef" clusterVersionName = "test-clusterversion" clusterName = "test-cluster" - - mysqlCompType = "replicasets" - mysqlCompName = "mysql" + mysqlCompDefName = "replicasets" + mysqlCompName = "mysql" + + clusterTolerationKey = "testClusterTolerationKey" + topologyKey = "testTopologyKey" + labelKey = "testNodeLabelKey" + labelValue = "testLabelValue" + nodeKey = "testNodeKey" ) var ( clusterObj *appsv1alpha1.Cluster component *SynthesizedComponent - ) - Context("with PodAntiAffinity set to Required", func() { - const topologyKey = "testTopologyKey" - const lableKey = "testNodeLabelKey" - const labelValue = "testLabelValue" - - BeforeEach(func() { + buildObjs = func(podAntiAffinity appsv1alpha1.PodAntiAffinity) { clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). GetObject() clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). GetObject() affinity := &appsv1alpha1.Affinity{ - PodAntiAffinity: appsv1alpha1.Required, + PodAntiAffinity: podAntiAffinity, TopologyKeys: []string{topologyKey}, NodeLabels: map[string]string{ - lableKey: labelValue, + labelKey: labelValue, }, } + + toleration := corev1.Toleration{ + Key: clusterTolerationKey, + Operator: corev1.TolerationOpExists, + Effect: corev1.TaintEffectNoExecute, + } + clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetClusterAffinity(affinity). + AddClusterToleration(toleration). GetObject() reqCtx := intctrlutil.RequestCtx{ Ctx: ctx, Log: tlog, } - component = BuildComponent( + component, _ = BuildComponent( reqCtx, *clusterObj, *clusterDefObj, clusterDefObj.Spec.ComponentDefs[0], clusterObj.Spec.ComponentSpecs[0], - &clusterVersionObj.Spec.ComponentVersions[0]) + &clusterVersionObj.Spec.ComponentVersions[0], + ) + } + ) + + Context("with PodAntiAffinity set to Required", func() { + BeforeEach(func() { + buildObjs(appsv1alpha1.Required) Expect(component).ShouldNot(BeNil()) }) It("should have correct Affinity and TopologySpreadConstraints", func() { - affinity := buildPodAffinity(clusterObj, clusterObj.Spec.Affinity, component) - Expect(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(lableKey)) + affinity, err := buildPodAffinity(clusterObj, clusterObj.Spec.Affinity, component) + Expect(err).Should(Succeed()) + Expect(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(labelKey)) Expect(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).Should(Equal(topologyKey)) Expect(affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution).Should(BeEmpty()) + Expect(affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution).Should(BeEmpty()) + + topologySpreadConstraints := buildPodTopologySpreadConstraints(clusterObj, clusterObj.Spec.Affinity, component) + Expect(topologySpreadConstraints[0].WhenUnsatisfiable).Should(Equal(corev1.DoNotSchedule)) + Expect(topologySpreadConstraints[0].TopologyKey).Should(Equal(topologyKey)) + }) - affinity = patchBuiltInAffinity(affinity) - Expect(affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Preference.MatchExpressions[0].Key).Should( - Equal(constant.KubeBlocksDataNodeLabelKey)) + It("when data plane affinity is set, should have correct Affinity and TopologySpreadConstraints", func() { + viper.Set(constant.CfgKeyDataPlaneAffinity, + fmt.Sprintf("{\"nodeAffinity\":{\"preferredDuringSchedulingIgnoredDuringExecution\":[{\"preference\":{\"matchExpressions\":[{\"key\":\"%s\",\"operator\":\"In\",\"values\":[\"true\"]}]},\"weight\":100}]}}", nodeKey)) + defer viper.Set(constant.CfgKeyDataPlaneAffinity, "") + + affinity, err := buildPodAffinity(clusterObj, clusterObj.Spec.Affinity, component) + Expect(err).Should(Succeed()) + Expect(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(labelKey)) + Expect(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution[0].TopologyKey).Should(Equal(topologyKey)) + Expect(affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution).Should(BeEmpty()) + Expect(affinity.NodeAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Preference.MatchExpressions[0].Key).Should(Equal(nodeKey)) topologySpreadConstraints := buildPodTopologySpreadConstraints(clusterObj, clusterObj.Spec.Affinity, component) Expect(topologySpreadConstraints[0].WhenUnsatisfiable).Should(Equal(corev1.DoNotSchedule)) @@ -100,51 +135,40 @@ var _ = Describe("affinity utils", func() { }) }) - Context("with PodAntiAffinity set to Preferred", func() { - const topologyKey = "testTopologyKey" - const lableKey = "testNodeLabelKey" - const labelValue = "testLabelValue" - + Context("with tolerations", func() { BeforeEach(func() { - clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - GetObject() + buildObjs(appsv1alpha1.Required) + }) - clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(mysqlCompType).AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - GetObject() + It("should have correct tolerations", func() { + tolerations, err := BuildTolerations(clusterObj, &clusterObj.Spec.ComponentSpecs[0]) + Expect(err).Should(Succeed()) + Expect(tolerations).ShouldNot(BeEmpty()) + Expect(tolerations[0].Key).Should(Equal(clusterTolerationKey)) + }) - affinity := &appsv1alpha1.Affinity{ - PodAntiAffinity: appsv1alpha1.Preferred, - TopologyKeys: []string{topologyKey}, - NodeLabels: map[string]string{ - lableKey: labelValue, - }, - } - clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType). - SetClusterAffinity(affinity). - GetObject() + It("when data plane tolerations is set, should have correct tolerations", func() { + const dpTolerationKey = "dataPlaneTolerationKey" + viper.Set(constant.CfgKeyDataPlaneTolerations, fmt.Sprintf("[{\"key\":\"%s\", \"operator\": \"Exists\", \"effect\": \"NoSchedule\"}]", dpTolerationKey)) + defer viper.Set(constant.CfgKeyDataPlaneTolerations, "") + tolerations, err := BuildTolerations(clusterObj, &clusterObj.Spec.ComponentSpecs[0]) + Expect(err).Should(Succeed()) + Expect(tolerations).Should(HaveLen(2)) + Expect(tolerations[0].Key).Should(Equal(clusterTolerationKey)) + Expect(tolerations[1].Key).Should(Equal(dpTolerationKey)) + }) + }) - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: tlog, - } - component = BuildComponent( - reqCtx, - *clusterObj, - *clusterDefObj, - clusterDefObj.Spec.ComponentDefs[0], - clusterObj.Spec.ComponentSpecs[0], - &clusterVersionObj.Spec.ComponentVersions[0], - ) + Context("with PodAntiAffinity set to Preferred", func() { + BeforeEach(func() { + buildObjs(appsv1alpha1.Preferred) Expect(component).ShouldNot(BeNil()) }) It("should have correct Affinity and TopologySpreadConstraints", func() { - affinity := buildPodAffinity(clusterObj, clusterObj.Spec.Affinity, component) - Expect(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(lableKey)) + affinity, err := buildPodAffinity(clusterObj, clusterObj.Spec.Affinity, component) + Expect(err).Should(Succeed()) + Expect(affinity.NodeAffinity.RequiredDuringSchedulingIgnoredDuringExecution.NodeSelectorTerms[0].MatchExpressions[0].Key).Should(Equal(labelKey)) Expect(affinity.PodAntiAffinity.RequiredDuringSchedulingIgnoredDuringExecution).Should(BeEmpty()) Expect(affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].Weight).ShouldNot(BeNil()) Expect(affinity.PodAntiAffinity.PreferredDuringSchedulingIgnoredDuringExecution[0].PodAffinityTerm.TopologyKey).Should(Equal(topologyKey)) diff --git a/internal/controller/component/component.go b/internal/controller/component/component.go index 550704263..a1074c875 100644 --- a/internal/controller/component/component.go +++ b/internal/controller/component/component.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -22,6 +25,7 @@ import ( corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" @@ -29,44 +33,75 @@ import ( intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// BuildComponent generates a new Component object, which is a mixture of -// component-related configs from input Cluster, ClusterDef and ClusterVersion. -func BuildComponent( - reqCtx intctrlutil.RequestCtx, +func BuildSynthesizedComponent(reqCtx intctrlutil.RequestCtx, + cli client.Client, cluster appsv1alpha1.Cluster, clusterDef appsv1alpha1.ClusterDefinition, clusterCompDef appsv1alpha1.ClusterComponentDefinition, clusterCompSpec appsv1alpha1.ClusterComponentSpec, clusterCompVers ...*appsv1alpha1.ClusterComponentVersion, -) *SynthesizedComponent { +) (*SynthesizedComponent, error) { + synthesizedComp, err := buildComponent(reqCtx, cluster, clusterDef, clusterCompDef, clusterCompSpec, clusterCompVers...) + if err != nil { + return nil, err + } + /* + if err := buildRestoreInfoFromBackup(reqCtx, cli, cluster, synthesizedComp); err != nil { + return nil, err + } + */ + return synthesizedComp, nil +} +func BuildComponent(reqCtx intctrlutil.RequestCtx, + cluster appsv1alpha1.Cluster, + clusterDef appsv1alpha1.ClusterDefinition, + clusterCompDef appsv1alpha1.ClusterComponentDefinition, + clusterCompSpec appsv1alpha1.ClusterComponentSpec, + clusterCompVers ...*appsv1alpha1.ClusterComponentVersion, +) (*SynthesizedComponent, error) { + return buildComponent(reqCtx, cluster, clusterDef, clusterCompDef, clusterCompSpec, clusterCompVers...) +} + +// buildComponent generates a new Component object, which is a mixture of +// component-related configs from input Cluster, ClusterDef and ClusterVersion. +func buildComponent(reqCtx intctrlutil.RequestCtx, + cluster appsv1alpha1.Cluster, + clusterDef appsv1alpha1.ClusterDefinition, + clusterCompDef appsv1alpha1.ClusterComponentDefinition, + clusterCompSpec appsv1alpha1.ClusterComponentSpec, + clusterCompVers ...*appsv1alpha1.ClusterComponentVersion, +) (*SynthesizedComponent, error) { + var err error clusterCompDefObj := clusterCompDef.DeepCopy() component := &SynthesizedComponent{ ClusterDefName: clusterDef.Name, + ClusterName: cluster.Name, + ClusterUID: string(cluster.UID), Name: clusterCompSpec.Name, - Type: clusterCompDefObj.Name, + CompDefName: clusterCompDefObj.Name, CharacterType: clusterCompDefObj.CharacterType, - MaxUnavailable: clusterCompDefObj.MaxUnavailable, WorkloadType: clusterCompDefObj.WorkloadType, + StatelessSpec: clusterCompDefObj.StatelessSpec, + StatefulSpec: clusterCompDefObj.StatefulSpec, ConsensusSpec: clusterCompDefObj.ConsensusSpec, + ReplicationSpec: clusterCompDefObj.ReplicationSpec, PodSpec: clusterCompDefObj.PodSpec, Probes: clusterCompDefObj.Probes, LogConfigs: clusterCompDefObj.LogConfigs, HorizontalScalePolicy: clusterCompDefObj.HorizontalScalePolicy, + ConfigTemplates: clusterCompDefObj.ConfigSpecs, + ScriptTemplates: clusterCompDefObj.ScriptSpecs, + VolumeTypes: clusterCompDefObj.VolumeTypes, + CustomLabelSpecs: clusterCompDefObj.CustomLabelSpecs, + StatefulSetWorkload: clusterCompDefObj.GetStatefulSetWorkload(), + MinAvailable: clusterCompSpec.GetMinAvailable(clusterCompDefObj.GetMinAvailable()), Replicas: clusterCompSpec.Replicas, EnabledLogs: clusterCompSpec.EnabledLogs, TLS: clusterCompSpec.TLS, Issuer: clusterCompSpec.Issuer, - VolumeTypes: clusterCompDefObj.VolumeTypes, - CustomLabelSpecs: clusterCompDefObj.CustomLabelSpecs, - } - - // resolve component.ConfigTemplates - if clusterCompDefObj.ConfigSpecs != nil { - component.ConfigTemplates = clusterCompDefObj.ConfigSpecs - } - if clusterCompDefObj.ScriptSpecs != nil { - component.ScriptTemplates = clusterCompDefObj.ScriptSpecs + ComponentDef: clusterCompSpec.ComponentDefRef, + ServiceAccountName: clusterCompSpec.ServiceAccountName, } if len(clusterCompVers) > 0 && clusterCompVers[0] != nil { @@ -82,34 +117,32 @@ func BuildComponent( } } + // handle component.PodSpec extra settings // set affinity and tolerations affinity := cluster.Spec.Affinity if clusterCompSpec.Affinity != nil { affinity = clusterCompSpec.Affinity } - podAffinity := buildPodAffinity(&cluster, affinity, component) - component.PodSpec.Affinity = patchBuiltInAffinity(podAffinity) + if component.PodSpec.Affinity, err = buildPodAffinity(&cluster, affinity, component); err != nil { + reqCtx.Log.Error(err, "build pod affinity failed.") + return nil, err + } component.PodSpec.TopologySpreadConstraints = buildPodTopologySpreadConstraints(&cluster, affinity, component) - - tolerations := cluster.Spec.Tolerations - if len(clusterCompSpec.Tolerations) != 0 { - tolerations = clusterCompSpec.Tolerations + if component.PodSpec.Tolerations, err = BuildTolerations(&cluster, &clusterCompSpec); err != nil { + reqCtx.Log.Error(err, "build pod tolerations failed.") + return nil, err } - component.PodSpec.Tolerations = PatchBuiltInToleration(tolerations) if clusterCompSpec.VolumeClaimTemplates != nil { component.VolumeClaimTemplates = clusterCompSpec.ToVolumeClaimTemplates() } - if clusterCompSpec.Resources.Requests != nil || clusterCompSpec.Resources.Limits != nil { component.PodSpec.Containers[0].Resources = clusterCompSpec.Resources } - if clusterCompDefObj.Service != nil { service := corev1.Service{Spec: clusterCompDefObj.Service.ToSVCSpec()} service.Spec.Type = corev1.ServiceTypeClusterIP component.Services = append(component.Services, service) - for _, item := range clusterCompSpec.Services { service = corev1.Service{ ObjectMeta: metav1.ObjectMeta{ @@ -122,11 +155,12 @@ func BuildComponent( component.Services = append(component.Services, service) } } - component.PrimaryIndex = clusterCompSpec.PrimaryIndex + // set component.PodSpec.ServiceAccountName + component.PodSpec.ServiceAccountName = component.ServiceAccountName - // TODO(zhixu.zt) We need to reserve the VolumeMounts of the container for ConfigMap or Secret, - // At present, it is possible to distinguish between ConfigMap volume and normal volume, + // TODO: (zhixu.zt) We need to reserve the VolumeMounts of the container for ConfigMap or Secret, + // At present, it is not possible to distinguish between ConfigMap volume and normal volume, // Compare the VolumeName of configTemplateRef and Name of VolumeMounts // // if component.VolumeClaimTemplates == nil { @@ -136,18 +170,15 @@ func BuildComponent( // } buildMonitorConfig(&clusterCompDef, &clusterCompSpec, component) - err := buildProbeContainers(reqCtx, component) - if err != nil { + if err = buildProbeContainers(reqCtx, component); err != nil { reqCtx.Log.Error(err, "build probe container failed.") - return nil + return nil, err } - replaceContainerPlaceholderTokens(component, GetEnvReplacementMapForConnCredential(cluster.GetName())) - - return component + return component, nil } -// appendOrOverrideContainerAttr is used to append targetContainer to compContainers or override the attributes of compContainers with a given targetContainer, +// appendOrOverrideContainerAttr appends targetContainer to compContainers or overrides the attributes of compContainers with a given targetContainer, // if targetContainer does not exist in compContainers, it will be appended. otherwise it will be updated with the attributes of the target container. func appendOrOverrideContainerAttr(compContainers []corev1.Container, targetContainer corev1.Container) []corev1.Container { index, compContainer := intctrlutil.GetContainerByName(compContainers, targetContainer.Name) @@ -223,7 +254,7 @@ func doContainerAttrOverride(compContainer *corev1.Container, container corev1.C // GetEnvReplacementMapForConnCredential gets the replacement map for connect credential func GetEnvReplacementMapForConnCredential(clusterName string) map[string]string { return map[string]string{ - constant.ConnCredentialPlaceHolder: GenerateConnCredential(clusterName), + constant.KBConnCredentialPlaceHolder: GenerateConnCredential(clusterName), } } @@ -237,12 +268,20 @@ func replaceContainerPlaceholderTokens(component *SynthesizedComponent, namedVal } // GetReplacementMapForBuiltInEnv gets the replacement map for KubeBlocks built-in environment variables. -func GetReplacementMapForBuiltInEnv(clusterName, componentName string) map[string]string { - return map[string]string{ +func GetReplacementMapForBuiltInEnv(clusterName, clusterUID, componentName string) map[string]string { + cc := fmt.Sprintf("%s-%s", clusterName, componentName) + replacementMap := map[string]string{ constant.KBClusterNamePlaceHolder: clusterName, constant.KBCompNamePlaceHolder: componentName, - constant.KBClusterCompNamePlaceHolder: fmt.Sprintf("%s-%s", clusterName, componentName), + constant.KBClusterCompNamePlaceHolder: cc, + constant.KBComponentEnvCMPlaceHolder: fmt.Sprintf("%s-env", cc), + } + if len(clusterUID) > 8 { + replacementMap[constant.KBClusterUIDPostfix8PlaceHolder] = clusterUID[len(clusterUID)-8:] + } else { + replacementMap[constant.KBClusterUIDPostfix8PlaceHolder] = clusterUID } + return replacementMap } // ReplaceNamedVars replaces the placeholder in targetVar if it is match and returns the replaced result diff --git a/internal/controller/component/component_test.go b/internal/controller/component/component_test.go index ffe190b5e..8fd643380 100644 --- a/internal/controller/component/component_test.go +++ b/internal/controller/component/component_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -40,9 +43,9 @@ var _ = Describe("component module", func() { clusterDefName = "test-clusterdef" clusterVersionName = "test-clusterversion" clusterName = "test-cluster" - mysqlCompType = "replicasets" + mysqlCompDefName = "replicasets" mysqlCompName = "mysql" - nginxCompType = "proxy" + proxyCompDefName = "proxy" mysqlSecretUserEnvName = "MYSQL_ROOT_USER" mysqlSecretPasswdEnvName = "MYSQL_ROOT_PASSWORD" ) @@ -55,20 +58,20 @@ var _ = Describe("component module", func() { BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). + AddComponentDef(testapps.StatelessNginxComponent, proxyCompDefName). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - AddComponent(nginxCompType). + AddComponentVersion(proxyCompDefName). AddInitContainerShort("nginx-init", testapps.NginxImage). AddContainerShort("nginx", testapps.NginxImage). GetObject() pvcSpec := testapps.NewPVCSpec("1Gi") cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). GetObject() }) @@ -79,45 +82,49 @@ var _ = Describe("component module", func() { Ctx: ctx, Log: tlog, } - component := BuildComponent( + component, err := BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) + Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) - By("leave clusterVersion.versionCtx empty initContains and conainers") + By("leave clusterVersion.versionCtx empty initContains and containers") clusterVersion.Spec.ComponentVersions[0].VersionsCtx.Containers = nil clusterVersion.Spec.ComponentVersions[0].VersionsCtx.InitContainers = nil - component = BuildComponent( + component, err = BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) + Expect(err).Should(Succeed()) Expect(component).ShouldNot(BeNil()) By("new container in clusterVersion not in clusterDefinition") - component = BuildComponent( + component, err = BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[1]) + Expect(err).Should(Succeed()) Expect(len(component.PodSpec.Containers)).Should(Equal(2)) By("new init container in clusterVersion not in clusterDefinition") - component = BuildComponent( + component, err = BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[1]) + Expect(err).Should(Succeed()) Expect(len(component.PodSpec.InitContainers)).Should(Equal(1)) }) @@ -131,7 +138,7 @@ var _ = Describe("component module", func() { SecretKeyRef: &corev1.SecretKeySelector{ Key: "username", LocalObjectReference: corev1.LocalObjectReference{ - Name: constant.ConnCredentialPlaceHolder, + Name: constant.KBConnCredentialPlaceHolder, }, }, }, @@ -142,7 +149,7 @@ var _ = Describe("component module", func() { SecretKeyRef: &corev1.SecretKeySelector{ Key: "password", LocalObjectReference: corev1.LocalObjectReference{ - Name: constant.ConnCredentialPlaceHolder, + Name: constant.KBConnCredentialPlaceHolder, }, }, }, diff --git a/internal/controller/component/cue/probe_template.cue b/internal/controller/component/cue/probe_template.cue index 01732a8b8..0eae78919 100644 --- a/internal/controller/component/cue/probe_template.cue +++ b/internal/controller/component/cue/probe_template.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . probeContainer: { image: "registry.cn-hangzhou.aliyuncs.com/google_containers/pause:3.6" diff --git a/internal/controller/component/monitor_utils.go b/internal/controller/component/monitor_utils.go index e7a36974a..87395f778 100644 --- a/internal/controller/component/monitor_utils.go +++ b/internal/controller/component/monitor_utils.go @@ -1,22 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component import ( + "k8s.io/apimachinery/pkg/util/intstr" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) @@ -42,18 +47,34 @@ func buildMonitorConfig( } component.Monitor = &MonitorConfig{ Enable: true, + BuiltIn: false, ScrapePath: monitorConfig.Exporter.ScrapePath, - ScrapePort: monitorConfig.Exporter.ScrapePort, + ScrapePort: monitorConfig.Exporter.ScrapePort.IntVal, + } + + if monitorConfig.Exporter.ScrapePort.Type == intstr.String { + portName := monitorConfig.Exporter.ScrapePort.StrVal + for _, c := range clusterCompDef.PodSpec.Containers { + for _, p := range c.Ports { + if p.Name == portName { + component.Monitor.ScrapePort = p.ContainerPort + break + } + } + } } return } - // TODO: builtin will support by an independent agent soon - disableMonitor(component) + component.Monitor = &MonitorConfig{ + Enable: true, + BuiltIn: true, + } } func disableMonitor(component *SynthesizedComponent) { component.Monitor = &MonitorConfig{ - Enable: false, + Enable: false, + BuiltIn: false, } } diff --git a/internal/controller/component/monitor_utils_test.go b/internal/controller/component/monitor_utils_test.go index 01663dfbc..32e3e82be 100644 --- a/internal/controller/component/monitor_utils_test.go +++ b/internal/controller/component/monitor_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -19,6 +22,7 @@ package component import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/util/intstr" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) @@ -37,7 +41,7 @@ var _ = Describe("monitor_utils", func() { clusterCompDef.Monitor = &appsv1alpha1.MonitorConfig{ BuiltIn: false, Exporter: &appsv1alpha1.ExporterConfig{ - ScrapePort: 9144, + ScrapePort: intstr.FromInt(9144), ScrapePath: "/metrics", }, } @@ -48,6 +52,7 @@ var _ = Describe("monitor_utils", func() { buildMonitorConfig(clusterCompDef, clusterCompSpec, component) monitorConfig := component.Monitor Expect(monitorConfig.Enable).Should(BeFalse()) + Expect(monitorConfig.BuiltIn).Should(BeFalse()) Expect(monitorConfig.ScrapePort).To(BeEquivalentTo(0)) Expect(monitorConfig.ScrapePath).To(Equal("")) }) @@ -58,28 +63,31 @@ var _ = Describe("monitor_utils", func() { buildMonitorConfig(clusterCompDef, clusterCompSpec, component) monitorConfig := component.Monitor Expect(monitorConfig.Enable).Should(BeTrue()) + Expect(monitorConfig.BuiltIn).Should(BeFalse()) Expect(monitorConfig.ScrapePort).To(BeEquivalentTo(9144)) Expect(monitorConfig.ScrapePath).To(Equal("/metrics")) }) - It("should disable monitor if ClusterComponentDefinition.Monitor.BuiltIn is false and lacks ExporterConfig", func() { + It("should disable monitor if ClusterComponentDefinition.Monitor.BuiltIn is false and lack of ExporterConfig", func() { clusterCompSpec.Monitor = true clusterCompDef.Monitor.BuiltIn = false clusterCompDef.Monitor.Exporter = nil buildMonitorConfig(clusterCompDef, clusterCompSpec, component) monitorConfig := component.Monitor Expect(monitorConfig.Enable).Should(BeFalse()) + Expect(monitorConfig.BuiltIn).Should(BeFalse()) Expect(monitorConfig.ScrapePort).To(BeEquivalentTo(0)) Expect(monitorConfig.ScrapePath).To(Equal("")) }) - It("should disable monitor if ClusterComponentDefinition.Monitor.BuiltIn is true", func() { + It("should enable monitor if ClusterComponentDefinition.Monitor.BuiltIn is true", func() { clusterCompSpec.Monitor = true clusterCompDef.Monitor.BuiltIn = true clusterCompDef.Monitor.Exporter = nil buildMonitorConfig(clusterCompDef, clusterCompSpec, component) monitorConfig := component.Monitor - Expect(monitorConfig.Enable).Should(BeFalse()) + Expect(monitorConfig.Enable).Should(BeTrue()) + Expect(monitorConfig.BuiltIn).Should(BeTrue()) Expect(monitorConfig.ScrapePort).To(BeEquivalentTo(0)) Expect(monitorConfig.ScrapePath).To(Equal("")) }) diff --git a/internal/controller/component/port_utils.go b/internal/controller/component/port_utils.go index 00191d3ad..6708d0840 100644 --- a/internal/controller/component/port_utils.go +++ b/internal/controller/component/port_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component diff --git a/internal/controller/component/port_utils_test.go b/internal/controller/component/port_utils_test.go index 8a4853f7d..183c0acdd 100644 --- a/internal/controller/component/port_utils_test.go +++ b/internal/controller/component/port_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component diff --git a/internal/controller/component/probe_utils.go b/internal/controller/component/probe_utils.go index ca4562617..f7e66cf3e 100644 --- a/internal/controller/component/probe_utils.go +++ b/internal/controller/component/probe_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -35,7 +38,9 @@ import ( const ( // http://localhost:/v1.0/bindings/ - roleObserveURIFormat = "http://localhost:%s/v1.0/bindings/%s" + checkRoleURIFormat = "http://localhost:%s/v1.0/bindings/%s" + checkRunningURIFormat = "/v1.0/bindings/%s?operation=checkRunning" + checkStatusURIFormat = "/v1.0/bindings/%s?operation=checkStatus" ) var ( @@ -66,21 +71,21 @@ func buildProbeContainers(reqCtx intctrlutil.RequestCtx, component *SynthesizedC return err } - if componentProbes.RoleChangedProbe != nil { + if componentProbes.RoleProbe != nil { roleChangedContainer := container.DeepCopy() - buildRoleChangedProbeContainer(component.CharacterType, roleChangedContainer, componentProbes.RoleChangedProbe, int(probeSvcHTTPPort)) + buildRoleProbeContainer(component.CharacterType, roleChangedContainer, componentProbes.RoleProbe, int(probeSvcHTTPPort)) probeContainers = append(probeContainers, *roleChangedContainer) } if componentProbes.StatusProbe != nil { statusProbeContainer := container.DeepCopy() - buildStatusProbeContainer(statusProbeContainer, componentProbes.StatusProbe, int(probeSvcHTTPPort)) + buildStatusProbeContainer(component.CharacterType, statusProbeContainer, componentProbes.StatusProbe, int(probeSvcHTTPPort)) probeContainers = append(probeContainers, *statusProbeContainer) } if componentProbes.RunningProbe != nil { runningProbeContainer := container.DeepCopy() - buildRunningProbeContainer(runningProbeContainer, componentProbes.RunningProbe, int(probeSvcHTTPPort)) + buildRunningProbeContainer(component.CharacterType, runningProbeContainer, componentProbes.RunningProbe, int(probeSvcHTTPPort)) probeContainers = append(probeContainers, *runningProbeContainer) } @@ -120,21 +125,17 @@ func buildProbeServiceContainer(component *SynthesizedComponent, container *core container.Command = []string{"probe", "--app-id", "batch-sdk", "--dapr-http-port", strconv.Itoa(probeSvcHTTPPort), "--dapr-grpc-port", strconv.Itoa(probeSvcGRPCPort), - "--app-protocol", "http", "--log-level", logLevel, "--config", "/config/probe/config.yaml", "--components-path", "/config/probe/components"} - if len(component.Services) > 0 && len(component.Services[0].Spec.Ports) > 0 { - service := component.Services[0] - port := service.Spec.Ports[0] - dbPort := port.TargetPort.IntValue() - if dbPort == 0 { - dbPort = int(port.Port) - } + if len(component.PodSpec.Containers) > 0 && len(component.PodSpec.Containers[0].Ports) > 0 { + mainContainer := component.PodSpec.Containers[0] + port := mainContainer.Ports[0] + dbPort := port.ContainerPort container.Env = append(container.Env, corev1.EnvVar{ Name: constant.KBPrefix + "_SERVICE_PORT", - Value: strconv.Itoa(dbPort), + Value: strconv.Itoa(int(dbPort)), ValueFrom: nil, }) } @@ -182,13 +183,13 @@ func getComponentRoles(component *SynthesizedComponent) map[string]string { return roles } -func buildRoleChangedProbeContainer(characterType string, roleChangedContainer *corev1.Container, +func buildRoleProbeContainer(characterType string, roleChangedContainer *corev1.Container, probeSetting *appsv1alpha1.ClusterDefinitionProbe, probeSvcHTTPPort int) { roleChangedContainer.Name = constant.RoleProbeContainerName probe := roleChangedContainer.ReadinessProbe bindingType := strings.ToLower(characterType) svcPort := strconv.Itoa(probeSvcHTTPPort) - roleObserveURI := fmt.Sprintf(roleObserveURIFormat, svcPort, bindingType) + roleObserveURI := fmt.Sprintf(checkRoleURIFormat, svcPort, bindingType) probe.Exec.Command = []string{ "curl", "-X", "POST", "--max-time", strconv.Itoa(int(probeSetting.TimeoutSeconds)), @@ -203,12 +204,12 @@ func buildRoleChangedProbeContainer(characterType string, roleChangedContainer * roleChangedContainer.StartupProbe.TCPSocket.Port = intstr.FromInt(probeSvcHTTPPort) } -func buildStatusProbeContainer(statusProbeContainer *corev1.Container, +func buildStatusProbeContainer(characterType string, statusProbeContainer *corev1.Container, probeSetting *appsv1alpha1.ClusterDefinitionProbe, probeSvcHTTPPort int) { statusProbeContainer.Name = constant.StatusProbeContainerName probe := statusProbeContainer.ReadinessProbe httpGet := &corev1.HTTPGetAction{} - httpGet.Path = "/v1.0/bindings/probe?operation=checkStatus" + httpGet.Path = fmt.Sprintf(checkStatusURIFormat, characterType) httpGet.Port = intstr.FromInt(probeSvcHTTPPort) probe.Exec = nil probe.HTTPGet = httpGet @@ -218,12 +219,12 @@ func buildStatusProbeContainer(statusProbeContainer *corev1.Container, statusProbeContainer.StartupProbe.TCPSocket.Port = intstr.FromInt(probeSvcHTTPPort) } -func buildRunningProbeContainer(runningProbeContainer *corev1.Container, +func buildRunningProbeContainer(characterType string, runningProbeContainer *corev1.Container, probeSetting *appsv1alpha1.ClusterDefinitionProbe, probeSvcHTTPPort int) { runningProbeContainer.Name = constant.RunningProbeContainerName probe := runningProbeContainer.ReadinessProbe httpGet := &corev1.HTTPGetAction{} - httpGet.Path = "/v1.0/bindings/probe?operation=checkRunning" + httpGet.Path = fmt.Sprintf(checkRunningURIFormat, characterType) httpGet.Port = intstr.FromInt(probeSvcHTTPPort) probe.Exec = nil probe.HTTPGet = httpGet diff --git a/internal/controller/component/probe_utils_test.go b/internal/controller/component/probe_utils_test.go index 746bcae46..b2b5ed0ec 100644 --- a/internal/controller/component/probe_utils_test.go +++ b/internal/controller/component/probe_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -72,9 +75,9 @@ var _ = Describe("probe_utils", func() { }, } component.Probes = &appsv1alpha1.ClusterDefinitionProbes{ - RunningProbe: &appsv1alpha1.ClusterDefinitionProbe{}, - StatusProbe: &appsv1alpha1.ClusterDefinitionProbe{}, - RoleChangedProbe: &appsv1alpha1.ClusterDefinitionProbe{}, + RunningProbe: &appsv1alpha1.ClusterDefinitionProbe{}, + StatusProbe: &appsv1alpha1.ClusterDefinitionProbe{}, + RoleProbe: &appsv1alpha1.ClusterDefinitionProbe{}, } component.PodSpec = &corev1.PodSpec{ Containers: []corev1.Container{}, @@ -92,7 +95,7 @@ var _ = Describe("probe_utils", func() { }) It("should build role changed probe container", func() { - buildRoleChangedProbeContainer("wesql", container, clusterDefProbe, probeServiceHTTPPort) + buildRoleProbeContainer("wesql", container, clusterDefProbe, probeServiceHTTPPort) Expect(container.ReadinessProbe.Exec.Command).ShouldNot(BeEmpty()) }) @@ -102,12 +105,12 @@ var _ = Describe("probe_utils", func() { }) It("should build status probe container", func() { - buildStatusProbeContainer(container, clusterDefProbe, probeServiceHTTPPort) + buildStatusProbeContainer("wesql", container, clusterDefProbe, probeServiceHTTPPort) Expect(container.ReadinessProbe.HTTPGet).ShouldNot(BeNil()) }) It("should build running probe container", func() { - buildRunningProbeContainer(container, clusterDefProbe, probeServiceHTTPPort) + buildRunningProbeContainer("wesql", container, clusterDefProbe, probeServiceHTTPPort) Expect(container.ReadinessProbe.HTTPGet).ShouldNot(BeNil()) }) }) diff --git a/internal/controller/component/restore_utils.go b/internal/controller/component/restore_utils.go index 4d27874cc..4565be8c8 100644 --- a/internal/controller/component/restore_utils.go +++ b/internal/controller/component/restore_utils.go @@ -1,34 +1,61 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component import ( + "encoding/json" "fmt" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) +func getClusterBackupSourceMap(cluster appsv1alpha1.Cluster) (map[string]string, error) { + compBackupMapString := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] + if len(compBackupMapString) == 0 { + return nil, nil + } + compBackupMap := map[string]string{} + err := json.Unmarshal([]byte(compBackupMapString), &compBackupMap) + return compBackupMap, err +} + +func getComponentBackupSource(cluster appsv1alpha1.Cluster, compName string) (string, error) { + backupSources, err := getClusterBackupSourceMap(cluster) + if err != nil { + return "", err + } + if source, ok := backupSources[compName]; ok { + return source, nil + } + return "", nil +} + func getBackupObjects(reqCtx intctrlutil.RequestCtx, cli client.Client, namespace string, @@ -49,9 +76,9 @@ func getBackupObjects(reqCtx intctrlutil.RequestCtx, return backup, backupTool, nil } -// BuildRestoredInfo builds restore-related infos if it needs to restore from backup, such as init container/pvc dataSource. -func BuildRestoredInfo( - reqCtx intctrlutil.RequestCtx, +// BuildRestoredInfo builds restore infos when restore from backup, such as init-container, pvc dataSource. +// Deprecated: using DoRestore function instead. +func BuildRestoredInfo(reqCtx intctrlutil.RequestCtx, cli client.Client, namespace string, component *SynthesizedComponent, @@ -60,32 +87,52 @@ func BuildRestoredInfo( if err != nil { return err } - return BuildRestoredInfo2(component, backup, backupTool) + return buildRestoredInfo2(component, backup, backupTool) } -// BuildRestoredInfo2 builds restore-related infos if it needs to restore from backup, such as init container/pvc dataSource. -func BuildRestoredInfo2( - component *SynthesizedComponent, +// BuildRestoreInfoFromBackup restore from snapshot or datafile +// Deprecated: using DoRestore function instead. +func BuildRestoreInfoFromBackup(reqCtx intctrlutil.RequestCtx, cli client.Client, cluster appsv1alpha1.Cluster, + component *SynthesizedComponent) error { + // build info that needs to be restored from backup + backupSourceName, err := getComponentBackupSource(cluster, component.Name) + if err != nil { + return err + } + if len(backupSourceName) == 0 { + return nil + } + + backup, backupTool, err := getBackupObjects(reqCtx, cli, cluster.Namespace, backupSourceName) + if err != nil { + return err + } + return buildRestoredInfo2(component, backup, backupTool) +} + +// buildRestoredInfo2 builds restore infos when restore from backup, such as init-container, pvc dataSource. +func buildRestoredInfo2(component *SynthesizedComponent, backup *dataprotectionv1alpha1.Backup, backupTool *dataprotectionv1alpha1.BackupTool) error { if backup.Status.Phase != dataprotectionv1alpha1.BackupCompleted { return intctrlutil.NewErrorf(intctrlutil.ErrorTypeBackupNotCompleted, "backup %s is not completed", backup.Name) } switch backup.Spec.BackupType { - case dataprotectionv1alpha1.BackupTypeFull: + case dataprotectionv1alpha1.BackupTypeDataFile: return buildInitContainerWithFullBackup(component, backup, backupTool) case dataprotectionv1alpha1.BackupTypeSnapshot: - buildVolumeClaimTemplatesWithSnapshot(component, backup) + return buildVolumeClaimTemplatesWithSnapshot(component, backup) } return nil } -// GetRestoredInitContainerName gets the init container name for restore. +// GetRestoredInitContainerName gets the restore init container name. +// Deprecated: using DoRestore function instead. func GetRestoredInitContainerName(backupName string) string { return fmt.Sprintf("restore-%s", backupName) } -// buildInitContainerWithFullBackup builds the init container if it needs to restore from full backup +// buildInitContainerWithFullBackup builds the init container when restore from full backup func buildInitContainerWithFullBackup( component *SynthesizedComponent, backup *dataprotectionv1alpha1.Backup, @@ -93,8 +140,8 @@ func buildInitContainerWithFullBackup( if component.PodSpec == nil || len(component.PodSpec.Containers) == 0 { return nil } - if backup.Status.RemoteVolume == nil { - return fmt.Errorf("remote volume can not be empty in Backup.status.remoteVolume") + if len(backup.Status.PersistentVolumeClaimName) == 0 { + return fmt.Errorf("persistentVolumeClaimName cannot be empty in Backup.status") } container := corev1.Container{} container.Name = GetRestoredInitContainerName(backup.Name) @@ -106,10 +153,17 @@ func buildInitContainerWithFullBackup( } container.VolumeMounts = component.PodSpec.Containers[0].VolumeMounts // add the volumeMounts with backup volume - randomVolumeName := fmt.Sprintf("%s-%s", component.Name, backup.Status.RemoteVolume.Name) - backup.Status.RemoteVolume.Name = randomVolumeName + backupVolumeName := fmt.Sprintf("%s-%s", component.Name, backup.Status.PersistentVolumeClaimName) + remoteVolume := corev1.Volume{ + Name: backupVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backup.Status.PersistentVolumeClaimName, + }, + }, + } remoteVolumeMount := corev1.VolumeMount{} - remoteVolumeMount.Name = randomVolumeName + remoteVolumeMount.Name = backupVolumeName remoteVolumeMount.MountPath = "/" + backup.Name container.VolumeMounts = append(container.VolumeMounts, remoteVolumeMount) @@ -119,6 +173,11 @@ func buildInitContainerWithFullBackup( AllowPrivilegeEscalation: &allowPrivilegeEscalation, RunAsUser: &runAsUser} + backupDataPath := fmt.Sprintf("/%s/%s", backup.Name, backup.Namespace) + manifests := backup.Status.Manifests + if manifests != nil && manifests.BackupTool != nil { + backupDataPath = fmt.Sprintf("/%s%s", backup.Name, manifests.BackupTool.FilePath) + } // build env for restore container.Env = []corev1.EnvVar{ { @@ -126,23 +185,33 @@ func buildInitContainerWithFullBackup( Value: backup.Name, }, { Name: "BACKUP_DIR", - Value: fmt.Sprintf("/%s/%s", backup.Name, backup.Namespace), + Value: backupDataPath, }} // merge env from backup tool. container.Env = append(container.Env, backupTool.Spec.Env...) // add volume of backup data - component.PodSpec.Volumes = append(component.PodSpec.Volumes, *backup.Status.RemoteVolume) + component.PodSpec.Volumes = append(component.PodSpec.Volumes, remoteVolume) component.PodSpec.InitContainers = append(component.PodSpec.InitContainers, container) return nil } -// buildVolumeClaimTemplatesWithSnapshot builds the volumeClaimTemplate if it needs to restore from volumeSnapshot +// buildVolumeClaimTemplatesWithSnapshot builds the volumeClaimTemplate when restore from volumeSnapshot func buildVolumeClaimTemplatesWithSnapshot(component *SynthesizedComponent, - backup *dataprotectionv1alpha1.Backup) { + backup *dataprotectionv1alpha1.Backup) error { if len(component.VolumeClaimTemplates) == 0 { - return + return intctrlutil.NewError(intctrlutil.ErrorTypeBackupNotSupported, + "need specified volumeClaimTemplates to restore.") } vct := component.VolumeClaimTemplates[0] + backupTotalSize, err := resource.ParseQuantity(backup.Status.TotalSize) + if err != nil { + return err + } + if vct.Spec.Resources.Requests.Storage().Value() < backupTotalSize.Value() { + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeStorageNotMatch, + "requests storage %s is less than source backup storage %s.", + vct.Spec.Resources.Requests.Storage(), backupTotalSize.String()) + } snapshotAPIGroup := snapshotv1.GroupName vct.Spec.DataSource = &corev1.TypedLocalObjectReference{ APIGroup: &snapshotAPIGroup, @@ -150,4 +219,5 @@ func buildVolumeClaimTemplatesWithSnapshot(component *SynthesizedComponent, Name: backup.Name, } component.VolumeClaimTemplates[0] = vct + return nil } diff --git a/internal/controller/component/restore_utils_test.go b/internal/controller/component/restore_utils_test.go index 2d5e5488c..73684f19a 100644 --- a/internal/controller/component/restore_utils_test.go +++ b/internal/controller/component/restore_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -21,6 +24,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/resource" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" corev1 "k8s.io/api/core/v1" @@ -36,14 +40,13 @@ import ( var _ = Describe("probe_utils", func() { const backupPolicyName = "test-backup-policy" - const defaultTTL = "168h0m0s" const backupName = "test-backup-job" var backupToolName string Context("build restore info ", func() { cleanEnv := func() { - // must wait until resources deleted and no longer exist before the testcases start, + // must wait till resources deleted and no longer existed before the testcases start, // otherwise if later it needs to create some new resource objects with the same name, // in race conditions, it will find the existence of old objects, resulting failure to // create the new objects. @@ -68,10 +71,9 @@ var _ = Describe("probe_utils", func() { updateBackupStatus := func(backup *dataprotectionv1alpha1.Backup, backupToolName string, expectPhase dataprotectionv1alpha1.BackupPhase) { Expect(testapps.ChangeObjStatus(&testCtx, backup, func() { backup.Status.BackupToolName = backupToolName - backup.Status.RemoteVolume = &corev1.Volume{ - Name: "backup-pvc", - } + backup.Status.PersistentVolumeClaimName = "backup-pvc" backup.Status.Phase = expectPhase + backup.Status.TotalSize = "1Gi" })).Should(Succeed()) } @@ -81,9 +83,8 @@ var _ = Describe("probe_utils", func() { Log: logger, } backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). - SetBackupType(dataprotectionv1alpha1.BackupTypeFull). + SetBackupType(dataprotectionv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() updateBackupStatus(backup, backupToolName, dataprotectionv1alpha1.BackupCompleted) component := &SynthesizedComponent{ @@ -107,7 +108,6 @@ var _ = Describe("probe_utils", func() { Log: logger, } backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyName). SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). Create(&testCtx).GetObject() @@ -117,6 +117,16 @@ var _ = Describe("probe_utils", func() { ObjectMeta: metav1.ObjectMeta{ Name: "data", }, + Spec: corev1.PersistentVolumeClaimSpec{ + AccessModes: []corev1.PersistentVolumeAccessMode{ + corev1.ReadWriteOnce, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceStorage: resource.MustParse("1Gi"), + }, + }, + }, }, }, } @@ -137,6 +147,10 @@ var _ = Describe("probe_utils", func() { Name: backupName, } Expect(reflect.DeepEqual(expectDataSource, vct.Spec.DataSource)).Should(BeTrue()) + + By("error if request storage is less than backup storage") + component.VolumeClaimTemplates[0].Spec.Resources.Requests[corev1.ResourceStorage] = resource.MustParse("512Mi") + Expect(BuildRestoredInfo(reqCtx, k8sClient, testCtx.DefaultNamespace, component, backupName)).Should(HaveOccurred()) }) }) diff --git a/internal/controller/component/suite_test.go b/internal/controller/component/suite_test.go index 429badf60..67cf9f7e3 100644 --- a/internal/controller/component/suite_test.go +++ b/internal/controller/component/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component diff --git a/internal/controller/component/type.go b/internal/controller/component/type.go index fbaaa91d6..f2596203a 100644 --- a/internal/controller/component/type.go +++ b/internal/controller/component/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package component @@ -25,19 +28,25 @@ import ( type MonitorConfig struct { Enable bool `json:"enable"` + BuiltIn bool `json:"builtIn"` ScrapePort int32 `json:"scrapePort,omitempty"` ScrapePath string `json:"scrapePath,omitempty"` } type SynthesizedComponent struct { ClusterDefName string `json:"clusterDefName,omitempty"` + ClusterName string `json:"clusterName,omitempty"` + ClusterUID string `json:"clusterUID,omitempty"` Name string `json:"name,omitempty"` - Type string `json:"type,omitempty"` + CompDefName string `json:"compDefName,omitempty"` CharacterType string `json:"characterType,omitempty"` - MaxUnavailable *intstr.IntOrString `json:"maxUnavailable,omitempty"` + MinAvailable *intstr.IntOrString `json:"minAvailable,omitempty"` Replicas int32 `json:"replicas"` WorkloadType v1alpha1.WorkloadType `json:"workloadType,omitempty"` + StatelessSpec *v1alpha1.StatelessSetSpec `json:"statelessSpec,omitempty"` + StatefulSpec *v1alpha1.StatefulSetSpec `json:"statefulSpec,omitempty"` ConsensusSpec *v1alpha1.ConsensusSetSpec `json:"consensusSpec,omitempty"` + ReplicationSpec *v1alpha1.ReplicationSetSpec `json:"replicationSpec,omitempty"` PrimaryIndex *int32 `json:"primaryIndex,omitempty"` PodSpec *corev1.PodSpec `json:"podSpec,omitempty"` Services []corev1.Service `json:"services,omitempty"` @@ -53,10 +62,13 @@ type SynthesizedComponent struct { Issuer *v1alpha1.Issuer `json:"issuer,omitempty"` VolumeTypes []v1alpha1.VolumeTypeSpec `json:"VolumeTypes,omitempty"` CustomLabelSpecs []v1alpha1.CustomLabelSpec `json:"customLabelSpecs,omitempty"` + ComponentDef string `json:"componentDef,omitempty"` + ServiceAccountName string `json:"serviceAccountName,omitempty"` + StatefulSetWorkload v1alpha1.StatefulSetWorkload } // GetPrimaryIndex provides PrimaryIndex value getter, if PrimaryIndex is -// a nil pointer it's treated at 0, return -1 if function receiver is nil. +// a nil pointer it's treated as 0, return -1 if function receiver is nil. func (r *SynthesizedComponent) GetPrimaryIndex() int32 { if r == nil { return -1 diff --git a/internal/controller/consensusset/enqueue_ancestor.go b/internal/controller/consensusset/enqueue_ancestor.go new file mode 100644 index 000000000..74ef2c205 --- /dev/null +++ b/internal/controller/consensusset/enqueue_ancestor.go @@ -0,0 +1,295 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "context" + "errors" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/util/workqueue" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + logf "sigs.k8s.io/controller-runtime/pkg/log" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/runtime/inject" + + roclient "github.com/apecloud/kubeblocks/internal/controller/client" + "github.com/apecloud/kubeblocks/internal/controller/model" +) + +var _ handler.EventHandler = &EnqueueRequestForAncestor{} + +var log = logf.FromContext(context.Background()).WithName("eventhandler").WithName("EnqueueRequestForAncestor") + +// EnqueueRequestForAncestor enqueues Requests for the ancestor object. +// E.g. the ancestor object creates the StatefulSet/Deployment which then creates the Pod. +// +// If a ConsensusSet creates Pods, users may reconcile the ConsensusSet in response to Pod Events using: +// +// - a source.Kind Source with Type of Pod. +// +// - a EnqueueRequestForAncestor EventHandler with an OwnerType of ConsensusSet and UpToLevel set to 2. +// +// If source kind is corev1.Event, Event.InvolvedObject will be used as the source kind +type EnqueueRequestForAncestor struct { + // Client used to get owner object of + Client roclient.ReadonlyClient + + // OwnerType is the type of the Owner object to look for in OwnerReferences. Only Group and Kind are compared. + OwnerType runtime.Object + + // find event source up to UpToLevel + UpToLevel int + + // groupKind is the cached Group and Kind from OwnerType + groupKind schema.GroupKind + + // mapper maps GroupVersionKinds to Resources + mapper meta.RESTMapper +} + +type empty struct{} + +// Create implements EventHandler. +func (e *EnqueueRequestForAncestor) Create(evt event.CreateEvent, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.getOwnerReconcileRequest(evt.Object, reqs) + for req := range reqs { + q.Add(req) + } +} + +// Update implements EventHandler. +func (e *EnqueueRequestForAncestor) Update(evt event.UpdateEvent, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.getOwnerReconcileRequest(evt.ObjectOld, reqs) + e.getOwnerReconcileRequest(evt.ObjectNew, reqs) + for req := range reqs { + q.Add(req) + } +} + +// Delete implements EventHandler. +func (e *EnqueueRequestForAncestor) Delete(evt event.DeleteEvent, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.getOwnerReconcileRequest(evt.Object, reqs) + for req := range reqs { + q.Add(req) + } +} + +// Generic implements EventHandler. +func (e *EnqueueRequestForAncestor) Generic(evt event.GenericEvent, q workqueue.RateLimitingInterface) { + reqs := map[reconcile.Request]empty{} + e.getOwnerReconcileRequest(evt.Object, reqs) + for req := range reqs { + q.Add(req) + } +} + +// parseOwnerTypeGroupKind parses the OwnerType into a Group and Kind and caches the result. Returns false +// if the OwnerType could not be parsed using the scheme. +func (e *EnqueueRequestForAncestor) parseOwnerTypeGroupKind(scheme *runtime.Scheme) error { + // Get the kinds of the type + kinds, _, err := scheme.ObjectKinds(e.OwnerType) + if err != nil { + log.Error(err, "Could not get ObjectKinds for OwnerType", "owner type", fmt.Sprintf("%T", e.OwnerType)) + return err + } + // Expect only 1 kind. If there is more than one kind this is probably an edge case such as ListOptions. + if len(kinds) != 1 { + err := fmt.Errorf("expected exactly 1 kind for OwnerType %T, but found %s kinds", e.OwnerType, kinds) + log.Error(nil, "expected exactly 1 kind for OwnerType", "owner type", fmt.Sprintf("%T", e.OwnerType), "kinds", kinds) + return err + } + // Cache the Group and Kind for the OwnerType + e.groupKind = schema.GroupKind{Group: kinds[0].Group, Kind: kinds[0].Kind} + return nil +} + +// getOwnerReconcileRequest looks at object and builds a map of reconcile.Request to reconcile +// owners of object that match e.OwnerType. +func (e *EnqueueRequestForAncestor) getOwnerReconcileRequest(obj client.Object, result map[reconcile.Request]empty) { + // get the object by the ownerRef + object, err := e.getSourceObject(obj) + if err != nil { + log.Info("could not find source object", "gvk", obj.GetObjectKind().GroupVersionKind(), "name", obj.GetName(), "error", err.Error()) + return + } + + // find the root object up to UpToLevel + scheme := *model.GetScheme() + ctx := context.Background() + ref, err := e.getOwnerUpTo(ctx, object, e.UpToLevel, scheme) + if err != nil { + log.Info("cloud not find top object", + "source object gvk", object.GetObjectKind().GroupVersionKind(), + "name", object.GetName(), + "up to level", e.UpToLevel, + "error", err.Error()) + return + } + if ref == nil { + log.Info("cloud not find top object", + "source object gvk", object.GetObjectKind().GroupVersionKind(), + "name", object.GetName(), + "up to level", e.UpToLevel) + return + } + + // Parse the Group out of the OwnerReference to compare it to what was parsed out of the requested OwnerType + refGV, err := schema.ParseGroupVersion(ref.APIVersion) + if err != nil { + log.Error(err, "Could not parse OwnerReference APIVersion", + "api version", ref.APIVersion) + return + } + + // Compare the OwnerReference Group and Kind against the OwnerType Group and Kind specified by the user. + // If the two match, create a Request for the objected referred to by + // the OwnerReference. Use the Name from the OwnerReference and the Namespace from the + // object in the event. + if ref.Kind == e.groupKind.Kind && refGV.Group == e.groupKind.Group { + // Match found - add a Request for the object referred to in the OwnerReference + request := reconcile.Request{NamespacedName: types.NamespacedName{ + Name: ref.Name, + }} + + // if owner is not namespaced then we should set the namespace to the empty + mapping, err := e.mapper.RESTMapping(e.groupKind, refGV.Version) + if err != nil { + log.Error(err, "Could not retrieve rest mapping", "kind", e.groupKind) + return + } + if mapping.Scope.Name() != meta.RESTScopeNameRoot { + request.Namespace = object.GetNamespace() + } + + result[request] = empty{} + } +} + +func (e *EnqueueRequestForAncestor) getSourceObject(object client.Object) (client.Object, error) { + eventObject, ok := object.(*corev1.Event) + // return the object directly if it's not corev1.Event kind + if !ok { + return object, nil + } + + objectRef := eventObject.InvolvedObject + scheme := *model.GetScheme() + // convert ObjectReference to OwnerReference + ownerRef := metav1.OwnerReference{ + APIVersion: objectRef.APIVersion, + Kind: objectRef.Kind, + Name: objectRef.Name, + UID: objectRef.UID, + } + + ctx := context.Background() + // get the object by the ownerRef + sourceObject, err := e.getObjectByOwnerRef(ctx, objectRef.Namespace, ownerRef, scheme) + if err != nil { + return nil, err + } + return sourceObject, nil +} + +// getOwnerUpTo gets the owner of object up to upToLevel. +// E.g. If ConsensusSet creates the StatefulSet which then creates the Pod, +// if the object is the Pod, then set upToLevel to 2 if you want to find the ConsensusSet. +// Each level of ownership should be a controller-relationship (i.e. controller=true in ownerReferences). +// nil return if no owner find in any level. +func (e *EnqueueRequestForAncestor) getOwnerUpTo(ctx context.Context, object client.Object, upToLevel int, scheme runtime.Scheme) (*metav1.OwnerReference, error) { + if upToLevel <= 0 { + return nil, nil + } + ownerRef := metav1.GetControllerOf(object) + if ownerRef == nil { + return nil, nil + } + if upToLevel == 1 { + return ownerRef, nil + } + objectNew, err := e.getObjectByOwnerRef(ctx, object.GetNamespace(), *ownerRef, scheme) + if err != nil { + return nil, err + } + return e.getOwnerUpTo(ctx, objectNew, upToLevel-1, scheme) +} + +func (e *EnqueueRequestForAncestor) getObjectByOwnerRef(ctx context.Context, ownerNameSpace string, ownerRef metav1.OwnerReference, scheme runtime.Scheme) (client.Object, error) { + gv, err := schema.ParseGroupVersion(ownerRef.APIVersion) + if err != nil { + return nil, err + } + gvk := schema.GroupVersionKind{ + Group: gv.Group, + Version: gv.Version, + Kind: ownerRef.Kind, + } + objectRT, err := scheme.New(gvk) + if err != nil { + return nil, err + } + object, ok := objectRT.(client.Object) + if !ok { + return nil, errors.New("runtime object can't be converted to client object") + } + request := reconcile.Request{NamespacedName: types.NamespacedName{ + Name: ownerRef.Name, + }} + // if owner is not namespaced then we should set the namespace to the empty + groupKind := schema.GroupKind{Group: gvk.Group, Kind: gvk.Kind} + mapping, err := e.mapper.RESTMapping(groupKind, gvk.Version) + if err != nil { + return nil, err + } + if mapping.Scope.Name() != meta.RESTScopeNameRoot { + request.Namespace = ownerNameSpace + } + if err := e.Client.Get(ctx, request.NamespacedName, object); err != nil { + return nil, err + } + return object, nil +} + +var _ inject.Scheme = &EnqueueRequestForAncestor{} + +// InjectScheme is called by the Controller to provide a singleton scheme to the EnqueueRequestForAncestor. +func (e *EnqueueRequestForAncestor) InjectScheme(s *runtime.Scheme) error { + return e.parseOwnerTypeGroupKind(s) +} + +var _ inject.Mapper = &EnqueueRequestForAncestor{} + +// InjectMapper is called by the Controller to provide the rest mapper used by the manager. +func (e *EnqueueRequestForAncestor) InjectMapper(m meta.RESTMapper) error { + e.mapper = m + return nil +} diff --git a/internal/controller/consensusset/plan_builder.go b/internal/controller/consensusset/plan_builder.go new file mode 100644 index 000000000..a2e4bb141 --- /dev/null +++ b/internal/controller/consensusset/plan_builder.go @@ -0,0 +1,212 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "errors" + "fmt" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type csSetPlanBuilder struct { + req ctrl.Request + cli client.Client + transCtx *CSSetTransformContext + transformers graph.TransformerChain +} + +type csSetPlan struct { + dag *graph.DAG + walkFunc graph.WalkFunc + cli client.Client + transCtx *CSSetTransformContext +} + +func init() { + model.AddScheme(workloads.AddToScheme) +} + +// PlanBuilder implementation + +func (b *csSetPlanBuilder) Init() error { + csSet := &workloads.ConsensusSet{} + if err := b.cli.Get(b.transCtx.Context, b.req.NamespacedName, csSet); err != nil { + return err + } + b.AddTransformer(&initTransformer{ConsensusSet: csSet}) + return nil +} + +func (b *csSetPlanBuilder) AddTransformer(transformer ...graph.Transformer) graph.PlanBuilder { + b.transformers = append(b.transformers, transformer...) + return b +} + +func (b *csSetPlanBuilder) AddParallelTransformer(transformer ...graph.Transformer) graph.PlanBuilder { + b.transformers = append(b.transformers, &model.ParallelTransformer{Transformers: transformer}) + return b +} + +func (b *csSetPlanBuilder) Build() (graph.Plan, error) { + var err error + // new a DAG and apply chain on it, after that we should get the final Plan + dag := graph.NewDAG() + err = b.transformers.ApplyTo(b.transCtx, dag) + // log for debug + b.transCtx.Logger.Info(fmt.Sprintf("DAG: %s", dag)) + + // we got the execution plan + plan := &csSetPlan{ + dag: dag, + walkFunc: b.csSetWalkFunc, + cli: b.cli, + transCtx: b.transCtx, + } + return plan, err +} + +// Plan implementation + +func (p *csSetPlan) Execute() error { + return p.dag.WalkReverseTopoOrder(p.walkFunc) +} + +// Do the real works + +func (b *csSetPlanBuilder) csSetWalkFunc(v graph.Vertex) error { + vertex, ok := v.(*model.ObjectVertex) + if !ok { + return fmt.Errorf("wrong vertex type %v", v) + } + if vertex.Action == nil { + return errors.New("vertex action can't be nil") + } + if vertex.Immutable { + return nil + } + switch *vertex.Action { + case model.CREATE: + err := b.cli.Create(b.transCtx.Context, vertex.Obj) + if err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + case model.UPDATE: + o, err := b.buildUpdateObj(vertex) + if err != nil { + return err + } + err = b.cli.Update(b.transCtx.Context, o) + if err != nil && !apierrors.IsNotFound(err) { + b.transCtx.Logger.Error(err, fmt.Sprintf("update %T error: %s", o, vertex.OriObj.GetName())) + return err + } + case model.DELETE: + if controllerutil.RemoveFinalizer(vertex.Obj, csSetFinalizerName) { + err := b.cli.Update(b.transCtx.Context, vertex.Obj) + if err != nil && !apierrors.IsNotFound(err) { + b.transCtx.Logger.Error(err, fmt.Sprintf("delete %T error: %s", vertex.Obj, vertex.Obj.GetName())) + return err + } + } + if !model.IsObjectDeleting(vertex.Obj) { + err := b.cli.Delete(b.transCtx.Context, vertex.Obj) + if err != nil && !apierrors.IsNotFound(err) { + return err + } + } + case model.STATUS: + patch := client.MergeFrom(vertex.OriObj) + if err := b.cli.Status().Patch(b.transCtx.Context, vertex.Obj, patch); err != nil { + return err + } + } + return nil +} + +func (b *csSetPlanBuilder) buildUpdateObj(vertex *model.ObjectVertex) (client.Object, error) { + handleSts := func(origObj, targetObj *appsv1.StatefulSet) (client.Object, error) { + origObj.Spec.Template = targetObj.Spec.Template + origObj.Spec.Replicas = targetObj.Spec.Replicas + origObj.Spec.UpdateStrategy = targetObj.Spec.UpdateStrategy + return origObj, nil + } + + handleDeploy := func(origObj, targetObj *appsv1.Deployment) (client.Object, error) { + origObj.Spec = targetObj.Spec + return origObj, nil + } + + handleSvc := func(origObj, targetObj *corev1.Service) (client.Object, error) { + origObj.Spec = targetObj.Spec + return origObj, nil + } + + handlePVC := func(origObj, targetObj *corev1.PersistentVolumeClaim) (client.Object, error) { + if origObj.Spec.Resources.Requests[corev1.ResourceStorage] == targetObj.Spec.Resources.Requests[corev1.ResourceStorage] { + return origObj, nil + } + origObj.Spec.Resources.Requests[corev1.ResourceStorage] = targetObj.Spec.Resources.Requests[corev1.ResourceStorage] + return origObj, nil + } + + origObj := vertex.OriObj.DeepCopyObject() + switch v := vertex.Obj.(type) { + case *appsv1.StatefulSet: + return handleSts(origObj.(*appsv1.StatefulSet), v) + case *appsv1.Deployment: + return handleDeploy(origObj.(*appsv1.Deployment), v) + case *corev1.Service: + return handleSvc(origObj.(*corev1.Service), v) + case *corev1.PersistentVolumeClaim: + return handlePVC(origObj.(*corev1.PersistentVolumeClaim), v) + case *corev1.Secret, *corev1.ConfigMap: + return v, nil + } + + return vertex.Obj, nil +} + +// NewCSSetPlanBuilder returns a csSetPlanBuilder powered PlanBuilder +func NewCSSetPlanBuilder(ctx intctrlutil.RequestCtx, cli client.Client, req ctrl.Request) graph.PlanBuilder { + return &csSetPlanBuilder{ + req: req, + cli: cli, + transCtx: &CSSetTransformContext{ + Context: ctx.Ctx, + Client: cli, + EventRecorder: ctx.Recorder, + Logger: ctx.Log, + }, + } +} + +var _ graph.PlanBuilder = &csSetPlanBuilder{} +var _ graph.Plan = &csSetPlan{} diff --git a/internal/controller/consensusset/pod_role_event_handler.go b/internal/controller/consensusset/pod_role_event_handler.go new file mode 100644 index 000000000..10af81e28 --- /dev/null +++ b/internal/controller/consensusset/pod_role_event_handler.go @@ -0,0 +1,147 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "encoding/json" + "fmt" + "regexp" + "strings" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" + "sigs.k8s.io/controller-runtime/pkg/client" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// TODO(free6om): dedup copied funcs from event_controllers.go +// TODO(free6om): refactor event_controller.go as it should NOT import controllers/apps/component/* + +type PodRoleEventHandler struct{} + +// probeEventType defines the type of probe event. +type probeEventType string + +type probeMessage struct { + Event probeEventType `json:"event,omitempty"` + Message string `json:"message,omitempty"` + OriginalRole string `json:"originalRole,omitempty"` + Role string `json:"role,omitempty"` +} + +const ( + // roleChangedAnnotKey is used to mark the role change event has been handled. + roleChangedAnnotKey = "role.kubeblocks.io/event-handled" +) + +const ( + probeEventOperationNotImpl probeEventType = "OperationNotImplemented" + probeEventCheckRoleFailed probeEventType = "checkRoleFailed" + probeEventRoleInvalid probeEventType = "roleInvalid" +) + +func (h *PodRoleEventHandler) Handle(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) error { + if event.InvolvedObject.FieldPath != roleObservationEventFieldPath { + return nil + } + var ( + err error + annotations = event.GetAnnotations() + ) + // filter role changed event that has been handled + count := fmt.Sprintf("count-%d", event.Count) + if annotations != nil && annotations[roleChangedAnnotKey] == count { + return nil + } + + if _, err = handleRoleChangedEvent(cli, reqCtx, recorder, event); err != nil { + return err + } + + // event order is crucial in role probing, but it's not guaranteed when controller restarted, so we have to mark them to be filtered + patch := client.MergeFrom(event.DeepCopy()) + if event.Annotations == nil { + event.Annotations = make(map[string]string, 0) + } + event.Annotations[roleChangedAnnotKey] = count + return cli.Patch(reqCtx.Ctx, event, patch) +} + +// handleRoleChangedEvent handles role changed event and return role. +func handleRoleChangedEvent(cli client.Client, reqCtx intctrlutil.RequestCtx, recorder record.EventRecorder, event *corev1.Event) (string, error) { + // parse probe event message + message := parseProbeEventMessage(reqCtx, event) + if message == nil { + reqCtx.Log.Info("parse probe event message failed", "message", event.Message) + return "", nil + } + + // if probe event operation is not impl, check role failed or role invalid, ignore it + if message.Event == probeEventOperationNotImpl || message.Event == probeEventCheckRoleFailed || message.Event == probeEventRoleInvalid { + reqCtx.Log.Info("probe event failed", "message", message.Message) + return "", nil + } + role := strings.ToLower(message.Role) + + podName := types.NamespacedName{ + Namespace: event.InvolvedObject.Namespace, + Name: event.InvolvedObject.Name, + } + // get pod + pod := &corev1.Pod{} + if err := cli.Get(reqCtx.Ctx, podName, pod); err != nil { + return role, err + } + // event belongs to old pod with the same name, ignore it + if pod.UID != event.InvolvedObject.UID { + return role, nil + } + name := pod.Labels[constant.AppInstanceLabelKey] + csSet := &workloads.ConsensusSet{} + if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: pod.Namespace, Name: name}, csSet); err != nil { + return "", err + } + reqCtx.Log.V(1).Info("handle role change event", "pod", pod.Name, "role", role, "originalRole", message.OriginalRole) + + return role, updatePodRoleLabel(cli, reqCtx, *csSet, pod, role) +} + +// parseProbeEventMessage parses probe event message. +func parseProbeEventMessage(reqCtx intctrlutil.RequestCtx, event *corev1.Event) *probeMessage { + message := &probeMessage{} + re := regexp.MustCompile(`Readiness probe failed: ({.*})`) + matches := re.FindStringSubmatch(event.Message) + if len(matches) != 2 { + reqCtx.Log.Info("parser Readiness probe event message failed", "message", event.Message) + return nil + } + msg := matches[1] + err := json.Unmarshal([]byte(msg), message) + if err != nil { + // not role related message, ignore it + reqCtx.Log.Info("not role message", "message", event.Message, "error", err) + return nil + } + return message +} diff --git a/internal/controller/consensusset/suite_test.go b/internal/controller/consensusset/suite_test.go new file mode 100644 index 000000000..a8420312a --- /dev/null +++ b/internal/controller/consensusset/suite_test.go @@ -0,0 +1,48 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +func init() { +} + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "ConsensusSet Suite") +} + +var _ = BeforeSuite(func() { + go func() { + defer GinkgoRecover() + }() +}) + +var _ = AfterSuite(func() { +}) diff --git a/internal/controller/consensusset/transformer_deletion.go b/internal/controller/consensusset/transformer_deletion.go new file mode 100644 index 000000000..57f09fc8d --- /dev/null +++ b/internal/controller/consensusset/transformer_deletion.go @@ -0,0 +1,59 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// CSSetDeletionTransformer handles ConsensusSet deletion +type CSSetDeletionTransformer struct{} + +func (t *CSSetDeletionTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + obj := transCtx.CSSet + if !model.IsObjectDeleting(obj) { + return nil + } + + // list all objects owned by this primary obj in cache, and delete them all + // there is chance that objects leak occurs because of cache stale + // ignore the problem currently + // TODO: GC the leaked objects + ml := client.MatchingLabels{model.AppInstanceLabelKey: obj.Name} + snapshot, err := model.ReadCacheSnapshot(transCtx, obj, ml, deletionKinds()...) + if err != nil { + return err + } + for _, object := range snapshot { + model.PrepareDelete(dag, object) + } + + if err := model.PrepareRootDelete(dag); err != nil { + return err + } + + // fast return, that is stopping the plan.Build() stage and jump to plan.Execute() directly + return graph.ErrPrematureStop +} + +var _ graph.Transformer = &CSSetDeletionTransformer{} diff --git a/internal/controller/consensusset/transformer_fix_meta.go b/internal/controller/consensusset/transformer_fix_meta.go new file mode 100644 index 000000000..48df967f2 --- /dev/null +++ b/internal/controller/consensusset/transformer_fix_meta.go @@ -0,0 +1,52 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" +) + +type FixMetaTransformer struct{} + +func (t *FixMetaTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + csSet := transCtx.CSSet + if model.IsObjectDeleting(csSet) { + return nil + } + + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if controllerutil.ContainsFinalizer(csSet, csSetFinalizerName) { + return nil + } + controllerutil.AddFinalizer(csSet, csSetFinalizerName) + if err := model.PrepareRootUpdate(dag); err != nil { + return err + } + + return graph.ErrPrematureStop +} + +var _ graph.Transformer = &FixMetaTransformer{} diff --git a/internal/controller/consensusset/transformer_init.go b/internal/controller/consensusset/transformer_init.go new file mode 100644 index 000000000..8bf93b1ae --- /dev/null +++ b/internal/controller/consensusset/transformer_init.go @@ -0,0 +1,42 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" +) + +type initTransformer struct { + *workloads.ConsensusSet +} + +func (t *initTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + // init context + transCtx, _ := ctx.(*CSSetTransformContext) + transCtx.CSSet, transCtx.OrigCSSet = t.ConsensusSet, t.ConsensusSet.DeepCopy() + + // init dag + model.PrepareStatus(dag, transCtx.OrigCSSet, transCtx.CSSet) + return nil +} + +var _ graph.Transformer = &initTransformer{} diff --git a/internal/controller/consensusset/transformer_member_reconfiguration.go b/internal/controller/consensusset/transformer_member_reconfiguration.go new file mode 100644 index 000000000..ef8fd2564 --- /dev/null +++ b/internal/controller/consensusset/transformer_member_reconfiguration.go @@ -0,0 +1,438 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "fmt" + "regexp" + "strconv" + + "github.com/go-logr/logr" + apps "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" +) + +// MemberReconfigurationTransformer handles member reconfiguration +type MemberReconfigurationTransformer struct{} + +type actionInfo struct { + shortActionName string + ordinal int + actionType string +} + +type conditionChecker = func() bool + +var actionNameRegex = regexp.MustCompile(`(.*)-([0-9]+)-([0-9]+)-([a-zA-Z\-]+)$`) + +func (t *MemberReconfigurationTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + if model.IsObjectDeleting(transCtx.CSSet) { + return nil + } + csSet := transCtx.CSSet + + // get the underlying sts + stsVertex, err := getUnderlyingStsVertex(dag) + if err != nil { + return err + } + + // handle cluster initialization + // set initReplicas at creation + if csSet.Status.InitReplicas == 0 { + csSet.Status.InitReplicas = csSet.Spec.Replicas + return nil + } + // update readyInitReplicas + if csSet.Status.ReadyInitReplicas < csSet.Status.InitReplicas { + csSet.Status.ReadyInitReplicas = int32(len(csSet.Status.MembersStatus)) + } + // return if cluster initialization not done + if csSet.Status.ReadyInitReplicas != csSet.Status.InitReplicas { + return nil + } + + // cluster initialization done, handle dynamic membership reconfiguration + + // consensus cluster is ready + if isConsensusSetReady(csSet) { + return cleanAction(transCtx, dag) + } + + if !shouldHaveActions(csSet) { + return nil + } + + // no enough replicas in scale out, tell sts to create them. + sts, _ := stsVertex.OriObj.(*apps.StatefulSet) + memberReadyReplicas := int32(len(csSet.Status.MembersStatus)) + if memberReadyReplicas < csSet.Spec.Replicas && + sts.Status.ReadyReplicas < csSet.Spec.Replicas { + return nil + } + + stsVertex.Immutable = true + + // barrier: the underlying sts is ready and has enough replicas + if sts.Status.ReadyReplicas < csSet.Spec.Replicas || !isStatefulSetReady(sts) { + return nil + } + + // get last action + actionList, err := getActionList(transCtx, jobScenarioMembership) + if err != nil { + return err + } + + // if no action, create the first one + if len(actionList) == 0 { + return createNextAction(transCtx, dag, csSet, nil) + } + + // got action, there should be only one action + action := actionList[0] + switch { + case action.Status.Succeeded > 0: + // wait action's result: + // e.g. action with ordinal 3 and type member-join, wait member 3 until it appears in status.membersStatus + if !isActionDone(csSet, action) { + return nil + } + // mark it as 'handled' + deleteAction(dag, action) + return createNextAction(transCtx, dag, csSet, action) + case action.Status.Failed > 0: + emitEvent(transCtx, action) + if !isSwitchoverAction(action) { + // need manual handling + return nil + } + return createNextAction(transCtx, dag, csSet, action) + default: + // action in progress + return nil + } +} + +// consensus_set level 'ready' state: +// 1. all replicas exist +// 2. all members have role set +func isConsensusSetReady(csSet *workloads.ConsensusSet) bool { + membersStatus := csSet.Status.MembersStatus + if len(membersStatus) != int(csSet.Spec.Replicas) { + return false + } + for i := 0; i < int(csSet.Spec.Replicas); i++ { + podName := getPodName(csSet.Name, i) + if !isMemberReady(podName, membersStatus) { + return false + } + } + return true +} + +func isStatefulSetReady(sts *apps.StatefulSet) bool { + if sts == nil { + return false + } + if sts.Status.ObservedGeneration == sts.Generation && + sts.Status.Replicas == *sts.Spec.Replicas && + sts.Status.ReadyReplicas == sts.Status.Replicas { + return true + } + return false +} + +func isMemberReady(podName string, membersStatus []workloads.ConsensusMemberStatus) bool { + for _, memberStatus := range membersStatus { + if memberStatus.PodName == podName { + return true + } + } + return false +} + +func cleanAction(transCtx *CSSetTransformContext, dag *graph.DAG) error { + actionList, err := getActionList(transCtx, jobScenarioMembership) + if err != nil { + return err + } + if len(actionList) == 0 { + return nil + } + action := actionList[0] + switch { + case action.Status.Succeeded > 0: + deleteAction(dag, action) + case action.Status.Failed > 0: + emitEvent(transCtx, action) + } + return nil +} + +func isActionDone(csSet *workloads.ConsensusSet, action *batchv1.Job) bool { + ordinal, _ := getActionOrdinal(action.Name) + podName := getPodName(csSet.Name, ordinal) + membersStatus := csSet.Status.MembersStatus + switch action.Labels[jobTypeLabel] { + case jobTypeSwitchover: + leader := getLeaderPodName(csSet.Status.MembersStatus) + return podName != leader + case jobTypeMemberLeaveNotifying: + return !isMemberReady(podName, membersStatus) + case jobTypeMemberJoinNotifying: + return isMemberReady(podName, membersStatus) + case jobTypeLogSync, jobTypePromote: + // no info, ignore them + } + return true +} + +func isSwitchoverAction(action *batchv1.Job) bool { + return action.Labels[jobTypeLabel] == jobTypeSwitchover +} + +func deleteAction(dag *graph.DAG, action *batchv1.Job) { + doActionCleanup(dag, action) +} + +func createNextAction(transCtx *CSSetTransformContext, dag *graph.DAG, csSet *workloads.ConsensusSet, currentAction *batchv1.Job) error { + actionInfoList := generateActionInfoList(csSet) + + if len(actionInfoList) == 0 { + return nil + } + + var nextActionInfo *actionInfo + switch { + case currentAction == nil, isSwitchoverAction(currentAction): + nextActionInfo = actionInfoList[0] + default: + nextActionInfo = nil + ordinal, _ := getActionOrdinal(currentAction.Name) + shortName := buildShortActionName(csSet.Name, ordinal, currentAction.Labels[jobTypeLabel]) + for i := 0; i < len(actionInfoList); i++ { + if actionInfoList[i].shortActionName != shortName { + continue + } + if i+1 < len(actionInfoList) { + nextActionInfo = actionInfoList[i+1] + break + } + } + } + + if nextActionInfo == nil { + return nil + } + + leader := getLeaderPodName(csSet.Status.MembersStatus) + ordinal := nextActionInfo.ordinal + if nextActionInfo.actionType == jobTypeSwitchover { + ordinal = 0 + } + target := getPodName(csSet.Name, ordinal) + actionName := getActionName(csSet.Name, int(csSet.Generation), nextActionInfo.ordinal, nextActionInfo.actionType) + nextAction := buildAction(csSet, actionName, nextActionInfo.actionType, jobScenarioMembership, leader, target) + + if err := abnormalAnalysis(csSet, nextAction); err != nil { + emitAbnormalEvent(transCtx, nextActionInfo.actionType, actionName, err) + return err + } + + return createAction(dag, csSet, nextAction) +} + +func generateActionInfoList(csSet *workloads.ConsensusSet) []*actionInfo { + var actionInfoList []*actionInfo + memberReadyReplicas := int32(len(csSet.Status.MembersStatus)) + + switch { + case memberReadyReplicas < csSet.Spec.Replicas: + // member join + // members with ordinal less than 'spec.replicas' should in the consensus cluster + actionTypeList := []string{jobTypeMemberJoinNotifying, jobTypeLogSync, jobTypePromote} + for i := memberReadyReplicas; i < csSet.Spec.Replicas; i++ { + actionInfos := generateActionInfos(csSet, int(i), actionTypeList) + actionInfoList = append(actionInfoList, actionInfos...) + } + case memberReadyReplicas > csSet.Spec.Replicas: + // member leave + // members with ordinal greater than 'spec.replicas - 1' should not in the consensus cluster + actionTypeList := []string{jobTypeSwitchover, jobTypeMemberLeaveNotifying} + for i := memberReadyReplicas - 1; i >= csSet.Spec.Replicas; i-- { + actionInfos := generateActionInfos(csSet, int(i), actionTypeList) + actionInfoList = append(actionInfoList, actionInfos...) + } + } + + return actionInfoList +} + +// TODO(free6om): remove all printActionList when all testes pass +func printActionList(logger logr.Logger, actionList []*batchv1.Job) { + var actionNameList []string + for _, action := range actionList { + actionNameList = append(actionNameList, fmt.Sprintf("%s-%v", action.Name, *action.Spec.Suspend)) + } + logger.Info(fmt.Sprintf("action list: %v\n", actionNameList)) +} + +func isPreAction(actionType string) bool { + return actionType == jobTypeSwitchover || actionType == jobTypeMemberLeaveNotifying +} + +func shouldHaveActions(csSet *workloads.ConsensusSet) bool { + currentReplicas := len(csSet.Status.MembersStatus) + expectedReplicas := int(csSet.Spec.Replicas) + + var actionTypeList []string + switch { + case currentReplicas > expectedReplicas: + actionTypeList = []string{jobTypeSwitchover, jobTypeMemberLeaveNotifying} + case currentReplicas < expectedReplicas: + actionTypeList = []string{jobTypeMemberJoinNotifying, jobTypeLogSync, jobTypePromote} + } + for _, actionType := range actionTypeList { + if shouldCreateAction(csSet, actionType, nil) { + return true + } + } + return false +} + +func shouldCreateAction(csSet *workloads.ConsensusSet, actionType string, checker conditionChecker) bool { + if checker != nil && !checker() { + return false + } + reconfiguration := csSet.Spec.MembershipReconfiguration + if reconfiguration == nil { + return false + } + switch actionType { + case jobTypeSwitchover: + return reconfiguration.SwitchoverAction != nil + case jobTypeMemberJoinNotifying: + return reconfiguration.MemberJoinAction != nil + case jobTypeMemberLeaveNotifying: + return reconfiguration.MemberLeaveAction != nil + case jobTypeLogSync: + return reconfiguration.LogSyncAction != nil + case jobTypePromote: + return reconfiguration.PromoteAction != nil + } + return false +} + +func buildShortActionName(parent string, ordinal int, actionType string) string { + return fmt.Sprintf("%s-%d-%s", parent, ordinal, actionType) +} + +func getActionOrdinal(actionName string) (int, error) { + subMatches := actionNameRegex.FindStringSubmatch(actionName) + if len(subMatches) < 5 { + return 0, fmt.Errorf("error actionName: %s", actionName) + } + return strconv.Atoi(subMatches[3]) +} + +func getUnderlyingStsVertex(dag *graph.DAG) (*model.ObjectVertex, error) { + vertices := model.FindAll[*apps.StatefulSet](dag) + if len(vertices) != 1 { + return nil, fmt.Errorf("unexpected sts found, expected 1, but found: %d", len(vertices)) + } + stsVertex, _ := vertices[0].(*model.ObjectVertex) + return stsVertex, nil +} + +// all members with ordinal less than action target pod should be in a good consensus state: +// 1. they should be in membersStatus +// 2. they should have a leader +func abnormalAnalysis(csSet *workloads.ConsensusSet, action *batchv1.Job) error { + membersStatus := csSet.Status.MembersStatus + statusMap := make(map[string]workloads.ConsensusMemberStatus, len(membersStatus)) + for _, status := range membersStatus { + statusMap[status.PodName] = status + } + ordinal, _ := getActionOrdinal(action.Name) + currentMembers := ordinal + if isPreAction(action.Labels[jobTypeLabel]) { + currentMembers = ordinal + 1 + } + var abnormalPodList, leaderPodList []string + for i := 0; i < currentMembers; i++ { + podName := getPodName(csSet.Name, i) + status, ok := statusMap[podName] + if !ok { + abnormalPodList = append(abnormalPodList, podName) + } + if status.IsLeader { + leaderPodList = append(leaderPodList, podName) + } + } + + var message string + if len(abnormalPodList) > 0 { + message = fmt.Sprintf("abnormal pods: %v", abnormalPodList) + } + switch len(leaderPodList) { + case 0: + message = fmt.Sprintf("%s, no leader exists", message) + case 1: + default: + message = fmt.Sprintf("%s, too many leaders: %v", message, leaderPodList) + } + if len(message) > 0 { + return fmt.Errorf("cluster unhealthy: %s", message) + } + + return nil +} + +func generateActionInfos(csSet *workloads.ConsensusSet, ordinal int, actionTypeList []string) []*actionInfo { + var actionInfos []*actionInfo + leaderPodName := getLeaderPodName(csSet.Status.MembersStatus) + podName := getPodName(csSet.Name, ordinal) + for _, actionType := range actionTypeList { + checker := func() bool { + return podName == leaderPodName + } + if actionType != jobTypeSwitchover { + checker = nil + } + if !shouldCreateAction(csSet, actionType, checker) { + continue + } + info := &actionInfo{ + shortActionName: buildShortActionName(csSet.Name, ordinal, actionType), + ordinal: ordinal, + actionType: actionType, + } + actionInfos = append(actionInfos, info) + } + return actionInfos +} + +var _ graph.Transformer = &MemberReconfigurationTransformer{} diff --git a/internal/controller/consensusset/transformer_object_generation.go b/internal/controller/consensusset/transformer_object_generation.go new file mode 100644 index 000000000..b3974375c --- /dev/null +++ b/internal/controller/consensusset/transformer_object_generation.go @@ -0,0 +1,464 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "encoding/json" + "fmt" + "strconv" + "strings" + + "github.com/spf13/viper" + apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type ObjectGenerationTransformer struct{} + +func (t *ObjectGenerationTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + csSet := transCtx.CSSet + oriSet := transCtx.OrigCSSet + + if model.IsObjectDeleting(oriSet) { + return nil + } + + // generate objects by current spec + svc := buildSvc(*csSet) + headLessSvc := buildHeadlessSvc(*csSet) + envConfig := buildEnvConfigMap(*csSet) + sts := buildSts(*csSet, headLessSvc.Name, *envConfig) + objects := []client.Object{svc, headLessSvc, envConfig, sts} + + for _, object := range objects { + if err := controllerutil.SetOwnership(csSet, object, model.GetScheme(), csSetFinalizerName); err != nil { + return err + } + } + + // read cache snapshot + ml := client.MatchingLabels{model.AppInstanceLabelKey: csSet.Name, model.KBManagedByKey: kindConsensusSet} + oldSnapshot, err := model.ReadCacheSnapshot(ctx, csSet, ml, ownedKinds()...) + if err != nil { + return err + } + + // compute create/update/delete set + newSnapshot := make(map[model.GVKName]client.Object) + for _, object := range objects { + name, err := model.GetGVKName(object) + if err != nil { + return err + } + newSnapshot[*name] = object + } + + // now compute the diff between old and target snapshot and generate the plan + oldNameSet := sets.KeySet(oldSnapshot) + newNameSet := sets.KeySet(newSnapshot) + + createSet := newNameSet.Difference(oldNameSet) + updateSet := newNameSet.Intersection(oldNameSet) + deleteSet := oldNameSet.Difference(newNameSet) + + createNewObjects := func() { + for name := range createSet { + model.PrepareCreate(dag, newSnapshot[name]) + } + } + updateObjects := func() { + for name := range updateSet { + model.PrepareUpdate(dag, oldSnapshot[name], newSnapshot[name]) + } + } + deleteOrphanObjects := func() { + for name := range deleteSet { + model.PrepareDelete(dag, oldSnapshot[name]) + } + } + + handleDependencies := func() { + model.DependOn(dag, sts, svc, headLessSvc, envConfig) + } + + // objects to be created + createNewObjects() + // objects to be updated + updateObjects() + // objects to be deleted + deleteOrphanObjects() + // handle object dependencies + handleDependencies() + + return nil +} + +func buildSvc(csSet workloads.ConsensusSet) *corev1.Service { + svcBuilder := builder.NewServiceBuilder(csSet.Namespace, csSet.Name). + AddLabels(model.AppInstanceLabelKey, csSet.Name). + AddLabels(model.KBManagedByKey, kindConsensusSet). + // AddAnnotationsInMap(csSet.Annotations). + AddSelectors(model.AppInstanceLabelKey, csSet.Name). + AddSelectors(model.KBManagedByKey, kindConsensusSet). + AddPorts(csSet.Spec.Service.Ports...). + SetType(csSet.Spec.Service.Type) + for _, role := range csSet.Spec.Roles { + if role.IsLeader && len(role.Name) > 0 { + svcBuilder.AddSelectors(model.ConsensusSetAccessModeLabelKey, string(role.AccessMode)) + } + } + return svcBuilder.GetObject() +} + +func buildHeadlessSvc(csSet workloads.ConsensusSet) *corev1.Service { + hdlBuilder := builder.NewHeadlessServiceBuilder(csSet.Namespace, getHeadlessSvcName(csSet)). + AddLabels(model.AppInstanceLabelKey, csSet.Name). + AddLabels(model.KBManagedByKey, kindConsensusSet). + AddSelectors(model.AppInstanceLabelKey, csSet.Name). + AddSelectors(model.KBManagedByKey, kindConsensusSet) + // .AddAnnotations("prometheus.io/scrape", strconv.FormatBool(component.Monitor.Enable)) + // if component.Monitor.Enable { + // hdBuilder.AddAnnotations("prometheus.io/path", component.Monitor.ScrapePath). + // AddAnnotations("prometheus.io/port", strconv.Itoa(int(component.Monitor.ScrapePort))). + // AddAnnotations("prometheus.io/scheme", "http") + // } + for _, container := range csSet.Spec.Template.Spec.Containers { + for _, port := range container.Ports { + servicePort := corev1.ServicePort{ + Protocol: port.Protocol, + Port: port.ContainerPort, + } + switch { + case len(port.Name) > 0: + servicePort.Name = port.Name + servicePort.TargetPort = intstr.FromString(port.Name) + default: + servicePort.Name = fmt.Sprintf("%s-%d", strings.ToLower(string(port.Protocol)), port.ContainerPort) + servicePort.TargetPort = intstr.FromInt(int(port.ContainerPort)) + } + hdlBuilder.AddPorts(servicePort) + } + } + return hdlBuilder.GetObject() +} + +func buildSts(csSet workloads.ConsensusSet, headlessSvcName string, envConfig corev1.ConfigMap) *apps.StatefulSet { + stsBuilder := builder.NewStatefulSetBuilder(csSet.Namespace, csSet.Name) + template := buildStsPodTemplate(csSet, envConfig) + stsBuilder.AddLabels(model.AppInstanceLabelKey, csSet.Name). + AddLabels(model.KBManagedByKey, kindConsensusSet). + AddMatchLabel(model.AppInstanceLabelKey, csSet.Name). + AddMatchLabel(model.KBManagedByKey, kindConsensusSet). + SetServiceName(headlessSvcName). + SetReplicas(csSet.Spec.Replicas). + SetPodManagementPolicy(apps.OrderedReadyPodManagement). + SetVolumeClaimTemplates(csSet.Spec.VolumeClaimTemplates...). + SetTemplate(*template). + SetUpdateStrategyType(apps.OnDeleteStatefulSetStrategyType) + return stsBuilder.GetObject() +} + +func buildEnvConfigMap(csSet workloads.ConsensusSet) *corev1.ConfigMap { + envData := buildEnvConfigData(csSet) + return builder.NewConfigMapBuilder(csSet.Namespace, csSet.Name+"-env"). + AddLabels(model.AppInstanceLabelKey, csSet.Name). + AddLabels(model.KBManagedByKey, kindConsensusSet). + SetData(envData).GetObject() +} + +func buildStsPodTemplate(csSet workloads.ConsensusSet, envConfig corev1.ConfigMap) *corev1.PodTemplateSpec { + template := csSet.Spec.Template + labels := template.Labels + if labels == nil { + labels = make(map[string]string, 2) + } + labels[model.AppInstanceLabelKey] = csSet.Name + labels[model.KBManagedByKey] = kindConsensusSet + template.Labels = labels + + // inject env ConfigMap into workload pods only + for i := range template.Spec.Containers { + template.Spec.Containers[i].EnvFrom = append(template.Spec.Containers[i].EnvFrom, + corev1.EnvFromSource{ + ConfigMapRef: &corev1.ConfigMapEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: envConfig.Name, + }, + Optional: func() *bool { optional := false; return &optional }(), + }}) + } + + injectRoleObservationContainer(csSet, &template) + + return &template +} + +func injectRoleObservationContainer(csSet workloads.ConsensusSet, template *corev1.PodTemplateSpec) { + roleObservation := csSet.Spec.RoleObservation + credential := csSet.Spec.Credential + credentialEnv := make([]corev1.EnvVar, 0) + if credential != nil { + credentialEnv = append(credentialEnv, + corev1.EnvVar{ + Name: usernameCredentialVarName, + Value: credential.Username.Value, + ValueFrom: credential.Username.ValueFrom, + }, + corev1.EnvVar{ + Name: passwordCredentialVarName, + Value: credential.Password.Value, + ValueFrom: credential.Password.ValueFrom, + }) + } + allUsedPorts := findAllUsedPorts(template) + svcPort := actionSvcPortBase + var actionSvcPorts []int32 + for range roleObservation.ObservationActions { + svcPort = findNextAvailablePort(svcPort, allUsedPorts) + actionSvcPorts = append(actionSvcPorts, svcPort) + } + injectObservationActionContainer(csSet, template, actionSvcPorts, credentialEnv) + actionSvcList, _ := json.Marshal(actionSvcPorts) + injectRoleObserveContainer(csSet, template, string(actionSvcList), credentialEnv) +} + +func findNextAvailablePort(base int32, allUsedPorts []int32) int32 { + for port := base + 1; port < 65535; port++ { + available := true + for _, usedPort := range allUsedPorts { + if port == usedPort { + available = false + break + } + } + if available { + return port + } + } + return 0 +} + +func findAllUsedPorts(template *corev1.PodTemplateSpec) []int32 { + allUsedPorts := make([]int32, 0) + for _, container := range template.Spec.Containers { + for _, port := range container.Ports { + allUsedPorts = append(allUsedPorts, port.ContainerPort) + allUsedPorts = append(allUsedPorts, port.HostPort) + } + } + return allUsedPorts +} + +func injectRoleObserveContainer(csSet workloads.ConsensusSet, template *corev1.PodTemplateSpec, actionSvcList string, credentialEnv []corev1.EnvVar) { + // compute parameters for role observation container + roleObservation := csSet.Spec.RoleObservation + credential := csSet.Spec.Credential + image := viper.GetString("ROLE_OBSERVATION_IMAGE") + if len(image) == 0 { + image = defaultRoleObservationImage + } + observationDaemonPort := viper.GetInt("ROLE_OBSERVATION_SERVICE_PORT") + if observationDaemonPort == 0 { + observationDaemonPort = defaultRoleObservationDaemonPort + } + roleObserveURI := fmt.Sprintf(roleObservationURIFormat, strconv.Itoa(observationDaemonPort)) + env := credentialEnv + env = append(env, + corev1.EnvVar{ + Name: actionSvcListVarName, + Value: actionSvcList, + }) + if credential != nil { + // for compatibility with old probe env var names + env = append(env, + corev1.EnvVar{ + Name: "KB_SERVICE_USER", + Value: credential.Username.Value, + ValueFrom: credential.Username.ValueFrom, + }, + corev1.EnvVar{ + Name: "KB_SERVICE_PASSWORD", + Value: credential.Password.Value, + ValueFrom: credential.Password.ValueFrom, + }) + } + // find service port of th db engine + servicePort := findSvcPort(csSet) + if servicePort > 0 { + env = append(env, + corev1.EnvVar{ + Name: servicePortVarName, + Value: strconv.Itoa(servicePort), + }, + // for compatibility with old probe env var names + corev1.EnvVar{ + Name: "KB_SERVICE_PORT", + Value: strconv.Itoa(servicePort), + }) + } + + // build container + container := corev1.Container{ + Name: roleObservationName, + Image: image, + ImagePullPolicy: "IfNotPresent", + Command: []string{"role-agent", + "--port", strconv.Itoa(observationDaemonPort), + "--protocol", "http", + "--log-level", "info", + }, + Ports: []corev1.ContainerPort{{ + ContainerPort: int32(observationDaemonPort), + Name: roleObservationName, + Protocol: "TCP", + }}, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: corev1.ProbeHandler{ + Exec: &corev1.ExecAction{ + Command: []string{ + "curl", "-X", "POST", + "--max-time", "1", + "--fail-with-body", "--silent", + "-H", "Content-ComponentDefRef: application/json", + roleObserveURI, + }, + }, + }, + InitialDelaySeconds: roleObservation.InitialDelaySeconds, + TimeoutSeconds: roleObservation.TimeoutSeconds, + PeriodSeconds: roleObservation.PeriodSeconds, + SuccessThreshold: roleObservation.SuccessThreshold, + FailureThreshold: roleObservation.FailureThreshold, + }, + Env: env, + } + + // inject role observation container + template.Spec.Containers = append(template.Spec.Containers, container) +} + +func injectObservationActionContainer(csSet workloads.ConsensusSet, template *corev1.PodTemplateSpec, actionSvcPorts []int32, credentialEnv []corev1.EnvVar) { + // inject shared volume + agentVolume := corev1.Volume{ + Name: roleAgentVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + template.Spec.Volumes = append(template.Spec.Volumes, agentVolume) + + // inject init container + agentVolumeMount := corev1.VolumeMount{ + Name: roleAgentVolumeName, + MountPath: roleAgentVolumeMountPath, + } + agentPath := strings.Join([]string{roleAgentVolumeMountPath, roleAgentName}, "/") + initContainer := corev1.Container{ + Name: roleAgentInstallerName, + Image: shell2httpImage, + ImagePullPolicy: corev1.PullIfNotPresent, + VolumeMounts: []corev1.VolumeMount{agentVolumeMount}, + Command: []string{ + "cp", + shell2httpBinaryPath, + agentPath, + }, + } + template.Spec.InitContainers = append(template.Spec.InitContainers, initContainer) + + // inject action containers based on utility images + for i, action := range csSet.Spec.RoleObservation.ObservationActions { + image := action.Image + if len(image) == 0 { + image = defaultActionImage + } + command := []string{ + agentPath, + "-port", fmt.Sprintf("%d", actionSvcPorts[i]), + "-export-all-vars", + "-form", + shell2httpServePath, + strings.Join(action.Command, " "), + } + container := corev1.Container{ + Name: fmt.Sprintf("action-%d", i), + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + VolumeMounts: []corev1.VolumeMount{agentVolumeMount}, + Env: credentialEnv, + Command: command, + } + template.Spec.Containers = append(template.Spec.Containers, container) + } +} + +func buildEnvConfigData(set workloads.ConsensusSet) map[string]string { + envData := map[string]string{} + + prefix := constant.KBPrefix + "_" + strings.ToUpper(set.Name) + "_" + prefix = strings.ReplaceAll(prefix, "-", "_") + svcName := getHeadlessSvcName(set) + envData[prefix+"N"] = strconv.Itoa(int(set.Spec.Replicas)) + for i := 0; i < int(set.Spec.Replicas); i++ { + hostNameTplKey := prefix + strconv.Itoa(i) + "_HOSTNAME" + hostNameTplValue := set.Name + "-" + strconv.Itoa(i) + envData[hostNameTplKey] = fmt.Sprintf("%s.%s", hostNameTplValue, svcName) + } + + // build consensus env from set.Status.MembersStatus + followers := "" + for _, memberStatus := range set.Status.MembersStatus { + if memberStatus.PodName == "" || memberStatus.PodName == defaultPodName { + continue + } + switch { + case memberStatus.IsLeader: + envData[prefix+"LEADER"] = memberStatus.PodName + case memberStatus.CanVote: + if len(followers) > 0 { + followers += "," + } + followers += memberStatus.PodName + } + } + if followers != "" { + envData[prefix+"FOLLOWERS"] = followers + } + + // set owner uid to let pod know if the owner is recreated + uid := string(set.UID) + envData[prefix+"OWNER_UID"] = uid + envData[constant.KBPrefix+"_CONSENSUS_SET_OWNER_UID_SUFFIX8"] = uid[len(uid)-4:] + + return envData +} + +var _ graph.Transformer = &ObjectGenerationTransformer{} diff --git a/internal/controller/consensusset/transformer_status.go b/internal/controller/consensusset/transformer_status.go new file mode 100644 index 000000000..f8b5a1653 --- /dev/null +++ b/internal/controller/consensusset/transformer_status.go @@ -0,0 +1,79 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + apps "k8s.io/api/apps/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" +) + +// CSSetStatusTransformer computes the current status: +// 1. read the underlying sts's status and copy them to consensus set's status +// 2. read pod role label and update consensus set's status role fields +type CSSetStatusTransformer struct{} + +func (t *CSSetStatusTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + csSet := transCtx.CSSet + origCSSet := transCtx.OrigCSSet + + // fast return + if model.IsObjectDeleting(origCSSet) { + return nil + } + + switch { + case model.IsObjectUpdating(origCSSet): + // use consensus set's generation instead of sts's + csSet.Status.ObservedGeneration = csSet.Generation + // hack for sts initialization error: is invalid: status.replicas: Required value + if csSet.Status.Replicas == 0 { + csSet.Status.Replicas = csSet.Spec.Replicas + } + case model.IsObjectStatusUpdating(origCSSet): + // read the underlying sts + sts := &apps.StatefulSet{} + if err := transCtx.Client.Get(transCtx.Context, client.ObjectKeyFromObject(csSet), sts); err != nil { + return err + } + // keep csSet's ObservedGeneration to avoid override by sts's ObservedGeneration + generation := csSet.Status.ObservedGeneration + csSet.Status.StatefulSetStatus = sts.Status + csSet.Status.ObservedGeneration = generation + // read all pods belong to the sts, hence belong to our consensus set + pods, err := getPodsOfStatefulSet(transCtx.Context, transCtx.Client, sts) + if err != nil { + return err + } + // update role fields + setMembersStatus(csSet, pods) + } + + if err := model.PrepareRootStatus(dag); err != nil { + return err + } + + return nil +} + +var _ graph.Transformer = &CSSetStatusTransformer{} diff --git a/internal/controller/consensusset/transformer_update_strategy.go b/internal/controller/consensusset/transformer_update_strategy.go new file mode 100644 index 000000000..74d0b82db --- /dev/null +++ b/internal/controller/consensusset/transformer_update_strategy.go @@ -0,0 +1,193 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +type UpdateStrategyTransformer struct{} + +func (t *UpdateStrategyTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*CSSetTransformContext) + csSet := transCtx.CSSet + origCSSet := transCtx.OrigCSSet + if !model.IsObjectStatusUpdating(origCSSet) { + return nil + } + + // read the underlying sts + stsObj := &apps.StatefulSet{} + if err := transCtx.Client.Get(transCtx.Context, client.ObjectKeyFromObject(csSet), stsObj); err != nil { + return err + } + // read all pods belong to the sts, hence belong to our consensus set + pods, err := getPodsOfStatefulSet(transCtx.Context, transCtx.Client, stsObj) + if err != nil { + return err + } + + // prepare to do pods Deletion, that's the only thing we should do, + // the stateful_set reconciler will do the others. + // to simplify the process, we do pods Deletion after stateful_set reconcile done, + // that is stsObj.Generation == stsObj.Status.ObservedGeneration + if stsObj.Generation != stsObj.Status.ObservedGeneration { + return nil + } + + // then we wait all pods' presence, that is len(pods) == stsObj.Spec.Replicas + // only then, we have enough info about the previous pods before delete the current one + if len(pods) != int(*stsObj.Spec.Replicas) { + return nil + } + + // we don't check whether pod role label present: prefer stateful set's Update done than role probing ready + // TODO(free6om): maybe should wait consensus ready for high availability: + // 1. after some pods updated + // 2. before switchover + // 3. after switchover done + + // generate the pods Deletion plan + plan := newUpdatePlan(*csSet, pods) + podsToBeUpdated, err := plan.execute() + if err != nil { + return err + } + + // do switchover if leader in pods to be updated + switch shouldWaitNextLoop, err := doSwitchoverIfNeeded(transCtx, dag, pods, podsToBeUpdated); { + case err != nil: + return err + case shouldWaitNextLoop: + return nil + } + + for _, pod := range podsToBeUpdated { + model.PrepareDelete(dag, pod) + } + + return nil +} + +// return true means action created or in progress, should wait it to the termination state +func doSwitchoverIfNeeded(transCtx *CSSetTransformContext, dag *graph.DAG, pods []corev1.Pod, podsToBeUpdated []*corev1.Pod) (bool, error) { + if len(podsToBeUpdated) == 0 { + return false, nil + } + + csSet := transCtx.CSSet + if !shouldSwitchover(csSet, podsToBeUpdated) { + return false, nil + } + + actionList, err := getActionList(transCtx, jobScenarioUpdate) + if err != nil { + return true, err + } + if len(actionList) == 0 { + return true, createSwitchoverAction(dag, csSet, pods) + } + + // switch status if found: + // 1. succeed means action executed successfully, + // but the consensus cluster may have false positive(apecloud-mysql only?), + // we can't wait forever, update is more important. + // do the next pod update stage + // 2. failed means action executed failed, + // but this doesn't mean the consensus cluster didn't switchover(again, apecloud-mysql only?) + // we can't do anything either in this situation, emit failed event and + // do the next pod update state + // 3. in progress means action still running, + // return and wait it reaches termination state. + action := actionList[0] + switch { + case action.Status.Succeeded == 0 && action.Status.Failed == 0: + // action in progress, wait + return true, nil + case action.Status.Failed > 0: + emitActionFailedEvent(transCtx, jobTypeSwitchover, action.Name) + fallthrough + case action.Status.Succeeded > 0: + // clean up the action + doActionCleanup(dag, action) + } + return false, nil +} + +func createSwitchoverAction(dag *graph.DAG, csSet *workloads.ConsensusSet, pods []corev1.Pod) error { + leader := getLeaderPodName(csSet.Status.MembersStatus) + targetOrdinal := selectSwitchoverTarget(csSet, pods) + target := getPodName(csSet.Name, targetOrdinal) + actionType := jobTypeSwitchover + ordinal, _ := getPodOrdinal(leader) + actionName := getActionName(csSet.Name, int(csSet.Generation), ordinal, actionType) + action := buildAction(csSet, actionName, actionType, jobScenarioUpdate, leader, target) + + // don't do cluster abnormal status analysis, prefer faster update process + return createAction(dag, csSet, action) +} + +func selectSwitchoverTarget(csSet *workloads.ConsensusSet, pods []corev1.Pod) int { + var podUpdated, podUpdatedWithLabel string + for _, pod := range pods { + if intctrlutil.GetPodRevision(&pod) != csSet.Status.UpdateRevision { + continue + } + if len(podUpdated) == 0 { + podUpdated = pod.Name + } + if _, ok := pod.Labels[model.RoleLabelKey]; !ok { + continue + } + if len(podUpdatedWithLabel) == 0 { + podUpdatedWithLabel = pod.Name + break + } + } + var finalPod string + switch { + case len(podUpdatedWithLabel) > 0: + finalPod = podUpdatedWithLabel + case len(podUpdated) > 0: + finalPod = podUpdated + default: + finalPod = pods[0].Name + } + ordinal, _ := getPodOrdinal(finalPod) + return ordinal +} + +func shouldSwitchover(csSet *workloads.ConsensusSet, podsToBeUpdated []*corev1.Pod) bool { + leaderName := getLeaderPodName(csSet.Status.MembersStatus) + for _, pod := range podsToBeUpdated { + if pod.Name == leaderName { + return true + } + } + return false +} + +var _ graph.Transformer = &UpdateStrategyTransformer{} diff --git a/internal/controller/consensusset/types.go b/internal/controller/consensusset/types.go new file mode 100644 index 000000000..e16e8a994 --- /dev/null +++ b/internal/controller/consensusset/types.go @@ -0,0 +1,100 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "context" + + "github.com/go-logr/logr" + "k8s.io/client-go/tools/record" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + roclient "github.com/apecloud/kubeblocks/internal/controller/client" + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +const ( + kindConsensusSet = "ConsensusSet" + + defaultPodName = "Unknown" + + csSetFinalizerName = "cs.workloads.kubeblocks.io/finalizer" + + jobHandledLabel = "cs.workloads.kubeblocks.io/job-handled" + jobTypeLabel = "cs.workloads.kubeblocks.io/job-type" + jobScenarioLabel = "cs.workloads.kubeblocks.io/job-scenario" + jobHandledTrue = "true" + jobHandledFalse = "false" + jobTypeSwitchover = "switchover" + jobTypeMemberJoinNotifying = "member-join" + jobTypeMemberLeaveNotifying = "member-leave" + jobTypeLogSync = "log-sync" + jobTypePromote = "promote" + jobScenarioMembership = "membership-reconfiguration" + jobScenarioUpdate = "pod-update" + + roleObservationName = "role-observe" + roleAgentVolumeName = "role-agent" + roleAgentInstallerName = "role-agent-installer" + roleAgentVolumeMountPath = "/role-observation" + roleAgentName = "agent" + shell2httpImage = "msoap/shell2http:1.16.0" + shell2httpBinaryPath = "/app/shell2http" + shell2httpServePath = "/role" + defaultRoleObservationImage = "apecloud/kubeblocks-role-observation:latest" + defaultRoleObservationDaemonPort = 3501 + roleObservationURIFormat = "http://localhost:%s/getRole" + defaultActionImage = "busybox:latest" + usernameCredentialVarName = "KB_CONSENSUS_SET_USERNAME" + passwordCredentialVarName = "KB_CONSENSUS_SET_PASSWORD" + servicePortVarName = "KB_CONSENSUS_SET_SERVICE_PORT" + actionSvcListVarName = "KB_CONSENSUS_SET_ACTION_SVC_LIST" + leaderHostVarName = "KB_CONSENSUS_SET_LEADER_HOST" + targetHostVarName = "KB_CONSENSUS_SET_TARGET_HOST" + roleObservationEventFieldPath = "spec.containers{" + roleObservationName + "}" + actionSvcPortBase = int32(36500) +) + +type CSSetTransformContext struct { + context.Context + Client roclient.ReadonlyClient + record.EventRecorder + logr.Logger + CSSet *workloads.ConsensusSet + OrigCSSet *workloads.ConsensusSet +} + +func (c *CSSetTransformContext) GetContext() context.Context { + return c.Context +} + +func (c *CSSetTransformContext) GetClient() roclient.ReadonlyClient { + return c.Client +} + +func (c *CSSetTransformContext) GetRecorder() record.EventRecorder { + return c.EventRecorder +} + +func (c *CSSetTransformContext) GetLogger() logr.Logger { + return c.Logger +} + +var _ graph.TransformContext = &CSSetTransformContext{} diff --git a/internal/controller/consensusset/update_plan.go b/internal/controller/consensusset/update_plan.go new file mode 100644 index 000000000..d68994913 --- /dev/null +++ b/internal/controller/consensusset/update_plan.go @@ -0,0 +1,195 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "errors" + + corev1 "k8s.io/api/core/v1" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type updatePlan interface { + // execute executes the plan + // return error when any error occurred + // return pods to be updated, + // nil slice means no pods need to be updated + execute() ([]*corev1.Pod, error) +} + +type realUpdatePlan struct { + csSet workloads.ConsensusSet + pods []corev1.Pod + dag *graph.DAG + podsToBeUpdated []*corev1.Pod +} + +var ( + ErrContinue error + ErrWait = errors.New("wait") + ErrStop = errors.New("stop") +) + +// planWalkFunc decides whether vertex should be updated +// nil error means vertex should be updated +func (p *realUpdatePlan) planWalkFunc(vertex graph.Vertex) error { + v, _ := vertex.(*model.ObjectVertex) + if v.Obj == nil { + return ErrContinue + } + pod, ok := v.Obj.(*corev1.Pod) + if !ok { + return ErrContinue + } + + // if DeletionTimestamp is not nil, it is terminating. + if !pod.DeletionTimestamp.IsZero() { + return ErrWait + } + + // if pod is the latest version, we do nothing + if intctrlutil.GetPodRevision(pod) == p.csSet.Status.UpdateRevision { + if intctrlutil.PodIsReadyWithLabel(*pod) { + return ErrContinue + } else { + return ErrWait + } + } + + // delete the pod to trigger associate StatefulSet to re-create it + p.podsToBeUpdated = append(p.podsToBeUpdated, pod) + return ErrStop +} + +// build builds the update plan based on updateStrategy +func (p *realUpdatePlan) build() { + // make a root vertex with nil Obj + root := &model.ObjectVertex{} + p.dag.AddVertex(root) + + rolePriorityMap := composeRolePriorityMap(p.csSet) + sortPods(p.pods, rolePriorityMap, false) + + // generate plan by UpdateStrategy + switch p.csSet.Spec.UpdateStrategy { + case workloads.SerialUpdateStrategy: + p.buildSerialUpdatePlan() + case workloads.ParallelUpdateStrategy: + p.buildParallelUpdatePlan() + case workloads.BestEffortParallelUpdateStrategy: + p.buildBestEffortParallelUpdatePlan(rolePriorityMap) + } +} + +// unknown & empty & learner & 1/2 followers -> 1/2 followers -> leader +func (p *realUpdatePlan) buildBestEffortParallelUpdatePlan(rolePriorityMap map[string]int) { + currentVertex, _ := model.FindRootVertex(p.dag) + preVertex := currentVertex + + // append unknown, empty and learner + index := 0 + podList := p.pods + for i, pod := range podList { + roleName := getRoleName(pod) + if rolePriorityMap[roleName] <= learnerPriority { + vertex := &model.ObjectVertex{Obj: &podList[i]} + p.dag.AddConnect(preVertex, vertex) + currentVertex = vertex + index++ + } + } + preVertex = currentVertex + + // append 1/2 followers + podList = podList[index:] + followerCount := 0 + for _, pod := range podList { + roleName := getRoleName(pod) + if rolePriorityMap[roleName] < leaderPriority { + followerCount++ + } + } + end := followerCount / 2 + for i := 0; i < end; i++ { + vertex := &model.ObjectVertex{Obj: &podList[i]} + p.dag.AddConnect(preVertex, vertex) + currentVertex = vertex + } + preVertex = currentVertex + + // append the other 1/2 followers + podList = podList[end:] + end = followerCount - end + for i := 0; i < end; i++ { + vertex := &model.ObjectVertex{Obj: &podList[i]} + p.dag.AddConnect(preVertex, vertex) + currentVertex = vertex + } + preVertex = currentVertex + + // append leader + podList = podList[end:] + for _, pod := range podList { + vertex := &model.ObjectVertex{Obj: &pod} + p.dag.AddConnect(preVertex, vertex) + } +} + +// unknown & empty & leader & followers & learner +func (p *realUpdatePlan) buildParallelUpdatePlan() { + root, _ := model.FindRootVertex(p.dag) + for _, pod := range p.pods { + vertex := &model.ObjectVertex{Obj: &pod} + p.dag.AddConnect(root, vertex) + } +} + +// unknown -> empty -> learner -> followers(none->readonly->readwrite) -> leader +func (p *realUpdatePlan) buildSerialUpdatePlan() { + preVertex, _ := model.FindRootVertex(p.dag) + for _, pod := range p.pods { + vertex := &model.ObjectVertex{Obj: &pod} + p.dag.AddConnect(preVertex, vertex) + preVertex = vertex + } +} + +func (p *realUpdatePlan) execute() ([]*corev1.Pod, error) { + p.build() + if err := p.dag.WalkBFS(p.planWalkFunc); err != ErrContinue && err != ErrWait && err != ErrStop { + return nil, err + } + + return p.podsToBeUpdated, nil +} + +func newUpdatePlan(csSet workloads.ConsensusSet, pods []corev1.Pod) updatePlan { + return &realUpdatePlan{ + csSet: csSet, + pods: pods, + dag: graph.NewDAG(), + } +} + +var _ updatePlan = &realUpdatePlan{} diff --git a/internal/controller/consensusset/utils.go b/internal/controller/consensusset/utils.go new file mode 100644 index 000000000..3e09f32ed --- /dev/null +++ b/internal/controller/consensusset/utils.go @@ -0,0 +1,496 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package consensusset + +import ( + "context" + "fmt" + "regexp" + "sort" + "strconv" + "strings" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + workloads "github.com/apecloud/kubeblocks/apis/workloads/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components/util" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" + roclient "github.com/apecloud/kubeblocks/internal/controller/client" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/model" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type getRole func(int) string +type getOrdinal func(int) int + +const ( + leaderPriority = 1 << 5 + followerReadWritePriority = 1 << 4 + followerReadonlyPriority = 1 << 3 + followerNonePriority = 1 << 2 + learnerPriority = 1 << 1 + emptyPriority = 1 << 0 + // unknownPriority = 0 +) + +var podNameRegex = regexp.MustCompile(`(.*)-([0-9]+)$`) + +// sortPods sorts pods by their role priority +// e.g.: unknown -> empty -> learner -> follower1 -> follower2 -> leader, with follower1.Name < follower2.Name +// reverse it if reverse==true +func sortPods(pods []corev1.Pod, rolePriorityMap map[string]int, reverse bool) { + getRoleFunc := func(i int) string { + return getRoleName(pods[i]) + } + getOrdinalFunc := func(i int) int { + _, ordinal := intctrlutil.GetParentNameAndOrdinal(&pods[i]) + return ordinal + } + sortMembers(pods, rolePriorityMap, getRoleFunc, getOrdinalFunc, reverse) +} + +func sortMembersStatus(membersStatus []workloads.ConsensusMemberStatus, rolePriorityMap map[string]int) { + getRoleFunc := func(i int) string { + return membersStatus[i].Name + } + getOrdinalFunc := func(i int) int { + ordinal, _ := getPodOrdinal(membersStatus[i].PodName) + return ordinal + } + sortMembers(membersStatus, rolePriorityMap, getRoleFunc, getOrdinalFunc, true) +} + +func sortMembers[T any](membersStatus []T, + rolePriorityMap map[string]int, + getRoleFunc getRole, getOrdinalFunc getOrdinal, + reverse bool) { + sort.SliceStable(membersStatus, func(i, j int) bool { + roleI := getRoleFunc(i) + roleJ := getRoleFunc(j) + if reverse { + roleI, roleJ = roleJ, roleI + } + + if rolePriorityMap[roleI] == rolePriorityMap[roleJ] { + ordinal1 := getOrdinalFunc(i) + ordinal2 := getOrdinalFunc(j) + if reverse { + ordinal1, ordinal2 = ordinal2, ordinal1 + } + return ordinal1 < ordinal2 + } + + return rolePriorityMap[roleI] < rolePriorityMap[roleJ] + }) +} + +// composeRolePriorityMap generates a priority map based on roles. +func composeRolePriorityMap(set workloads.ConsensusSet) map[string]int { + rolePriorityMap := make(map[string]int, 0) + rolePriorityMap[""] = emptyPriority + for _, role := range set.Spec.Roles { + roleName := strings.ToLower(role.Name) + switch { + case role.IsLeader: + rolePriorityMap[roleName] = leaderPriority + case role.CanVote: + switch role.AccessMode { + case workloads.NoneMode: + rolePriorityMap[roleName] = followerNonePriority + case workloads.ReadonlyMode: + rolePriorityMap[roleName] = followerReadonlyPriority + case workloads.ReadWriteMode: + rolePriorityMap[roleName] = followerReadWritePriority + } + default: + rolePriorityMap[roleName] = learnerPriority + } + } + + return rolePriorityMap +} + +// updatePodRoleLabel updates pod role label when internal container role changed +func updatePodRoleLabel(cli client.Client, + reqCtx intctrlutil.RequestCtx, + set workloads.ConsensusSet, + pod *corev1.Pod, roleName string) error { + ctx := reqCtx.Ctx + roleMap := composeRoleMap(set) + // role not defined in CR, ignore it + roleName = strings.ToLower(roleName) + + // update pod role label + patch := client.MergeFrom(pod.DeepCopy()) + role, ok := roleMap[roleName] + switch ok { + case true: + pod.Labels[model.RoleLabelKey] = role.Name + pod.Labels[model.ConsensusSetAccessModeLabelKey] = string(role.AccessMode) + case false: + delete(pod.Labels, model.RoleLabelKey) + delete(pod.Labels, model.ConsensusSetAccessModeLabelKey) + } + return cli.Patch(ctx, pod, patch) +} + +func composeRoleMap(set workloads.ConsensusSet) map[string]workloads.ConsensusRole { + roleMap := make(map[string]workloads.ConsensusRole, 0) + for _, role := range set.Spec.Roles { + roleMap[strings.ToLower(role.Name)] = role + } + return roleMap +} + +func setMembersStatus(set *workloads.ConsensusSet, pods []corev1.Pod) { + // compose new status + newMembersStatus := make([]workloads.ConsensusMemberStatus, 0) + roleMap := composeRoleMap(*set) + for _, pod := range pods { + if !intctrlutil.PodIsReadyWithLabel(pod) { + continue + } + roleName := getRoleName(pod) + role, ok := roleMap[roleName] + if !ok { + continue + } + memberStatus := workloads.ConsensusMemberStatus{ + PodName: pod.Name, + ConsensusRole: role, + } + newMembersStatus = append(newMembersStatus, memberStatus) + } + + // members(pods) being scheduled should be kept + oldMemberMap := make(map[string]*workloads.ConsensusMemberStatus, len(set.Status.MembersStatus)) + for i, status := range set.Status.MembersStatus { + oldMemberMap[status.PodName] = &set.Status.MembersStatus[i] + } + newMemberMap := make(map[string]*workloads.ConsensusMemberStatus, len(newMembersStatus)) + for i, status := range newMembersStatus { + newMemberMap[status.PodName] = &newMembersStatus[i] + } + oldMemberSet := sets.KeySet(oldMemberMap) + newMemberSet := sets.KeySet(newMemberMap) + memberToKeepSet := oldMemberSet.Difference(newMemberSet) + for podName := range memberToKeepSet { + ordinal, _ := getPodOrdinal(podName) + // members have left because of scale-in + if ordinal >= int(set.Spec.Replicas) { + continue + } + newMembersStatus = append(newMembersStatus, *oldMemberMap[podName]) + } + + rolePriorityMap := composeRolePriorityMap(*set) + sortMembersStatus(newMembersStatus, rolePriorityMap) + set.Status.MembersStatus = newMembersStatus +} + +func getRoleName(pod corev1.Pod) string { + return strings.ToLower(pod.Labels[constant.RoleLabelKey]) +} + +func ownedKinds() []client.ObjectList { + return []client.ObjectList{ + &appsv1.StatefulSetList{}, + &corev1.ServiceList{}, + &corev1.SecretList{}, + &corev1.ConfigMapList{}, + &policyv1.PodDisruptionBudgetList{}, + } +} + +func deletionKinds() []client.ObjectList { + kinds := ownedKinds() + kinds = append(kinds, &corev1.PersistentVolumeClaimList{}, &batchv1.JobList{}) + return kinds +} + +func getPodsOfStatefulSet(ctx context.Context, cli roclient.ReadonlyClient, stsObj *appsv1.StatefulSet) ([]corev1.Pod, error) { + podList := &corev1.PodList{} + if err := cli.List(ctx, podList, + &client.ListOptions{Namespace: stsObj.Namespace}, + client.MatchingLabels{ + model.KBManagedByKey: stsObj.Labels[model.KBManagedByKey], + model.AppInstanceLabelKey: stsObj.Labels[model.AppInstanceLabelKey], + }); err != nil { + return nil, err + } + var pods []corev1.Pod + for _, pod := range podList.Items { + if util.IsMemberOf(stsObj, &pod) { + pods = append(pods, pod) + } + } + return pods, nil +} + +func getHeadlessSvcName(set workloads.ConsensusSet) string { + return strings.Join([]string{set.Name, "headless"}, "-") +} + +func findSvcPort(csSet workloads.ConsensusSet) int { + port := csSet.Spec.Service.Ports[0] + for _, c := range csSet.Spec.Template.Spec.Containers { + for _, p := range c.Ports { + if port.TargetPort.Type == intstr.String && p.Name == port.TargetPort.StrVal || + port.TargetPort.Type == intstr.Int && p.ContainerPort == port.TargetPort.IntVal { + return int(p.ContainerPort) + } + } + } + return 0 +} + +func getActionList(transCtx *CSSetTransformContext, actionScenario string) ([]*batchv1.Job, error) { + var actionList []*batchv1.Job + ml := client.MatchingLabels{ + model.AppInstanceLabelKey: transCtx.CSSet.Name, + model.KBManagedByKey: kindConsensusSet, + jobScenarioLabel: actionScenario, + jobHandledLabel: jobHandledFalse, + } + jobList := &batchv1.JobList{} + if err := transCtx.Client.List(transCtx.Context, jobList, ml); err != nil { + return nil, err + } + for i := range jobList.Items { + actionList = append(actionList, &jobList.Items[i]) + } + printActionList(transCtx.Logger, actionList) + return actionList, nil +} + +func getPodName(parent string, ordinal int) string { + return fmt.Sprintf("%s-%d", parent, ordinal) +} + +func getActionName(parent string, generation, ordinal int, actionType string) string { + return fmt.Sprintf("%s-%d-%d-%s", parent, generation, ordinal, actionType) +} + +func getLeaderPodName(membersStatus []workloads.ConsensusMemberStatus) string { + for _, memberStatus := range membersStatus { + if memberStatus.IsLeader { + return memberStatus.PodName + } + } + return "" +} + +func getPodOrdinal(podName string) (int, error) { + subMatches := podNameRegex.FindStringSubmatch(podName) + if len(subMatches) < 3 { + return 0, fmt.Errorf("wrong pod name: %s", podName) + } + return strconv.Atoi(subMatches[2]) +} + +// ordinal is the ordinal of pod which this action apply to +func createAction(dag *graph.DAG, csSet *workloads.ConsensusSet, action *batchv1.Job) error { + if err := intctrlutil.SetOwnership(csSet, action, model.GetScheme(), csSetFinalizerName); err != nil { + return err + } + model.PrepareCreate(dag, action) + return nil +} + +func buildAction(csSet *workloads.ConsensusSet, actionName, actionType, actionScenario string, leader, target string) *batchv1.Job { + env := buildActionEnv(csSet, leader, target) + template := buildActionPodTemplate(csSet, env, actionType) + return builder.NewJobBuilder(csSet.Namespace, actionName). + AddLabels(model.AppInstanceLabelKey, csSet.Name). + AddLabels(model.KBManagedByKey, kindConsensusSet). + AddLabels(jobScenarioLabel, actionScenario). + AddLabels(jobTypeLabel, actionType). + AddLabels(jobHandledLabel, jobHandledFalse). + SetSuspend(false). + SetPodTemplateSpec(*template). + GetObject() +} + +func buildActionPodTemplate(csSet *workloads.ConsensusSet, env []corev1.EnvVar, actionType string) *corev1.PodTemplateSpec { + credential := csSet.Spec.Credential + credentialEnv := make([]corev1.EnvVar, 0) + if credential != nil { + credentialEnv = append(credentialEnv, + corev1.EnvVar{ + Name: usernameCredentialVarName, + Value: credential.Username.Value, + ValueFrom: credential.Username.ValueFrom, + }, + corev1.EnvVar{ + Name: passwordCredentialVarName, + Value: credential.Password.Value, + ValueFrom: credential.Password.ValueFrom, + }) + } + env = append(env, credentialEnv...) + reconfiguration := csSet.Spec.MembershipReconfiguration + image := findActionImage(reconfiguration, actionType) + command := getActionCommand(reconfiguration, actionType) + container := corev1.Container{ + Name: actionType, + Image: image, + ImagePullPolicy: corev1.PullIfNotPresent, + Command: command, + Env: env, + } + template := &corev1.PodTemplateSpec{ + Spec: corev1.PodSpec{ + Containers: []corev1.Container{container}, + RestartPolicy: corev1.RestartPolicyOnFailure, + }, + } + return template +} + +func buildActionEnv(csSet *workloads.ConsensusSet, leader, target string) []corev1.EnvVar { + svcName := getHeadlessSvcName(*csSet) + leaderHost := fmt.Sprintf("%s.%s", leader, svcName) + targetHost := fmt.Sprintf("%s.%s", target, svcName) + svcPort := findSvcPort(*csSet) + return []corev1.EnvVar{ + { + Name: leaderHostVarName, + Value: leaderHost, + }, + { + Name: servicePortVarName, + Value: strconv.Itoa(svcPort), + }, + { + Name: targetHostVarName, + Value: targetHost, + }, + } +} + +func findActionImage(reconfiguration *workloads.MembershipReconfiguration, actionType string) string { + if reconfiguration == nil { + return "" + } + + getImage := func(action *workloads.Action) string { + if action != nil && len(action.Image) > 0 { + return action.Image + } + return "" + } + switch actionType { + case jobTypePromote: + if image := getImage(reconfiguration.PromoteAction); len(image) > 0 { + return image + } + fallthrough + case jobTypeLogSync: + if image := getImage(reconfiguration.LogSyncAction); len(image) > 0 { + return image + } + fallthrough + case jobTypeMemberLeaveNotifying: + if image := getImage(reconfiguration.MemberLeaveAction); len(image) > 0 { + return image + } + fallthrough + case jobTypeMemberJoinNotifying: + if image := getImage(reconfiguration.MemberJoinAction); len(image) > 0 { + return image + } + fallthrough + case jobTypeSwitchover: + if image := getImage(reconfiguration.SwitchoverAction); len(image) > 0 { + return image + } + return defaultActionImage + } + + return "" +} + +func getActionCommand(reconfiguration *workloads.MembershipReconfiguration, actionType string) []string { + if reconfiguration == nil { + return nil + } + getCommand := func(action *workloads.Action) []string { + if action == nil { + return nil + } + return action.Command + } + switch actionType { + case jobTypeSwitchover: + return getCommand(reconfiguration.SwitchoverAction) + case jobTypeMemberJoinNotifying: + return getCommand(reconfiguration.MemberJoinAction) + case jobTypeMemberLeaveNotifying: + return getCommand(reconfiguration.MemberLeaveAction) + case jobTypeLogSync: + return getCommand(reconfiguration.LogSyncAction) + case jobTypePromote: + return getCommand(reconfiguration.PromoteAction) + } + return nil +} + +func doActionCleanup(dag *graph.DAG, action *batchv1.Job) { + actionOld := action.DeepCopy() + actionNew := actionOld.DeepCopy() + actionNew.Labels[jobHandledLabel] = jobHandledTrue + model.PrepareUpdate(dag, actionOld, actionNew) +} + +func emitEvent(transCtx *CSSetTransformContext, action *batchv1.Job) { + switch { + case action.Status.Succeeded > 0: + emitActionSucceedEvent(transCtx, action.Labels[jobTypeLabel], action.Name) + case action.Status.Failed > 0: + emitActionFailedEvent(transCtx, action.Labels[jobTypeLabel], action.Name) + } +} + +func emitActionSucceedEvent(transCtx *CSSetTransformContext, actionType, actionName string) { + message := fmt.Sprintf("%s succeed, job name: %s", actionType, actionName) + emitActionEvent(transCtx, corev1.EventTypeNormal, actionType, message) +} + +func emitActionFailedEvent(transCtx *CSSetTransformContext, actionType, actionName string) { + message := fmt.Sprintf("%s failed, job name: %s", actionType, actionName) + emitActionEvent(transCtx, corev1.EventTypeWarning, actionType, message) +} + +func emitAbnormalEvent(transCtx *CSSetTransformContext, actionType, actionName string, err error) { + message := fmt.Sprintf("%s, job name: %s", err.Error(), actionName) + emitActionEvent(transCtx, corev1.EventTypeWarning, actionType, message) +} + +func emitActionEvent(transCtx *CSSetTransformContext, eventType, reason, message string) { + transCtx.EventRecorder.Event(transCtx.CSSet, eventType, strings.ToUpper(reason), message) +} diff --git a/internal/controller/graph/dag.go b/internal/controller/graph/dag.go index feaeeb2b4..f5ec859a2 100644 --- a/internal/controller/graph/dag.go +++ b/internal/controller/graph/dag.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package graph @@ -51,7 +54,7 @@ func (r *realEdge) To() Vertex { return r.T } -// AddVertex put 'v' into 'd' +// AddVertex puts 'v' into 'd' func (d *DAG) AddVertex(v Vertex) bool { if v == nil { return false @@ -60,7 +63,7 @@ func (d *DAG) AddVertex(v Vertex) bool { return true } -// RemoveVertex delete 'v' from 'd' +// RemoveVertex deletes 'v' from 'd' // the in&out edges are also deleted func (d *DAG) RemoveVertex(v Vertex) bool { if v == nil { @@ -75,7 +78,7 @@ func (d *DAG) RemoveVertex(v Vertex) bool { return true } -// Vertices return all vertices in 'd' +// Vertices returns all vertices in 'd' func (d *DAG) Vertices() []Vertex { vertices := make([]Vertex, 0) for v := range d.vertices { @@ -84,7 +87,7 @@ func (d *DAG) Vertices() []Vertex { return vertices } -// AddEdge put edge 'e' into 'd' +// AddEdge puts edge 'e' into 'd' func (d *DAG) AddEdge(e Edge) bool { if e.From() == nil || e.To() == nil { return false @@ -98,7 +101,7 @@ func (d *DAG) AddEdge(e Edge) bool { return true } -// RemoveEdge delete edge 'e' +// RemoveEdge deletes edge 'e' func (d *DAG) RemoveEdge(e Edge) bool { for k := range d.edges { if k.From() == e.From() && k.To() == e.To() { @@ -123,7 +126,24 @@ func (d *DAG) Connect(from, to Vertex) bool { return true } -// WalkTopoOrder walk the DAG 'd' in topology order +// AddConnect add 'to' to the DAG 'd' and connect 'from' to 'to' +func (d *DAG) AddConnect(from, to Vertex) bool { + if !d.AddVertex(to) { + return false + } + return d.Connect(from, to) +} + +// AddConnectRoot add 'v' to the DAG 'd' and connect root to 'v' +func (d *DAG) AddConnectRoot(v Vertex) bool { + root := d.Root() + if root == nil { + return false + } + return d.AddConnect(root, v) +} + +// WalkTopoOrder walks the DAG 'd' in topology order func (d *DAG) WalkTopoOrder(walkFunc WalkFunc) error { if err := d.validate(); err != nil { return err @@ -137,7 +157,7 @@ func (d *DAG) WalkTopoOrder(walkFunc WalkFunc) error { return nil } -// WalkReverseTopoOrder walk the DAG 'd' in reverse topology order +// WalkReverseTopoOrder walks the DAG 'd' in reverse topology order func (d *DAG) WalkReverseTopoOrder(walkFunc WalkFunc) error { if err := d.validate(); err != nil { return err @@ -151,7 +171,44 @@ func (d *DAG) WalkReverseTopoOrder(walkFunc WalkFunc) error { return nil } -// Root return root vertex that has no in adjacent. +// WalkBFS walks the DAG 'd' in breadth-first order +func (d *DAG) WalkBFS(walkFunc WalkFunc) error { + if err := d.validate(); err != nil { + return err + } + queue := make([]Vertex, 0) + walked := make(map[Vertex]bool, len(d.Vertices())) + + root := d.Root() + queue = append(queue, root) + for len(queue) > 0 { + var walkErr error + for _, vertex := range queue { + if err := walkFunc(vertex); err != nil { + walkErr = err + } + } + if walkErr != nil { + return walkErr + } + + nextStep := make([]Vertex, 0) + for _, vertex := range queue { + adjs := d.outAdj(vertex) + for _, adj := range adjs { + if !walked[adj] { + nextStep = append(nextStep, adj) + walked[adj] = true + } + } + } + queue = nextStep + } + + return nil +} + +// Root returns root vertex that has no in adjacent. // our DAG should have one and only one root vertex func (d *DAG) Root() Vertex { roots := make([]Vertex, 0) @@ -166,7 +223,17 @@ func (d *DAG) Root() Vertex { return roots[0] } -// String return a string representation of the DAG in topology order +func (d *DAG) Merge(subDag *DAG) { + root := d.Root() + for v := range subDag.vertices { + if len(d.inAdj(v)) == 0 { + d.AddVertex(v) + d.Connect(root, v) + } + } +} + +// String returns a string representation of the DAG in topology order func (d *DAG) String() string { str := "|" walkFunc := func(v Vertex) error { @@ -226,7 +293,7 @@ func (d *DAG) validate() error { return nil } -// topologicalOrder return a vertex list that is in topology order +// topologicalOrder returns a vertex list that is in topology order // 'd' MUST be a legal DAG func (d *DAG) topologicalOrder(reverse bool) []Vertex { // orders is what we want, a (reverse) topological order of this DAG @@ -256,7 +323,6 @@ func (d *DAG) topologicalOrder(reverse bool) []Vertex { for v := range d.vertices { walk(v) } - return orders } @@ -282,7 +348,7 @@ func (d *DAG) inAdj(v Vertex) []Vertex { return vertices } -// NewDAG new an empty DAG +// NewDAG news an empty DAG func NewDAG() *DAG { dag := &DAG{ vertices: make(map[Vertex]Vertex), diff --git a/internal/controller/graph/dag_test.go b/internal/controller/graph/dag_test.go index 471e39f58..e02f403df 100644 --- a/internal/controller/graph/dag_test.go +++ b/internal/controller/graph/dag_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package graph diff --git a/internal/controller/graph/doc.go b/internal/controller/graph/doc.go index a29985523..57cd51e63 100644 --- a/internal/controller/graph/doc.go +++ b/internal/controller/graph/doc.go @@ -1,36 +1,41 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ /* -Package graph tries to model the controller reconciliation loop in a more structure way. -It structures the reconciliation loop to 4 stage: Init, Validate, Build and Execute. +Package graph tries to model the controller reconciliation loop in a more structured way. +It structures the reconciliation loop to 3 stage: Init, Build and Execute. # Initialization Stage the Init stage is for meta loading, object query etc. Try loading infos that used in the following stages. -# Validation Stage +# Building Stage -Validating everything (object spec is legal, resources in K8s cluster are enough etc.) in this stage -to make sure the following Build and Execute stages can go well. +## Validation -# Building Stage +The first part of Building is Validation, +which Validates everything (object spec is legal, resources in K8s cluster are enough etc.) +to make sure the following Build and Execute parts can go well. -The Build stage's target is to generate an execution plan. +## Building +The Building part's target is to generate an execution plan. The plan is composed by a DAG which represents the actions that should be taken on all K8s native objects owned by the controller, a group of Transformers which transform the initial DAG to the final one, and a WalkFunc which does the real action when the final DAG is walked through. diff --git a/internal/controller/graph/plan_builder.go b/internal/controller/graph/plan_builder.go index 7620bbc95..9e6453a3d 100644 --- a/internal/controller/graph/plan_builder.go +++ b/internal/controller/graph/plan_builder.go @@ -1,29 +1,43 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package graph // PlanBuilder builds a Plan by applying a group of Transformer to an empty DAG. type PlanBuilder interface { + // Init loads the primary object to be reconciled, and does meta initialization Init() error - Validate() error + + // AddTransformer adds transformers to the builder in sequence order. + // And the transformers will be executed in the add order. + AddTransformer(transformer ...Transformer) PlanBuilder + + // AddParallelTransformer adds transformers to the builder. + // And the transformers will be executed in parallel. + AddParallelTransformer(transformer ...Transformer) PlanBuilder + + // Build runs all the transformers added by AddTransformer and/or AddParallelTransformer. Build() (Plan, error) } // Plan defines the final actions should be executed. type Plan interface { + // Execute the plan Execute() error } diff --git a/internal/controller/graph/transformer.go b/internal/controller/graph/transformer.go index 03646013e..c4febfb54 100644 --- a/internal/controller/graph/transformer.go +++ b/internal/controller/graph/transformer.go @@ -1,33 +1,75 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package graph +import ( + "context" + "errors" + + "github.com/go-logr/logr" + "k8s.io/client-go/tools/record" + + "github.com/apecloud/kubeblocks/internal/controller/client" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// TransformContext is used by Transformer.Transform +type TransformContext interface { + GetContext() context.Context + GetClient() client.ReadonlyClient + GetRecorder() record.EventRecorder + GetLogger() logr.Logger +} + // Transformer transforms a DAG to a new version type Transformer interface { - Transform(dag *DAG) error + Transform(ctx TransformContext, dag *DAG) error } +// TransformerChain chains a group Transformer together type TransformerChain []Transformer -func (t *TransformerChain) ApplyTo(dag *DAG) error { - for _, transformer := range *t { - if err := transformer.Transform(dag); err != nil { - return err +// ErrPrematureStop is used to stop the Transformer chain for some purpose. +// Use it in Transformer.Transform when all jobs have done and no need to run following transformers +var ErrPrematureStop = errors.New("Premature-Stop") + +// ApplyTo applies TransformerChain t to dag +func (r TransformerChain) ApplyTo(ctx TransformContext, dag *DAG) error { + var delayedError error + for _, transformer := range r { + if err := transformer.Transform(ctx, dag); err != nil { + if intctrlutil.IsDelayedRequeueError(err) { + if delayedError == nil { + delayedError = err + } + continue + } + return ignoredIfPrematureStop(err) } } - return nil + return delayedError +} + +func ignoredIfPrematureStop(err error) error { + if err == ErrPrematureStop { + return nil + } + return err } diff --git a/internal/controller/graph/validator.go b/internal/controller/graph/validator.go deleted file mode 100644 index 3272c2c64..000000000 --- a/internal/controller/graph/validator.go +++ /dev/null @@ -1,33 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 graph - -// Validator validate everything is ok before Build the plan -type Validator interface { - Validate() error -} - -type ValidatorChain []Validator - -func (v *ValidatorChain) WalkThrough() error { - for _, validator := range *v { - if err := validator.Validate(); err != nil { - return err - } - } - return nil -} diff --git a/internal/controller/lifecycle/cluster_plan_builder.go b/internal/controller/lifecycle/cluster_plan_builder.go index 14e40c12d..e627c732e 100644 --- a/internal/controller/lifecycle/cluster_plan_builder.go +++ b/internal/controller/lifecycle/cluster_plan_builder.go @@ -1,573 +1,364 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle import ( + "context" "errors" "fmt" "reflect" + "strings" - snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" - appsv1 "k8s.io/api/apps/v1" + "github.com/go-logr/logr" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" "github.com/apecloud/kubeblocks/internal/constant" - types2 "github.com/apecloud/kubeblocks/internal/controller/client" + roclient "github.com/apecloud/kubeblocks/internal/controller/client" "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) +// TODO: cluster plan builder can be abstracted as a common flow + +// ClusterTransformContext a graph.TransformContext implementation for Cluster reconciliation +type ClusterTransformContext struct { + context.Context + Client roclient.ReadonlyClient + record.EventRecorder + logr.Logger + Cluster *appsv1alpha1.Cluster + OrigCluster *appsv1alpha1.Cluster + ClusterDef *appsv1alpha1.ClusterDefinition + ClusterVer *appsv1alpha1.ClusterVersion +} + // clusterPlanBuilder a graph.PlanBuilder implementation for Cluster reconciliation type clusterPlanBuilder struct { - ctx intctrlutil.RequestCtx - cli client.Client - req ctrl.Request - recorder record.EventRecorder - cluster *appsv1alpha1.Cluster - originCluster appsv1alpha1.Cluster + req ctrl.Request + cli client.Client + transCtx *ClusterTransformContext + transformers graph.TransformerChain } // clusterPlan a graph.Plan implementation for Cluster reconciliation type clusterPlan struct { - ctx intctrlutil.RequestCtx - cli client.Client - recorder record.EventRecorder dag *graph.DAG walkFunc graph.WalkFunc - cluster *appsv1alpha1.Cluster + cli client.Client + transCtx *ClusterTransformContext } +var _ graph.TransformContext = &ClusterTransformContext{} var _ graph.PlanBuilder = &clusterPlanBuilder{} var _ graph.Plan = &clusterPlan{} -func (c *clusterPlanBuilder) Init() error { - cluster := &appsv1alpha1.Cluster{} - if err := c.cli.Get(c.ctx.Ctx, c.req.NamespacedName, cluster); err != nil { - return err - } - c.cluster = cluster - c.originCluster = *cluster.DeepCopy() - // handles the cluster phase and ops condition first to indicates what the current cluster is doing. - c.handleClusterPhase() - c.handleLatestOpsRequestProcessingCondition() - return nil +// TransformContext implementation + +func (c *ClusterTransformContext) GetContext() context.Context { + return c.Context } -// updateClusterPhase handles the cluster phase and ops condition first to indicates what the current cluster is doing. -func (c *clusterPlanBuilder) handleClusterPhase() { - clusterPhase := c.cluster.Status.Phase - if isClusterUpdating(*c.cluster) { - if clusterPhase == "" { - c.cluster.Status.Phase = appsv1alpha1.CreatingClusterPhase - } else if clusterPhase != appsv1alpha1.CreatingClusterPhase { - c.cluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase - } - } +func (c *ClusterTransformContext) GetClient() roclient.ReadonlyClient { + return c.Client } -// updateLatestOpsRequestProcessingCondition handles the latest opsRequest processing condition. -func (c *clusterPlanBuilder) handleLatestOpsRequestProcessingCondition() { - opsRecords, _ := opsutil.GetOpsRequestSliceFromCluster(c.cluster) - if len(opsRecords) == 0 { - return - } - ops := opsRecords[0] - opsBehaviour, ok := appsv1alpha1.OpsRequestBehaviourMapper[ops.Type] - if !ok { - return - } - opsCondition := newOpsRequestProcessingCondition(ops.Name, string(ops.Type), opsBehaviour.ProcessingReasonInClusterCondition) - oldCondition := meta.FindStatusCondition(c.cluster.Status.Conditions, opsCondition.Type) - if oldCondition == nil { - // if this condition not exists, insert it to the first position. - opsCondition.LastTransitionTime = metav1.Now() - c.cluster.Status.Conditions = append([]metav1.Condition{opsCondition}, c.cluster.Status.Conditions...) - } else { - meta.SetStatusCondition(&c.cluster.Status.Conditions, opsCondition) - } +func (c *ClusterTransformContext) GetRecorder() record.EventRecorder { + return c.EventRecorder } -func (c *clusterPlanBuilder) Validate() error { - var err error - defer func() { - if err != nil { - _ = c.updateClusterStatusWithCondition(newFailedProvisioningStartedCondition(err.Error(), ReasonPreCheckFailed)) - } - }() +func (c *ClusterTransformContext) GetLogger() logr.Logger { + return c.Logger +} - validateExistence := func(key client.ObjectKey, object client.Object) error { - err = c.cli.Get(c.ctx.Ctx, key, object) - if err != nil { - return newRequeueError(requeueDuration, err.Error()) - } - return nil - } +// PlanBuilder implementation - // validate cd & cv existences - cd := &appsv1alpha1.ClusterDefinition{} - if err = validateExistence(types.NamespacedName{Name: c.cluster.Spec.ClusterDefRef}, cd); err != nil { +func (c *clusterPlanBuilder) Init() error { + cluster := &appsv1alpha1.Cluster{} + if err := c.cli.Get(c.transCtx.Context, c.req.NamespacedName, cluster); err != nil { return err } - var cv *appsv1alpha1.ClusterVersion - if len(c.cluster.Spec.ClusterVersionRef) > 0 { - cv = &appsv1alpha1.ClusterVersion{} - if err = validateExistence(types.NamespacedName{Name: c.cluster.Spec.ClusterVersionRef}, cv); err != nil { - return err - } - } - - // validate cd & cv availability - if cd.Status.Phase != appsv1alpha1.AvailablePhase || (cv != nil && cv.Status.Phase != appsv1alpha1.AvailablePhase) { - message := fmt.Sprintf("ref resource is unavailable, this problem needs to be solved first. cd: %v, cv: %v", cd, cv) - err = errors.New(message) - return newRequeueError(requeueDuration, message) - } - - // validate logs - // and a sample validator chain - chain := &graph.ValidatorChain{ - &enableLogsValidator{cluster: c.cluster, clusterDef: cd}, - } - if err = chain.WalkThrough(); err != nil { - return newRequeueError(requeueDuration, err.Error()) - } + c.transCtx.Cluster = cluster + c.transCtx.OrigCluster = cluster.DeepCopy() + c.transformers = append(c.transformers, &initTransformer{ + cluster: c.transCtx.Cluster, + originCluster: c.transCtx.OrigCluster, + }) return nil } -func (c *clusterPlanBuilder) handleProvisionStartedCondition() { - // set provisioning cluster condition - condition := newProvisioningStartedCondition(c.cluster.Name, c.cluster.Generation) - oldCondition := meta.FindStatusCondition(c.cluster.Status.Conditions, condition.Type) - if conditionIsChanged(oldCondition, condition) { - meta.SetStatusCondition(&c.cluster.Status.Conditions, condition) - c.recorder.Event(c.cluster, corev1.EventTypeNormal, condition.Reason, condition.Message) - } +func (c *clusterPlanBuilder) AddTransformer(transformer ...graph.Transformer) graph.PlanBuilder { + c.transformers = append(c.transformers, transformer...) + return c +} + +func (c *clusterPlanBuilder) AddParallelTransformer(transformer ...graph.Transformer) graph.PlanBuilder { + c.transformers = append(c.transformers, &ParallelTransformers{transformers: transformer}) + return c } -// Build only cluster Creation, Update and Deletion supported. +// Build runs all transformers to generate a plan func (c *clusterPlanBuilder) Build() (graph.Plan, error) { - // set provisioning cluster condition - c.handleProvisionStartedCondition() var err error defer func() { - if err != nil { - _ = c.updateClusterStatusWithCondition(newFailedApplyResourcesCondition(err.Error())) + // set apply resource condition + // if cluster is being deleted, no need to set apply resource condition + if c.transCtx.Cluster.IsDeleting() { + return + } + preCheckCondition := meta.FindStatusCondition(c.transCtx.Cluster.Status.Conditions, appsv1alpha1.ConditionTypeProvisioningStarted) + if preCheckCondition == nil { + // this should not happen + return } + // if pre-check failed, this is a fast return, no need to set apply resource condition + if preCheckCondition.Status != metav1.ConditionTrue { + sendWaringEventWithError(c.transCtx.GetRecorder(), c.transCtx.Cluster, ReasonPreCheckFailed, err) + return + } + setApplyResourceCondition(&c.transCtx.Cluster.Status.Conditions, c.transCtx.Cluster.Generation, err) + sendWaringEventWithError(c.transCtx.GetRecorder(), c.transCtx.Cluster, ReasonApplyResourcesFailed, err) }() - var cr *clusterRefResources - cr, err = c.getClusterRefResources() - if err != nil { - return nil, err - } - var roClient types2.ReadonlyClient = delegateClient{Client: c.cli} - - // TODO: remove all cli & ctx fields from transformers, keep them in pure-dag-manipulation form - // build transformer chain - chain := &graph.TransformerChain{ - // init dag, that is put cluster vertex into dag - &initTransformer{cluster: c.cluster, originCluster: &c.originCluster}, - // fill class related info - &fillClass{cc: *cr, cli: c.cli, ctx: c.ctx}, - // fix cd&cv labels of cluster - &fixClusterLabelsTransformer{}, - // cluster to K8s objects and put them into dag - &clusterTransformer{cc: *cr, cli: c.cli, ctx: c.ctx}, - // tls certs secret - &tlsCertsTransformer{cr: *cr, cli: roClient, ctx: c.ctx}, - // add our finalizer to all objects - &ownershipTransformer{finalizer: dbClusterFinalizerName}, - // make all workload objects depending on credential secret - &credentialTransformer{}, - // make config configmap immutable - &configTransformer{}, - // read old snapshot from cache, and generate diff plan - &objectActionTransformer{cli: roClient, ctx: c.ctx}, - // handle TerminationPolicyType=DoNotTerminate - &doNotTerminateTransformer{}, - // horizontal scaling - &stsHorizontalScalingTransformer{cr: *cr, cli: roClient, ctx: c.ctx}, - // stateful set pvc Update - &stsPVCTransformer{cli: c.cli, ctx: c.ctx}, - // replication set horizontal scaling - &rplSetHorizontalScalingTransformer{cr: *cr, cli: c.cli, ctx: c.ctx}, - // finally, update cluster status - newClusterStatusTransformer(c.ctx, c.cli, c.recorder, *cr), - } - - // new a DAG and apply chain on it, after that we should get the final Plan + // new a DAG and apply chain on it dag := graph.NewDAG() - if err = chain.ApplyTo(dag); err != nil { - return nil, err - } + err = c.transformers.ApplyTo(c.transCtx, dag) + c.transCtx.Logger.V(1).Info(fmt.Sprintf("DAG: %s", dag)) - c.ctx.Log.Info(fmt.Sprintf("DAG: %s", dag)) - // we got the execution plan + // construct execution plan plan := &clusterPlan{ - ctx: c.ctx, - cli: c.cli, - recorder: c.recorder, dag: dag, - walkFunc: c.defaultWalkFunc, - cluster: c.cluster, - } - return plan, nil -} - -func (c *clusterPlanBuilder) updateClusterStatusWithCondition(condition metav1.Condition) error { - oldCondition := meta.FindStatusCondition(c.cluster.Status.Conditions, condition.Type) - meta.SetStatusCondition(&c.cluster.Status.Conditions, condition) - if !reflect.DeepEqual(c.cluster.Status, c.originCluster.Status) { - if err := c.cli.Status().Patch(c.ctx.Ctx, c.cluster, client.MergeFrom(c.originCluster.DeepCopy())); err != nil { - return err - } - } - // Normal events are only sent once. - if !conditionIsChanged(oldCondition, condition) && condition.Status == metav1.ConditionTrue { - return nil - } - eventType := corev1.EventTypeWarning - if condition.Status == metav1.ConditionTrue { - eventType = corev1.EventTypeNormal + walkFunc: c.defaultWalkFuncWithLogging, + cli: c.cli, + transCtx: c.transCtx, } - c.recorder.Event(c.cluster, eventType, condition.Reason, condition.Message) - return nil + return plan, err } -// NewClusterPlanBuilder returns a clusterPlanBuilder powered PlanBuilder -// TODO: change ctx to context.Context -func NewClusterPlanBuilder(ctx intctrlutil.RequestCtx, cli client.Client, req ctrl.Request, recorder record.EventRecorder) graph.PlanBuilder { - return &clusterPlanBuilder{ - ctx: ctx, - cli: cli, - req: req, - recorder: recorder, - } -} +// Plan implementation func (p *clusterPlan) Execute() error { err := p.dag.WalkReverseTopoOrder(p.walkFunc) if err != nil { - if hErr := p.handleDAGWalkError(err); hErr != nil { + if hErr := p.handlePlanExecutionError(err); hErr != nil { return hErr } } return err } -func (p *clusterPlan) handleDAGWalkError(err error) error { - condition := newFailedApplyResourcesCondition(err.Error()) - meta.SetStatusCondition(&p.cluster.Status.Conditions, condition) - p.recorder.Event(p.cluster, corev1.EventTypeWarning, condition.Reason, condition.Message) - rootVertex, _ := findRootVertex(p.dag) - if rootVertex == nil { - return nil - } - originCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - if originCluster == nil || reflect.DeepEqual(originCluster.Status, p.cluster.Status) { - return nil - } - return p.cli.Status().Patch(p.ctx.Ctx, p.cluster, client.MergeFrom(originCluster.DeepCopy())) +func (p *clusterPlan) handlePlanExecutionError(err error) error { + clusterCopy := p.transCtx.OrigCluster.DeepCopy() + condition := newFailedApplyResourcesCondition(err) + meta.SetStatusCondition(&clusterCopy.Status.Conditions, condition) + sendWaringEventWithError(p.transCtx.GetRecorder(), clusterCopy, ReasonApplyResourcesFailed, err) + return p.cli.Status().Patch(p.transCtx.Context, clusterCopy, client.MergeFrom(p.transCtx.OrigCluster)) } -func (c *clusterPlanBuilder) getClusterRefResources() (*clusterRefResources, error) { - cluster := c.cluster - cd := &appsv1alpha1.ClusterDefinition{} - if err := c.cli.Get(c.ctx.Ctx, types.NamespacedName{ - Name: cluster.Spec.ClusterDefRef, - }, cd); err != nil { - return nil, err - } - cv := &appsv1alpha1.ClusterVersion{} - if len(cluster.Spec.ClusterVersionRef) > 0 { - if err := c.cli.Get(c.ctx.Ctx, types.NamespacedName{ - Name: cluster.Spec.ClusterVersionRef, - }, cv); err != nil { - return nil, err - } +// Do the real works + +// NewClusterPlanBuilder returns a clusterPlanBuilder powered PlanBuilder +func NewClusterPlanBuilder(ctx intctrlutil.RequestCtx, cli client.Client, req ctrl.Request) graph.PlanBuilder { + return &clusterPlanBuilder{ + req: req, + cli: cli, + transCtx: &ClusterTransformContext{ + Context: ctx.Ctx, + Client: cli, + EventRecorder: ctx.Recorder, + Logger: ctx.Log, + }, } +} - cc := &clusterRefResources{ - cd: *cd, - cv: *cv, +func (c *clusterPlanBuilder) defaultWalkFuncWithLogging(vertex graph.Vertex) error { + node, ok := vertex.(*ictrltypes.LifecycleVertex) + err := c.defaultWalkFunc(vertex) + if err != nil { + if !ok { + c.transCtx.Logger.Error(err, "") + } else { + if node.Action == nil { + c.transCtx.Logger.Error(err, fmt.Sprintf("%T", node)) + } else { + c.transCtx.Logger.Error(err, fmt.Sprintf("%s %T error", *node.Action, node.Obj)) + } + } } - return cc, nil + return err } +// TODO: retry strategy on error func (c *clusterPlanBuilder) defaultWalkFunc(vertex graph.Vertex) error { - node, ok := vertex.(*lifecycleVertex) + node, ok := vertex.(*ictrltypes.LifecycleVertex) if !ok { return fmt.Errorf("wrong vertex type %v", vertex) } - if node.action == nil { + if node.Action == nil { return errors.New("node action can't be nil") } - updateComponentPhaseIfNeeded := func(orig, curr client.Object) { - switch orig.(type) { - case *appsv1.StatefulSet, *appsv1.Deployment: - componentName := orig.GetLabels()[constant.KBAppComponentLabelKey] - origSpec := reflect.ValueOf(orig).Elem().FieldByName("Spec").Interface() - newSpec := reflect.ValueOf(curr).Elem().FieldByName("Spec").Interface() - if !reflect.DeepEqual(origSpec, newSpec) { - // sync component phase - updateComponentPhaseWithOperation(c.cluster, componentName) - } - } - } + // cluster object has more business to do, handle them here - if _, ok := node.obj.(*appsv1alpha1.Cluster); ok { - cluster := node.obj.(*appsv1alpha1.Cluster).DeepCopy() - origCluster := node.oriObj.(*appsv1alpha1.Cluster) - switch *node.action { - // cluster.meta and cluster.spec might change - case CREATE, UPDATE, STATUS: - if !reflect.DeepEqual(cluster.ObjectMeta, origCluster.ObjectMeta) || - !reflect.DeepEqual(cluster.Spec, origCluster.Spec) { - // TODO: we should Update instead of Patch cluster object, - // TODO: but Update failure happens too frequently as other controllers are updating cluster object too. - // TODO: use Patch here, revert to Update after refactoring done - // if err := c.cli.Update(c.ctx.Ctx, cluster); err != nil { - // tmpCluster := &appsv1alpha1.Cluster{} - // err = c.cli.Get(c.ctx.Ctx,client.ObjectKeyFromObject(origCluster), tmpCluster) - // c.ctx.Log.Error(err, fmt.Sprintf("update %T error, orig: %v, curr: %v, api-server: %v", origCluster, origCluster, cluster, tmpCluster)) - // return err - // } - patch := client.MergeFrom(origCluster.DeepCopy()) - if err := c.cli.Patch(c.ctx.Ctx, cluster, patch); err != nil { - c.ctx.Log.Error(err, fmt.Sprintf("patch %T error, orig: %v, curr: %v", origCluster, origCluster, cluster)) - return err - } - } - case DELETE: - if err := c.handleClusterDeletion(cluster); err != nil { - return err - } - if cluster.Spec.TerminationPolicy == appsv1alpha1.DoNotTerminate { - return nil - } + if _, ok = node.Obj.(*appsv1alpha1.Cluster); ok { + if err := c.reconcileCluster(node); err != nil { + return err } } - switch *node.action { - case CREATE: - err := c.cli.Create(c.ctx.Ctx, node.obj) + return c.reconcileObject(node) +} + +func (c *clusterPlanBuilder) reconcileObject(node *ictrltypes.LifecycleVertex) error { + switch *node.Action { + case ictrltypes.CREATE: + err := c.cli.Create(c.transCtx.Context, node.Obj) if err != nil && !apierrors.IsAlreadyExists(err) { return err } - case UPDATE: - if node.immutable { + case ictrltypes.UPDATE: + if node.Immutable { return nil } - o, err := c.buildUpdateObj(node) - if err != nil { + err := c.cli.Update(c.transCtx.Context, node.Obj) + if err != nil && !apierrors.IsNotFound(err) { return err } - err = c.cli.Update(c.ctx.Ctx, o) - if err != nil && !apierrors.IsNotFound(err) { - c.ctx.Log.Error(err, fmt.Sprintf("update %T error, orig: %v, curr: %v", o, node.oriObj, o)) + case ictrltypes.PATCH: + patch := client.MergeFrom(node.ObjCopy) + if err := c.cli.Patch(c.transCtx.Context, node.Obj, patch); !apierrors.IsNotFound(err) { + c.transCtx.Logger.Error(err, fmt.Sprintf("patch %T error", node.ObjCopy)) return err } - // TODO: find a better comparison way that knows whether fields are updated before calling the Update func - updateComponentPhaseIfNeeded(node.oriObj, o) - case DELETE: - if controllerutil.RemoveFinalizer(node.obj, dbClusterFinalizerName) { - err := c.cli.Update(c.ctx.Ctx, node.obj) + case ictrltypes.DELETE: + if controllerutil.RemoveFinalizer(node.Obj, constant.DBClusterFinalizerName) { + err := c.cli.Update(c.transCtx.Context, node.Obj) if err != nil && !apierrors.IsNotFound(err) { - c.ctx.Log.Error(err, fmt.Sprintf("delete %T error, orig: %v, curr: %v", node.obj, node.oriObj, node.obj)) return err } } - if node.isOrphan { - err := c.cli.Delete(c.ctx.Ctx, node.obj) + // delete secondary objects + if _, ok := node.Obj.(*appsv1alpha1.Cluster); !ok { + // retain backup for data protection even if the cluster is wiped out. + if strings.EqualFold(node.Obj.GetLabels()[constant.BackupProtectionLabelKey], constant.BackupRetain) { + return nil + } + err := intctrlutil.BackgroundDeleteObject(c.cli, c.transCtx.Context, node.Obj) + // err := c.cli.Delete(c.transCtx.Context, node.obj) if err != nil && !apierrors.IsNotFound(err) { return err } } - // TODO: delete backup objects created in scale-out - // TODO: should manage backup objects in a better way - if isTypeOf[*snapshotv1.VolumeSnapshot](node.obj) || - isTypeOf[*dataprotectionv1alpha1.BackupPolicy](node.obj) || - isTypeOf[*dataprotectionv1alpha1.Backup](node.obj) { - _ = c.cli.Delete(c.ctx.Ctx, node.obj) - } - - case STATUS: - if node.immutable { - return nil - } - patch := client.MergeFrom(node.oriObj) - if err := c.cli.Status().Patch(c.ctx.Ctx, node.obj, patch); err != nil { + case ictrltypes.STATUS: + patch := client.MergeFrom(node.ObjCopy) + if err := c.cli.Status().Patch(c.transCtx.Context, node.Obj, patch); err != nil { return err } - for _, postHandle := range node.postHandleAfterStatusPatch { - if err := postHandle(); err != nil { - return err - } + // handle condition and phase changing triggered events + if newCluster, ok := node.Obj.(*appsv1alpha1.Cluster); ok { + oldCluster, _ := node.ObjCopy.(*appsv1alpha1.Cluster) + c.emitConditionUpdatingEvent(oldCluster.Status.Conditions, newCluster.Status.Conditions) + c.emitStatusUpdatingEvent(oldCluster.Status, newCluster.Status) } + case ictrltypes.NOOP: + // nothing } return nil } -func (c *clusterPlanBuilder) buildUpdateObj(node *lifecycleVertex) (client.Object, error) { - handleSts := func(origObj, stsProto *appsv1.StatefulSet) (client.Object, error) { - stsObj := origObj.DeepCopy() - componentName := stsObj.Labels[constant.KBAppComponentLabelKey] - if *stsObj.Spec.Replicas != *stsProto.Spec.Replicas { - c.recorder.Eventf(c.cluster, - corev1.EventTypeNormal, - "HorizontalScale", - "Start horizontal scale component %s from %d to %d", - componentName, - *stsObj.Spec.Replicas, - *stsProto.Spec.Replicas) - } - // keep the original template annotations. - // if annotations exist and are replaced, the statefulSet will be updated. - mergeAnnotations(stsObj.Spec.Template.Annotations, - &stsProto.Spec.Template.Annotations) - stsObj.Spec.Template = stsProto.Spec.Template - stsObj.Spec.Replicas = stsProto.Spec.Replicas - stsObj.Spec.UpdateStrategy = stsProto.Spec.UpdateStrategy - return stsObj, nil - } - - handleDeploy := func(origObj, deployProto *appsv1.Deployment) (client.Object, error) { - deployObj := origObj.DeepCopy() - mergeAnnotations(deployObj.Spec.Template.Annotations, - &deployProto.Spec.Template.Annotations) - deployObj.Spec = deployProto.Spec - return deployObj, nil - } - - handleSvc := func(origObj, svcProto *corev1.Service) (client.Object, error) { - svcObj := origObj.DeepCopy() - svcObj.Spec = svcProto.Spec - svcObj.Annotations = mergeServiceAnnotations(svcObj.Annotations, svcProto.Annotations) - return svcObj, nil - } - - handlePVC := func(origObj, pvcProto *corev1.PersistentVolumeClaim) (client.Object, error) { - pvcObj := origObj.DeepCopy() - if pvcObj.Spec.Resources.Requests[corev1.ResourceStorage] == pvcProto.Spec.Resources.Requests[corev1.ResourceStorage] { - return pvcObj, nil - } - pvcObj.Spec.Resources.Requests[corev1.ResourceStorage] = pvcProto.Spec.Resources.Requests[corev1.ResourceStorage] - return pvcObj, nil - } - - switch v := node.obj.(type) { - case *appsv1.StatefulSet: - return handleSts(node.oriObj.(*appsv1.StatefulSet), v) - case *appsv1.Deployment: - return handleDeploy(node.oriObj.(*appsv1.Deployment), v) - case *corev1.Service: - return handleSvc(node.oriObj.(*corev1.Service), v) - case *corev1.PersistentVolumeClaim: - return handlePVC(node.oriObj.(*corev1.PersistentVolumeClaim), v) - case *corev1.Secret, *corev1.ConfigMap: - return v, nil - } - - return node.obj, nil -} - -func (c *clusterPlanBuilder) handleClusterDeletion(cluster *appsv1alpha1.Cluster) error { - switch cluster.Spec.TerminationPolicy { - case appsv1alpha1.DoNotTerminate: - c.recorder.Eventf(cluster, corev1.EventTypeWarning, "DoNotTerminate", "spec.terminationPolicy %s is preventing deletion.", cluster.Spec.TerminationPolicy) - return nil - case appsv1alpha1.Delete, appsv1alpha1.WipeOut: - if err := c.deletePVCs(cluster); err != nil && !apierrors.IsNotFound(err) { - return err - } - // The backup policy must be cleaned up when the cluster is deleted. - // Automatic backup scheduling needs to be stopped at this point. - if err := c.deleteBackupPolicies(cluster); err != nil && !apierrors.IsNotFound(err) { - return err - } - if cluster.Spec.TerminationPolicy == appsv1alpha1.WipeOut { - // TODO check whether delete backups together with cluster is allowed - // wipe out all backups - if err := c.deleteBackups(cluster); err != nil && !apierrors.IsNotFound(err) { +func (c *clusterPlanBuilder) reconcileCluster(node *ictrltypes.LifecycleVertex) error { + cluster := node.Obj.(*appsv1alpha1.Cluster).DeepCopy() + origCluster := node.ObjCopy.(*appsv1alpha1.Cluster) + switch *node.Action { + // cluster.meta and cluster.spec might change + case ictrltypes.STATUS: + if !reflect.DeepEqual(cluster.ObjectMeta, origCluster.ObjectMeta) || !reflect.DeepEqual(cluster.Spec, origCluster.Spec) { + // TODO: we should Update instead of Patch cluster object, + // TODO: but Update failure happens too frequently as other controllers are updating cluster object too. + // TODO: use Patch here, revert to Update after refactoring done + // if err := c.cli.Update(c.ctx.Ctx, cluster); err != nil { + // tmpCluster := &appsv1alpha1.Cluster{} + // err = c.cli.Get(c.ctx.Ctx,client.ObjectKeyFromObject(origCluster), tmpCluster) + // c.ctx.Log.Error(err, fmt.Sprintf("update %T error, orig: %v, curr: %v, api-server: %v", origCluster, origCluster, cluster, tmpCluster)) + // return err + // } + patch := client.MergeFrom(origCluster.DeepCopy()) + if err := c.cli.Patch(c.transCtx.Context, cluster, patch); err != nil { + // log for debug + // TODO:(free6om) make error message smaller when refactor done. + c.transCtx.Logger.Error(err, fmt.Sprintf("patch %T error, orig: %v, curr: %v", origCluster, origCluster, cluster)) return err } } + case ictrltypes.CREATE, ictrltypes.UPDATE: + return fmt.Errorf("cluster can't be created or updated: %s", cluster.Name) } return nil } -func (c *clusterPlanBuilder) deletePVCs(cluster *appsv1alpha1.Cluster) error { - // it's possible at time of external resource deletion, cluster definition has already been deleted. - ml := client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.GetName(), - } - inNS := client.InNamespace(cluster.Namespace) - - pvcList := &corev1.PersistentVolumeClaimList{} - if err := c.cli.List(c.ctx.Ctx, pvcList, inNS, ml); err != nil { - return err - } - for _, pvc := range pvcList.Items { - if err := c.cli.Delete(c.ctx.Ctx, &pvc); err != nil { - return err +func (c *clusterPlanBuilder) emitConditionUpdatingEvent(oldConditions, newConditions []metav1.Condition) { + for _, newCondition := range newConditions { + oldCondition := meta.FindStatusCondition(oldConditions, newCondition.Type) + // filtered in cluster creation + if oldCondition == nil && newCondition.Status == metav1.ConditionFalse { + return + } + if !reflect.DeepEqual(oldCondition, &newCondition) { + eType := corev1.EventTypeNormal + if newCondition.Status == metav1.ConditionFalse { + eType = corev1.EventTypeWarning + } + c.transCtx.EventRecorder.Event(c.transCtx.Cluster, eType, newCondition.Reason, newCondition.Message) } } - return nil } -func (c *clusterPlanBuilder) deleteBackupPolicies(cluster *appsv1alpha1.Cluster) error { - inNS := client.InNamespace(cluster.Namespace) - ml := client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.GetName(), +func (c *clusterPlanBuilder) emitStatusUpdatingEvent(oldStatus, newStatus appsv1alpha1.ClusterStatus) { + cluster := c.transCtx.Cluster + if !reflect.DeepEqual(oldStatus, newStatus) { + _ = opsutil.MarkRunningOpsRequestAnnotation(c.transCtx.Context, c.cli, cluster) } - // clean backupPolicies - return c.cli.DeleteAllOf(c.ctx.Ctx, &dataprotectionv1alpha1.BackupPolicy{}, inNS, ml) -} - -func (c *clusterPlanBuilder) deleteBackups(cluster *appsv1alpha1.Cluster) error { - inNS := client.InNamespace(cluster.Namespace) - ml := client.MatchingLabels{ - constant.AppInstanceLabelKey: cluster.GetName(), + newPhase := newStatus.Phase + if newPhase == oldStatus.Phase { + return } - // clean backups - backups := &dataprotectionv1alpha1.BackupList{} - if err := c.cli.List(c.ctx.Ctx, backups, inNS, ml); err != nil { - return err + eType := corev1.EventTypeNormal + message := "" + switch newPhase { + case appsv1alpha1.RunningClusterPhase: + message = fmt.Sprintf("Cluster: %s is ready, current phase is %s", cluster.Name, newPhase) + case appsv1alpha1.StoppedClusterPhase: + message = fmt.Sprintf("Cluster: %s stopped successfully.", cluster.Name) + case appsv1alpha1.FailedClusterPhase, appsv1alpha1.AbnormalClusterPhase: + message = fmt.Sprintf("Cluster: %s is %s, check according to the components message", cluster.Name, newPhase) + eType = corev1.EventTypeWarning } - for _, backup := range backups.Items { - // check backup delete protection label - deleteProtection, exists := backup.GetLabels()[constant.BackupProtectionLabelKey] - // not found backup-protection or value is Delete, delete it. - if !exists || deleteProtection == constant.BackupDelete { - if err := c.cli.Delete(c.ctx.Ctx, &backup); err != nil { - return err - } - } + if len(message) > 0 { + c.transCtx.EventRecorder.Event(cluster, eType, string(newPhase), message) } - return nil } diff --git a/internal/controller/lifecycle/cluster_plan_utils.go b/internal/controller/lifecycle/cluster_plan_utils.go deleted file mode 100644 index 13ea656e1..000000000 --- a/internal/controller/lifecycle/cluster_plan_utils.go +++ /dev/null @@ -1,70 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "strings" - - "golang.org/x/exp/maps" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" -) - -// mergeAnnotations keeps the original annotations. -// if annotations exist and are replaced, the Deployment/StatefulSet will be updated. -func mergeAnnotations(originalAnnotations map[string]string, targetAnnotations *map[string]string) { - if targetAnnotations == nil { - return - } - if *targetAnnotations == nil { - *targetAnnotations = map[string]string{} - } - for k, v := range originalAnnotations { - // if the annotation not exist in targetAnnotations, copy it from original. - if _, ok := (*targetAnnotations)[k]; !ok { - (*targetAnnotations)[k] = v - } - } -} - -// mergeServiceAnnotations keeps the original annotations except prometheus scrape annotations. -// if annotations exist and are replaced, the Service will be updated. -func mergeServiceAnnotations(originalAnnotations, targetAnnotations map[string]string) map[string]string { - if len(originalAnnotations) == 0 { - return targetAnnotations - } - tmpAnnotations := make(map[string]string, len(originalAnnotations)+len(targetAnnotations)) - for k, v := range originalAnnotations { - if !strings.HasPrefix(k, "prometheus.io") { - tmpAnnotations[k] = v - } - } - maps.Copy(tmpAnnotations, targetAnnotations) - return tmpAnnotations -} - -// updateComponentPhaseWithOperation if workload of component changes, should update the component phase. -func updateComponentPhaseWithOperation(cluster *appsv1alpha1.Cluster, componentName string) { - componentPhase := appsv1alpha1.SpecReconcilingClusterCompPhase - if cluster.Status.Phase == appsv1alpha1.CreatingClusterPhase { - componentPhase = appsv1alpha1.CreatingClusterCompPhase - } - compStatus := cluster.Status.Components[componentName] - // synchronous component phase is consistent with cluster phase - compStatus.Phase = componentPhase - cluster.Status.SetComponentStatus(componentName, compStatus) -} diff --git a/internal/controller/lifecycle/cluster_plan_utils_test.go b/internal/controller/lifecycle/cluster_plan_utils_test.go deleted file mode 100644 index fcd505921..000000000 --- a/internal/controller/lifecycle/cluster_plan_utils_test.go +++ /dev/null @@ -1,72 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" -) - -var _ = Describe("cluster plan utils test", func() { - Context("test mergeServiceAnnotations", func() { - It("original and target annotations are nil", func() { - Expect(mergeServiceAnnotations(nil, nil)).Should(BeNil()) - }) - It("target annotations is nil", func() { - originalAnnotations := map[string]string{"k1": "v1"} - Expect(mergeServiceAnnotations(originalAnnotations, nil)).To(Equal(originalAnnotations)) - }) - It("original annotations is nil", func() { - targetAnnotations := map[string]string{"k1": "v1"} - Expect(mergeServiceAnnotations(nil, targetAnnotations)).To(Equal(targetAnnotations)) - }) - It("original annotations have prometheus annotations which should be removed", func() { - originalAnnotations := map[string]string{"k1": "v1", "prometheus.io/path": "/metrics"} - targetAnnotations := map[string]string{"k2": "v2"} - expectAnnotations := map[string]string{"k1": "v1", "k2": "v2"} - Expect(mergeServiceAnnotations(originalAnnotations, targetAnnotations)).To(Equal(expectAnnotations)) - }) - It("target annotations should override original annotations", func() { - originalAnnotations := map[string]string{"k1": "v1", "prometheus.io/path": "/metrics"} - targetAnnotations := map[string]string{"k1": "v11"} - expectAnnotations := map[string]string{"k1": "v11"} - Expect(mergeServiceAnnotations(originalAnnotations, targetAnnotations)).To(Equal(expectAnnotations)) - }) - - It("should merge annotations from original that not exist in target to final result", func() { - originalKey := "only-existing-in-original" - targetKey := "only-existing-in-target" - updatedKey := "updated-in-target" - originalAnnotations := map[string]string{ - originalKey: "true", - updatedKey: "false", - } - targetAnnotations := map[string]string{ - targetKey: "true", - updatedKey: "true", - } - mergeAnnotations(originalAnnotations, &targetAnnotations) - Expect(targetAnnotations[targetKey]).ShouldNot(BeEmpty()) - Expect(targetAnnotations[originalKey]).ShouldNot(BeEmpty()) - Expect(targetAnnotations[updatedKey]).Should(Equal("true")) - By("merging with target being nil") - var nilAnnotations map[string]string - mergeAnnotations(originalAnnotations, &nilAnnotations) - Expect(nilAnnotations).ShouldNot(BeNil()) - }) - }) -}) diff --git a/internal/controller/lifecycle/cluster_status_conditions.go b/internal/controller/lifecycle/cluster_status_conditions.go index bb24b8d61..4233166b2 100644 --- a/internal/controller/lifecycle/cluster_status_conditions.go +++ b/internal/controller/lifecycle/cluster_status_conditions.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle @@ -23,21 +26,23 @@ import ( "golang.org/x/exp/maps" "golang.org/x/exp/slices" + "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) const ( ReasonOpsRequestProcessed = "Processed" // ReasonOpsRequestProcessed the latest OpsRequest has been processed. - ReasonPreCheckSucceed = "PreCheckSucceed" // ReasonPreCheckSucceed preChecks succeed for provisioning started + ReasonPreCheckSucceed = "PreCheckSucceed" // ReasonPreCheckSucceed preChecks succeeded for provisioning started ReasonPreCheckFailed = "PreCheckFailed" // ReasonPreCheckFailed preChecks failed for provisioning started ReasonApplyResourcesFailed = "ApplyResourcesFailed" // ReasonApplyResourcesFailed applies resources failed to create or change the cluster - ReasonApplyResourcesSucceed = "ApplyResourcesSucceed" // ReasonApplyResourcesSucceed applies resources succeed to create or change the cluster + ReasonApplyResourcesSucceed = "ApplyResourcesSucceed" // ReasonApplyResourcesSucceed applies resources succeeded to create or change the cluster ReasonReplicasNotReady = "ReplicasNotReady" // ReasonReplicasNotReady the pods of components are not ready ReasonAllReplicasReady = "AllReplicasReady" // ReasonAllReplicasReady the pods of components are ready ReasonComponentsNotReady = "ComponentsNotReady" // ReasonComponentsNotReady the components of cluster are not ready - ReasonClusterReady = "ClusterReady" // ReasonClusterReady the components of cluster are ready, the component phase are running + ReasonClusterReady = "ClusterReady" // ReasonClusterReady the components of cluster are ready, the component phase is running ) // conditionIsChanged checks if the condition is changed. @@ -49,6 +54,14 @@ func conditionIsChanged(oldCondition *metav1.Condition, newCondition metav1.Cond return !reflect.DeepEqual(oldCondition, &newCondition) } +func setProvisioningStartedCondition(conditions *[]metav1.Condition, clusterName string, clusterGeneration int64, err error) { + condition := newProvisioningStartedCondition(clusterName, clusterGeneration) + if err != nil { + condition = newFailedProvisioningStartedCondition(err) + } + meta.SetStatusCondition(conditions, condition) +} + // newProvisioningStartedCondition creates the provisioning started condition in cluster conditions. func newProvisioningStartedCondition(clusterName string, clusterGeneration int64) metav1.Condition { return metav1.Condition{ @@ -60,14 +73,33 @@ func newProvisioningStartedCondition(clusterName string, clusterGeneration int64 } } +func getConditionReasonWithError(defaultReason string, err error) string { + if err == nil { + return defaultReason + } + controllerErr := intctrlutil.ToControllerError(err) + if controllerErr != nil { + defaultReason = string(controllerErr.Type) + } + return defaultReason +} + // newApplyResourcesCondition creates a condition when applied resources succeed. -func newFailedProvisioningStartedCondition(message, reason string) metav1.Condition { +func newFailedProvisioningStartedCondition(err error) metav1.Condition { return metav1.Condition{ Type: appsv1alpha1.ConditionTypeProvisioningStarted, Status: metav1.ConditionFalse, - Message: message, - Reason: reason, + Message: err.Error(), + Reason: getConditionReasonWithError(ReasonPreCheckFailed, err), + } +} + +func setApplyResourceCondition(conditions *[]metav1.Condition, clusterGeneration int64, err error) { + condition := newApplyResourcesCondition(clusterGeneration) + if err != nil { + condition = newFailedApplyResourcesCondition(err) } + meta.SetStatusCondition(conditions, condition) } // newApplyResourcesCondition creates a condition when applied resources succeed. @@ -82,12 +114,12 @@ func newApplyResourcesCondition(clusterGeneration int64) metav1.Condition { } // newApplyResourcesCondition creates a condition when applied resources succeed. -func newFailedApplyResourcesCondition(message string) metav1.Condition { +func newFailedApplyResourcesCondition(err error) metav1.Condition { return metav1.Condition{ Type: appsv1alpha1.ConditionTypeApplyResources, Status: metav1.ConditionFalse, - Message: message, - Reason: ReasonApplyResourcesFailed, + Message: err.Error(), + Reason: getConditionReasonWithError(ReasonApplyResourcesFailed, err), } } diff --git a/internal/controller/lifecycle/suite_test.go b/internal/controller/lifecycle/suite_test.go index 97dd91b8a..162b41439 100644 --- a/internal/controller/lifecycle/suite_test.go +++ b/internal/controller/lifecycle/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle diff --git a/internal/controller/lifecycle/transform_restore.go b/internal/controller/lifecycle/transform_restore.go new file mode 100644 index 000000000..18e736233 --- /dev/null +++ b/internal/controller/lifecycle/transform_restore.go @@ -0,0 +1,73 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/controllers/apps/components" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/controller/plan" + ictrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +type RestoreTransformer struct { + client.Client +} + +var _ graph.Transformer = &RestoreTransformer{} + +func (t *RestoreTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + clusterDef := transCtx.ClusterDef + clusterVer := transCtx.ClusterVer + reqCtx := ictrlutil.RequestCtx{ + Ctx: transCtx.Context, + Log: transCtx.Logger, + Recorder: transCtx.EventRecorder, + } + commitError := func(err error) error { + if ictrlutil.IsTargetError(err, ictrlutil.ErrorTypeNeedWaiting) { + transCtx.EventRecorder.Event(transCtx.Cluster, corev1.EventTypeNormal, string(ictrlutil.ErrorTypeNeedWaiting), err.Error()) + return graph.ErrPrematureStop + } + return err + } + for _, spec := range cluster.Spec.ComponentSpecs { + comp, err := components.NewComponent(reqCtx, t.Client, clusterDef, clusterVer, cluster, spec.Name, nil) + if err != nil { + return err + } + syncComp := comp.GetSynthesizedComponent() + if cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] != "" { + if err = plan.DoRestore(reqCtx.Ctx, t.Client, cluster, syncComp, scheme); err != nil { + return commitError(err) + } + } else if cluster.Annotations[constant.RestoreFromTimeAnnotationKey] != "" { + if err = plan.DoPITR(reqCtx.Ctx, t.Client, cluster, syncComp, scheme); err != nil { + return commitError(err) + } + } + } + return nil +} diff --git a/internal/controller/lifecycle/transform_types.go b/internal/controller/lifecycle/transform_types.go index 695041b4a..d27e4332c 100644 --- a/internal/controller/lifecycle/transform_types.go +++ b/internal/controller/lifecycle/transform_types.go @@ -1,26 +1,29 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle import ( - "fmt" "time" snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + batchv1 "k8s.io/api/batch/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -44,89 +47,27 @@ func init() { utilruntime.Must(dataprotectionv1alpha1.AddToScheme(scheme)) utilruntime.Must(snapshotv1.AddToScheme(scheme)) utilruntime.Must(extensionsv1alpha1.AddToScheme(scheme)) + utilruntime.Must(batchv1.AddToScheme(scheme)) } const ( // TODO: deduplicate - dbClusterFinalizerName = "cluster.kubeblocks.io/finalizer" clusterDefLabelKey = "clusterdefinition.kubeblocks.io/name" clusterVersionLabelKey = "clusterversion.kubeblocks.io/name" ) -type Action string - -const ( - CREATE = Action("CREATE") - UPDATE = Action("UPDATE") - DELETE = Action("DELETE") - STATUS = Action("STATUS") -) - // default reconcile requeue after duration var requeueDuration = time.Millisecond * 100 -type gvkName struct { - gvk schema.GroupVersionKind - ns, name string -} - -type clusterRefResources struct { - cd appsv1alpha1.ClusterDefinition - cv appsv1alpha1.ClusterVersion -} - -// lifecycleVertex describes expected object spec and how to reach it -// obj always represents the expected part: new object in Create/Update action and old object in Delete action -// oriObj is set in Update action -// all transformers doing their object manipulation works on obj.spec -// the root vertex(i.e. the cluster vertex) will be treated specially: -// as all its meta, spec and status can be updated in one reconciliation loop -// Update is ignored when immutable=true -// orphan object will be force deleted when action is DELETE -type lifecycleVertex struct { - obj client.Object - oriObj client.Object - immutable bool - isOrphan bool - action *Action - // postHandleAfterStatusPatch is called after the object status has changed - postHandleAfterStatusPatch []func() error -} - -func (v lifecycleVertex) String() string { - if v.action == nil { - return fmt.Sprintf("{obj:%T, immutable: %v, action: nil}", v.obj, v.immutable) - } - return fmt.Sprintf("{obj:%T, immutable: %v, action: %v}", v.obj, v.immutable, *v.action) +type gvkNObjKey struct { + schema.GroupVersionKind + client.ObjectKey } -type clusterSnapshot map[gvkName]client.Object - -type RequeueError interface { - RequeueAfter() time.Duration - Reason() string -} - -type realRequeueError struct { - reason string - requeueAfter time.Duration -} - -func (r *realRequeueError) Error() string { - return fmt.Sprintf("requeue after: %v as: %s", r.requeueAfter, r.reason) -} - -func (r *realRequeueError) RequeueAfter() time.Duration { - return r.requeueAfter -} - -func (r *realRequeueError) Reason() string { - return r.reason -} +type clusterOwningObjects map[gvkNObjKey]client.Object type delegateClient struct { client.Client } var _ client2.ReadonlyClient = delegateClient{} -var _ RequeueError = &realRequeueError{} diff --git a/internal/controller/lifecycle/transform_utils.go b/internal/controller/lifecycle/transform_utils.go index f0b80327f..6024f148b 100644 --- a/internal/controller/lifecycle/transform_utils.go +++ b/internal/controller/lifecycle/transform_utils.go @@ -1,163 +1,108 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle import ( - "fmt" + "reflect" "time" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/tools/record" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/client/apiutil" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - types2 "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/graph" + "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -func findAll[T interface{}](dag *graph.DAG) []graph.Vertex { - vertices := make([]graph.Vertex, 0) - for _, vertex := range dag.Vertices() { - v, _ := vertex.(*lifecycleVertex) - if _, ok := v.obj.(T); ok { - vertices = append(vertices, vertex) - } - } - return vertices -} - -func findAllNot[T interface{}](dag *graph.DAG) []graph.Vertex { - vertices := make([]graph.Vertex, 0) - for _, vertex := range dag.Vertices() { - v, _ := vertex.(*lifecycleVertex) - if _, ok := v.obj.(T); !ok { - vertices = append(vertices, vertex) - } - } - return vertices -} - -func findRootVertex(dag *graph.DAG) (*lifecycleVertex, error) { - root := dag.Root() - if root == nil { - return nil, fmt.Errorf("root vertex not found: %v", dag) - } - rootVertex, _ := root.(*lifecycleVertex) - return rootVertex, nil +func newRequeueError(after time.Duration, reason string) error { + return intctrlutil.NewRequeueError(after, reason) } -func getGVKName(object client.Object, scheme *runtime.Scheme) (*gvkName, error) { +func getGVKName(object client.Object, scheme *runtime.Scheme) (*gvkNObjKey, error) { gvk, err := apiutil.GVKForObject(object, scheme) if err != nil { return nil, err } - return &gvkName{ - gvk: gvk, - ns: object.GetNamespace(), - name: object.GetName(), + return &gvkNObjKey{ + GroupVersionKind: gvk, + ObjectKey: client.ObjectKey{ + Namespace: object.GetNamespace(), + Name: object.GetName(), + }, }, nil } -func isOwnerOf(owner, obj client.Object, scheme *runtime.Scheme) bool { - ro, ok := owner.(runtime.Object) - if !ok { - return false - } - gvk, err := apiutil.GVKForObject(ro, scheme) - if err != nil { - return false +func getAppInstanceML(cluster appsv1alpha1.Cluster) client.MatchingLabels { + return client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.Name, } - ref := metav1.OwnerReference{ - APIVersion: gvk.GroupVersion().String(), - Kind: gvk.Kind, - UID: owner.GetUID(), - Name: owner.GetName(), - } - owners := obj.GetOwnerReferences() - referSameObject := func(a, b metav1.OwnerReference) bool { - aGV, err := schema.ParseGroupVersion(a.APIVersion) - if err != nil { - return false - } +} - bGV, err := schema.ParseGroupVersion(b.APIVersion) - if err != nil { - return false +// func getAppInstanceAndManagedByML(cluster appsv1alpha1.Cluster) client.MatchingLabels { +// return client.MatchingLabels{ +// constant.AppInstanceLabelKey: cluster.Name, +// constant.AppManagedByLabelKey: constant.AppName, +// } +// } + +// getClusterOwningObjects reads objects owned by our cluster with kinds and label matching specifier. +func getClusterOwningObjects(transCtx *ClusterTransformContext, cluster appsv1alpha1.Cluster, + matchLabels client.MatchingLabels, kinds ...client.ObjectList) (clusterOwningObjects, error) { + // list what kinds of object cluster owns + objs := make(clusterOwningObjects) + inNS := client.InNamespace(cluster.Namespace) + for _, list := range kinds { + if err := transCtx.Client.List(transCtx.Context, list, inNS, matchLabels); err != nil { + return nil, err } - - return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name - } - for _, ownerRef := range owners { - if referSameObject(ownerRef, ref) { - return true + // reflect get list.Items + items := reflect.ValueOf(list).Elem().FieldByName("Items") + l := items.Len() + for i := 0; i < l; i++ { + // get the underlying object + object := items.Index(i).Addr().Interface().(client.Object) + name, err := getGVKName(object, scheme) + if err != nil { + return nil, err + } + objs[*name] = object } } - return false -} - -func actionPtr(action Action) *Action { - return &action -} - -func newRequeueError(after time.Duration, reason string) error { - return &realRequeueError{ - reason: reason, - requeueAfter: after, - } -} - -func isClusterDeleting(cluster appsv1alpha1.Cluster) bool { - return !cluster.GetDeletionTimestamp().IsZero() -} - -func isClusterUpdating(cluster appsv1alpha1.Cluster) bool { - return cluster.Status.ObservedGeneration != cluster.Generation -} - -func isClusterStatusUpdating(cluster appsv1alpha1.Cluster) bool { - return !isClusterDeleting(cluster) && !isClusterUpdating(cluster) - // return cluster.Status.ObservedGeneration == cluster.Generation && - // slices.Contains(appsv1alpha1.GetClusterTerminalPhases(), cluster.Status.Phase) + return objs, nil } -func getBackupObjects(reqCtx intctrlutil.RequestCtx, - cli types2.ReadonlyClient, - namespace string, - backupName string) (*dataprotectionv1alpha1.Backup, *dataprotectionv1alpha1.BackupTool, error) { - // get backup - backup := &dataprotectionv1alpha1.Backup{} - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Name: backupName, Namespace: namespace}, backup); err != nil { - return nil, nil, err +// sendWaringEventForCluster sends a warning event when occurs error. +func sendWaringEventWithError( + recorder record.EventRecorder, + cluster *appsv1alpha1.Cluster, + reason string, + err error) { + if err == nil { + return } - - // get backup tool - backupTool := &dataprotectionv1alpha1.BackupTool{} - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Name: backup.Status.BackupToolName}, backupTool); err != nil { - return nil, nil, err + controllerErr := intctrlutil.ToControllerError(err) + if controllerErr != nil { + reason = string(controllerErr.Type) } - return backup, backupTool, nil -} - -func isTypeOf[T interface{}](obj client.Object) bool { - _, ok := obj.(T) - return ok + recorder.Event(cluster, corev1.EventTypeWarning, reason, err.Error()) } diff --git a/internal/controller/lifecycle/transform_utils_test.go b/internal/controller/lifecycle/transform_utils_test.go index 52c78dd78..cb89fc441 100644 --- a/internal/controller/lifecycle/transform_utils_test.go +++ b/internal/controller/lifecycle/transform_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle diff --git a/internal/controller/lifecycle/transformer_assure_meta.go b/internal/controller/lifecycle/transformer_assure_meta.go new file mode 100644 index 000000000..9d450b968 --- /dev/null +++ b/internal/controller/lifecycle/transformer_assure_meta.go @@ -0,0 +1,60 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +type AssureMetaTransformer struct{} + +var _ graph.Transformer = &AssureMetaTransformer{} + +func (t *AssureMetaTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + + // The object is not being deleted, so if it does not have our finalizer, + // then lets add the finalizer and update the object. This is equivalent + // registering our finalizer. + if !controllerutil.ContainsFinalizer(cluster, constant.DBClusterFinalizerName) { + controllerutil.AddFinalizer(cluster, constant.DBClusterFinalizerName) + } + + // patch the label to prevent the label from being modified by the user. + labels := cluster.Labels + if labels == nil { + labels = map[string]string{} + } + cdLabelName := labels[clusterDefLabelKey] + cvLabelName := labels[clusterVersionLabelKey] + cdName, cvName := cluster.Spec.ClusterDefRef, cluster.Spec.ClusterVersionRef + if cdLabelName == cdName && cvLabelName == cvName { + return nil + } + labels[clusterDefLabelKey] = cdName + labels[clusterVersionLabelKey] = cvName + cluster.Labels = labels + + return nil +} diff --git a/internal/controller/lifecycle/transformer_backup_policy_tpl.go b/internal/controller/lifecycle/transformer_backup_policy_tpl.go new file mode 100644 index 000000000..3cfbf7030 --- /dev/null +++ b/internal/controller/lifecycle/transformer_backup_policy_tpl.go @@ -0,0 +1,389 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "fmt" + + "github.com/spf13/viper" + "golang.org/x/exp/slices" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// BackupPolicyTPLTransformer transforms the backup policy template to the backup policy. +type BackupPolicyTPLTransformer struct { + tplCount int + tplIdentifier string + isDefaultTemplate string +} + +var _ graph.Transformer = &BackupPolicyTPLTransformer{} + +const ( + trueVal = "true" +) + +func (r *BackupPolicyTPLTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + clusterDefName := transCtx.ClusterDef.Name + backupPolicyTPLs := &appsv1alpha1.BackupPolicyTemplateList{} + if err := transCtx.Client.List(transCtx.Context, backupPolicyTPLs, client.MatchingLabels{constant.ClusterDefLabelKey: clusterDefName}); err != nil { + return err + } + r.tplCount = len(backupPolicyTPLs.Items) + if r.tplCount == 0 { + return nil + } + rootVertex, err := ictrltypes.FindRootVertex(dag) + if err != nil { + return err + } + origCluster := transCtx.OrigCluster + backupPolicyNames := map[string]struct{}{} + for _, tpl := range backupPolicyTPLs.Items { + r.isDefaultTemplate = tpl.Annotations[constant.DefaultBackupPolicyTemplateAnnotationKey] + r.tplIdentifier = tpl.Spec.Identifier + for _, v := range tpl.Spec.BackupPolicies { + compDef := transCtx.ClusterDef.GetComponentDefByName(v.ComponentDefRef) + if compDef == nil { + return intctrlutil.NewNotFound("componentDef %s not found in ClusterDefinition: %s ", v.ComponentDefRef, clusterDefName) + } + // build the backup policy from the template. + backupPolicy, action := r.transformBackupPolicy(transCtx, v, origCluster, compDef.WorkloadType, &tpl) + if backupPolicy == nil { + continue + } + // if exist multiple backup policy templates and duplicate spec.identifier, + // the backupPolicy that may be generated may have duplicate names, and it is necessary to check if it already exists. + if _, ok := backupPolicyNames[backupPolicy.Name]; ok { + continue + } + vertex := &ictrltypes.LifecycleVertex{Obj: backupPolicy, Action: action} + dag.AddVertex(vertex) + dag.Connect(rootVertex, vertex) + backupPolicyNames[backupPolicy.Name] = struct{}{} + } + } + return nil +} + +// transformBackupPolicy transforms backup policy template to backup policy. +func (r *BackupPolicyTPLTransformer) transformBackupPolicy(transCtx *ClusterTransformContext, + policyTPL appsv1alpha1.BackupPolicy, + cluster *appsv1alpha1.Cluster, + workloadType appsv1alpha1.WorkloadType, + tpl *appsv1alpha1.BackupPolicyTemplate) (*dataprotectionv1alpha1.BackupPolicy, *ictrltypes.LifecycleAction) { + backupPolicyName := DeriveBackupPolicyName(cluster.Name, policyTPL.ComponentDefRef, r.tplIdentifier) + backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} + if err := transCtx.Client.Get(transCtx.Context, client.ObjectKey{Namespace: cluster.Namespace, Name: backupPolicyName}, backupPolicy); err != nil && !apierrors.IsNotFound(err) { + return nil, nil + } + if len(backupPolicy.Name) == 0 { + // build a new backup policy from the backup policy template. + return r.buildBackupPolicy(policyTPL, cluster, workloadType, tpl, backupPolicyName), ictrltypes.ActionCreatePtr() + } + // sync the existing backup policy with the cluster changes + r.syncBackupPolicy(backupPolicy, cluster, policyTPL, workloadType, tpl) + return backupPolicy, ictrltypes.ActionUpdatePtr() +} + +// syncBackupPolicy syncs labels and annotations of the backup policy with the cluster changes. +func (r *BackupPolicyTPLTransformer) syncBackupPolicy(backupPolicy *dataprotectionv1alpha1.BackupPolicy, + cluster *appsv1alpha1.Cluster, + policyTPL appsv1alpha1.BackupPolicy, + workloadType appsv1alpha1.WorkloadType, + tpl *appsv1alpha1.BackupPolicyTemplate) { + // update labels and annotations of the backup policy. + if backupPolicy.Annotations == nil { + backupPolicy.Annotations = map[string]string{} + } + backupPolicy.Annotations[constant.DefaultBackupPolicyAnnotationKey] = r.defaultPolicyAnnotationValue() + backupPolicy.Annotations[constant.BackupPolicyTemplateAnnotationKey] = tpl.Name + if tpl.Annotations[constant.ReconfigureRefAnnotationKey] != "" { + backupPolicy.Annotations[constant.ReconfigureRefAnnotationKey] = tpl.Annotations[constant.ReconfigureRefAnnotationKey] + } + if backupPolicy.Labels == nil { + backupPolicy.Labels = map[string]string{} + } + backupPolicy.Labels[constant.AppInstanceLabelKey] = cluster.Name + backupPolicy.Labels[constant.KBAppComponentDefRefLabelKey] = policyTPL.ComponentDefRef + backupPolicy.Labels[constant.AppManagedByLabelKey] = constant.AppName + + // only update the role labelSelector of the backup target instance when component workload is Replication/Consensus. + // because the replicas of component will change, such as 2->1. then if the target role is 'follower' and replicas is 1, + // the target instance can not be found. so we sync the label selector automatically. + if !slices.Contains([]appsv1alpha1.WorkloadType{appsv1alpha1.Replication, appsv1alpha1.Consensus}, workloadType) { + return + } + component := r.getFirstComponent(cluster, policyTPL.ComponentDefRef) + if component == nil { + return + } + // convert role labelSelector based on the replicas of the component automatically. + syncTheRoleLabel := func(target dataprotectionv1alpha1.TargetCluster, + basePolicy appsv1alpha1.BasePolicy) dataprotectionv1alpha1.TargetCluster { + role := basePolicy.Target.Role + if len(role) == 0 { + return target + } + if target.LabelsSelector == nil || target.LabelsSelector.MatchLabels == nil { + target.LabelsSelector = &metav1.LabelSelector{MatchLabels: map[string]string{}} + } + if component.Replicas == 1 { + // if replicas is 1, remove the role label selector. + delete(target.LabelsSelector.MatchLabels, constant.RoleLabelKey) + } else { + target.LabelsSelector.MatchLabels[constant.RoleLabelKey] = role + } + return target + } + if backupPolicy.Spec.Snapshot != nil && policyTPL.Snapshot != nil { + backupPolicy.Spec.Snapshot.Target = syncTheRoleLabel(backupPolicy.Spec.Snapshot.Target, + policyTPL.Snapshot.BasePolicy) + } + if backupPolicy.Spec.Datafile != nil && policyTPL.Datafile != nil { + backupPolicy.Spec.Datafile.Target = syncTheRoleLabel(backupPolicy.Spec.Datafile.Target, + policyTPL.Datafile.BasePolicy) + } + if backupPolicy.Spec.Logfile != nil && policyTPL.Logfile != nil { + backupPolicy.Spec.Logfile.Target = syncTheRoleLabel(backupPolicy.Spec.Logfile.Target, + policyTPL.Logfile.BasePolicy) + } +} + +// buildBackupPolicy builds a new backup policy from the backup policy template. +func (r *BackupPolicyTPLTransformer) buildBackupPolicy(policyTPL appsv1alpha1.BackupPolicy, + cluster *appsv1alpha1.Cluster, + workloadType appsv1alpha1.WorkloadType, + tpl *appsv1alpha1.BackupPolicyTemplate, + backupPolicyName string) *dataprotectionv1alpha1.BackupPolicy { + component := r.getFirstComponent(cluster, policyTPL.ComponentDefRef) + if component == nil { + return nil + } + + backupPolicy := &dataprotectionv1alpha1.BackupPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: backupPolicyName, + Namespace: cluster.Namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: cluster.Name, + constant.KBAppComponentDefRefLabelKey: policyTPL.ComponentDefRef, + constant.AppManagedByLabelKey: constant.AppName, + }, + Annotations: map[string]string{ + constant.DefaultBackupPolicyAnnotationKey: r.defaultPolicyAnnotationValue(), + constant.BackupPolicyTemplateAnnotationKey: tpl.Name, + constant.BackupDataPathPrefixAnnotationKey: fmt.Sprintf("/%s-%s/%s", cluster.Name, cluster.UID, component.Name), + }, + }, + } + if tpl.Annotations[constant.ReconfigureRefAnnotationKey] != "" { + backupPolicy.Annotations[constant.ReconfigureRefAnnotationKey] = tpl.Annotations[constant.ReconfigureRefAnnotationKey] + } + bpSpec := backupPolicy.Spec + if policyTPL.Retention != nil { + bpSpec.Retention = &dataprotectionv1alpha1.RetentionSpec{ + TTL: policyTPL.Retention.TTL, + } + } + + bpSpec.Schedule.Snapshot = r.convertSchedulePolicy(policyTPL.Schedule.Snapshot) + bpSpec.Schedule.Datafile = r.convertSchedulePolicy(policyTPL.Schedule.Datafile) + bpSpec.Schedule.Logfile = r.convertSchedulePolicy(policyTPL.Schedule.Logfile) + bpSpec.Datafile = r.convertCommonPolicy(policyTPL.Datafile, cluster.Name, *component, workloadType) + bpSpec.Logfile = r.convertCommonPolicy(policyTPL.Logfile, cluster.Name, *component, workloadType) + bpSpec.Snapshot = r.convertSnapshotPolicy(policyTPL.Snapshot, cluster.Name, *component, workloadType) + backupPolicy.Spec = bpSpec + return backupPolicy +} + +// getFirstComponent returns the first component name of the componentDefRef. +func (r *BackupPolicyTPLTransformer) getFirstComponent(cluster *appsv1alpha1.Cluster, + componentDefRef string) *appsv1alpha1.ClusterComponentSpec { + for _, v := range cluster.Spec.ComponentSpecs { + if v.ComponentDefRef == componentDefRef { + return &v + } + } + return nil +} + +// convertSchedulePolicy converts the schedulePolicy from backupPolicyTemplate. +func (r *BackupPolicyTPLTransformer) convertSchedulePolicy(sp *appsv1alpha1.SchedulePolicy) *dataprotectionv1alpha1.SchedulePolicy { + if sp == nil { + return nil + } + return &dataprotectionv1alpha1.SchedulePolicy{ + Enable: sp.Enable, + CronExpression: sp.CronExpression, + } +} + +// convertBasePolicy converts the basePolicy from backupPolicyTemplate. +func (r *BackupPolicyTPLTransformer) convertBasePolicy(bp appsv1alpha1.BasePolicy, + clusterName string, + component appsv1alpha1.ClusterComponentSpec, + workloadType appsv1alpha1.WorkloadType) dataprotectionv1alpha1.BasePolicy { + basePolicy := dataprotectionv1alpha1.BasePolicy{ + Target: dataprotectionv1alpha1.TargetCluster{ + LabelsSelector: &metav1.LabelSelector{ + MatchLabels: map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.KBAppComponentLabelKey: component.Name, + constant.AppManagedByLabelKey: constant.AppName, + }, + }, + }, + BackupsHistoryLimit: bp.BackupsHistoryLimit, + OnFailAttempted: bp.OnFailAttempted, + } + if len(bp.BackupStatusUpdates) != 0 { + backupStatusUpdates := make([]dataprotectionv1alpha1.BackupStatusUpdate, len(bp.BackupStatusUpdates)) + for i, v := range bp.BackupStatusUpdates { + backupStatusUpdates[i] = dataprotectionv1alpha1.BackupStatusUpdate{ + Path: v.Path, + ContainerName: v.ContainerName, + Script: v.Script, + UpdateStage: dataprotectionv1alpha1.BackupStatusUpdateStage(v.UpdateStage), + } + } + basePolicy.BackupStatusUpdates = backupStatusUpdates + } + switch workloadType { + case appsv1alpha1.Replication, appsv1alpha1.Consensus: + if len(bp.Target.Role) > 0 && component.Replicas > 1 { + // the role only works when the component has multiple replicas. + basePolicy.Target.LabelsSelector.MatchLabels[constant.RoleLabelKey] = bp.Target.Role + } + } + // build the target secret. + if len(bp.Target.Account) > 0 { + basePolicy.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{ + Name: fmt.Sprintf("%s-%s-%s", clusterName, component.Name, bp.Target.Account), + PasswordKey: constant.AccountPasswdForSecret, + UsernameKey: constant.AccountNameForSecret, + } + } else { + basePolicy.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{ + Name: fmt.Sprintf("%s-conn-credential", clusterName), + } + connectionCredentialKey := bp.Target.ConnectionCredentialKey + if connectionCredentialKey.PasswordKey != nil { + basePolicy.Target.Secret.PasswordKey = *connectionCredentialKey.PasswordKey + } + if connectionCredentialKey.UsernameKey != nil { + basePolicy.Target.Secret.UsernameKey = *connectionCredentialKey.UsernameKey + } + } + return basePolicy +} + +// convertBaseBackupSchedulePolicy converts the snapshotPolicy from backupPolicyTemplate. +func (r *BackupPolicyTPLTransformer) convertSnapshotPolicy(sp *appsv1alpha1.SnapshotPolicy, + clusterName string, + component appsv1alpha1.ClusterComponentSpec, + workloadType appsv1alpha1.WorkloadType) *dataprotectionv1alpha1.SnapshotPolicy { + if sp == nil { + return nil + } + snapshotPolicy := &dataprotectionv1alpha1.SnapshotPolicy{ + BasePolicy: r.convertBasePolicy(sp.BasePolicy, clusterName, component, workloadType), + } + if sp.Hooks != nil { + snapshotPolicy.Hooks = &dataprotectionv1alpha1.BackupPolicyHook{ + PreCommands: sp.Hooks.PreCommands, + PostCommands: sp.Hooks.PostCommands, + ContainerName: sp.Hooks.ContainerName, + Image: sp.Hooks.Image, + } + } + return snapshotPolicy +} + +// convertBaseBackupSchedulePolicy converts the commonPolicy from backupPolicyTemplate. +func (r *BackupPolicyTPLTransformer) convertCommonPolicy(bp *appsv1alpha1.CommonBackupPolicy, + clusterName string, + component appsv1alpha1.ClusterComponentSpec, + workloadType appsv1alpha1.WorkloadType) *dataprotectionv1alpha1.CommonBackupPolicy { + if bp == nil { + return nil + } + defaultCreatePolicy := dataprotectionv1alpha1.CreatePVCPolicyIfNotPresent + globalCreatePolicy := viper.GetString(constant.CfgKeyBackupPVCCreatePolicy) + if dataprotectionv1alpha1.CreatePVCPolicy(globalCreatePolicy) == dataprotectionv1alpha1.CreatePVCPolicyNever { + defaultCreatePolicy = dataprotectionv1alpha1.CreatePVCPolicyNever + } + defaultInitCapacity := constant.DefaultBackupPvcInitCapacity + globalInitCapacity := viper.GetString(constant.CfgKeyBackupPVCInitCapacity) + if len(globalInitCapacity) != 0 { + defaultInitCapacity = globalInitCapacity + } + // set the persistent volume configmap infos if these variables exist. + globalPVConfigMapName := viper.GetString(constant.CfgKeyBackupPVConfigmapName) + globalPVConfigMapNamespace := viper.GetString(constant.CfgKeyBackupPVConfigmapNamespace) + var persistentVolumeConfigMap *dataprotectionv1alpha1.PersistentVolumeConfigMap + if globalPVConfigMapName != "" && globalPVConfigMapNamespace != "" { + persistentVolumeConfigMap = &dataprotectionv1alpha1.PersistentVolumeConfigMap{ + Name: globalPVConfigMapName, + Namespace: globalPVConfigMapNamespace, + } + } + globalStorageClass := viper.GetString(constant.CfgKeyBackupPVCStorageClass) + var storageClassName *string + if globalStorageClass != "" { + storageClassName = &globalStorageClass + } + return &dataprotectionv1alpha1.CommonBackupPolicy{ + BackupToolName: bp.BackupToolName, + PersistentVolumeClaim: dataprotectionv1alpha1.PersistentVolumeClaim{ + InitCapacity: resource.MustParse(defaultInitCapacity), + CreatePolicy: defaultCreatePolicy, + PersistentVolumeConfigMap: persistentVolumeConfigMap, + StorageClassName: storageClassName, + }, + BasePolicy: r.convertBasePolicy(bp.BasePolicy, clusterName, component, workloadType), + } +} + +func (r *BackupPolicyTPLTransformer) defaultPolicyAnnotationValue() string { + if r.tplCount > 1 && r.isDefaultTemplate != trueVal { + return "false" + } + return trueVal +} + +// DeriveBackupPolicyName generates the backup policy name which is created from backup policy template. +func DeriveBackupPolicyName(clusterName, componentDef, identifier string) string { + if len(identifier) == 0 { + return fmt.Sprintf("%s-%s-backup-policy", clusterName, componentDef) + } + return fmt.Sprintf("%s-%s-backup-policy-%s", clusterName, componentDef, identifier) +} diff --git a/internal/controller/lifecycle/transformer_cluster.go b/internal/controller/lifecycle/transformer_cluster.go deleted file mode 100644 index 2a40432a9..000000000 --- a/internal/controller/lifecycle/transformer_cluster.go +++ /dev/null @@ -1,161 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "encoding/json" - "reflect" - - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - "github.com/apecloud/kubeblocks/internal/controller/builder" - "github.com/apecloud/kubeblocks/internal/controller/component" - "github.com/apecloud/kubeblocks/internal/controller/graph" - "github.com/apecloud/kubeblocks/internal/controller/plan" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// clusterTransformer transforms a Cluster to a K8s objects DAG -// TODO: remove cli and ctx, we should read all objects needed, and then do pure objects computation -// TODO: only replication set left -type clusterTransformer struct { - cc clusterRefResources - cli client.Client - ctx intctrlutil.RequestCtx -} - -func (c *clusterTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - - // return fast when cluster is deleting - if isClusterDeleting(*origCluster) { - return nil - } - - // we copy the K8s objects prepare stage directly first - // TODO: refactor plan.PrepareComponentResources - resourcesQueue := make([]client.Object, 0, 3) - task := intctrltypes.ReconcileTask{ - Cluster: cluster, - ClusterDefinition: &c.cc.cd, - ClusterVersion: &c.cc.cv, - Resources: &resourcesQueue, - } - - clusterBackupResourceMap, err := getClusterBackupSourceMap(cluster) - if err != nil { - return err - } - - clusterCompSpecMap := cluster.Spec.GetDefNameMappingComponents() - clusterCompVerMap := c.cc.cv.Spec.GetDefNameMappingComponents() - process1stComp := true - - // TODO: should move credential secrets creation from system_account_controller & here into credential_transformer, - // TODO: as those secrets are owned by the cluster - prepareComp := func(synthesizedComp *component.SynthesizedComponent) error { - iParams := task - iParams.Component = synthesizedComp - if process1stComp && len(synthesizedComp.Services) > 0 { - if err := prepareConnCredential(&iParams); err != nil { - return err - } - process1stComp = false - } - - // build info that needs to be restored from backup - backupSourceName := clusterBackupResourceMap[synthesizedComp.Name] - if len(backupSourceName) > 0 { - backup, backupTool, err := getBackupObjects(c.ctx, c.cli, cluster.Namespace, backupSourceName) - if err != nil { - return err - } - if err := component.BuildRestoredInfo2(synthesizedComp, backup, backupTool); err != nil { - return err - } - } - return plan.PrepareComponentResources(c.ctx, c.cli, &iParams) - } - - for _, compDef := range c.cc.cd.Spec.ComponentDefs { - compDefName := compDef.Name - compVer := clusterCompVerMap[compDefName] - compSpecs := clusterCompSpecMap[compDefName] - for _, compSpec := range compSpecs { - if err := prepareComp(component.BuildComponent(c.ctx, *cluster, c.cc.cd, compDef, compSpec, compVer)); err != nil { - return err - } - } - } - - // replication set will create duplicate env configmap and headless service - // dedup them - objects := deDupResources(*task.Resources) - // now task.Resources to DAG vertices - for _, object := range objects { - vertex := &lifecycleVertex{obj: object} - dag.AddVertex(vertex) - dag.Connect(rootVertex, vertex) - } - return nil -} - -func deDupResources(resources []client.Object) []client.Object { - objects := make([]client.Object, 0) - for _, resource := range resources { - contains := false - for _, object := range objects { - if reflect.DeepEqual(resource, object) { - contains = true - break - } - } - if !contains { - objects = append(objects, resource) - } - } - return objects -} - -func prepareConnCredential(task *intctrltypes.ReconcileTask) error { - secret, err := builder.BuildConnCredential(task.GetBuilderParams()) - if err != nil { - return err - } - // must make sure secret resources are created before workloads resources - task.AppendResource(secret) - return nil -} - -// getClusterBackupSourceMap gets the backup source map from cluster.annotations -func getClusterBackupSourceMap(cluster *appsv1alpha1.Cluster) (map[string]string, error) { - compBackupMapString := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] - if len(compBackupMapString) == 0 { - return nil, nil - } - compBackupMap := map[string]string{} - err := json.Unmarshal([]byte(compBackupMapString), &compBackupMap) - return compBackupMap, err -} diff --git a/internal/controller/lifecycle/transformer_cluster_credential.go b/internal/controller/lifecycle/transformer_cluster_credential.go new file mode 100644 index 000000000..c43f72e46 --- /dev/null +++ b/internal/controller/lifecycle/transformer_cluster_credential.go @@ -0,0 +1,69 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + corev1 "k8s.io/api/core/v1" + + "github.com/apecloud/kubeblocks/internal/controller/builder" + "github.com/apecloud/kubeblocks/internal/controller/component" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" +) + +// ClusterCredentialTransformer creates the connection credential secret +type ClusterCredentialTransformer struct{} + +var _ graph.Transformer = &ClusterCredentialTransformer{} + +func (c *ClusterCredentialTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + if cluster.IsDeleting() { + return nil + } + + root, err := ictrltypes.FindRootVertex(dag) + if err != nil { + return err + } + + var secret *corev1.Secret + for _, compDef := range transCtx.ClusterDef.Spec.ComponentDefs { + if compDef.Service == nil { + continue + } + + component := &component.SynthesizedComponent{ + Services: []corev1.Service{ + {Spec: compDef.Service.ToSVCSpec()}, + }, + } + if secret, err = builder.BuildConnCredentialLow(transCtx.ClusterDef, cluster, component); err != nil { + return err + } + break + } + + if secret != nil { + ictrltypes.LifecycleObjectCreate(dag, secret, root) + } + return nil +} diff --git a/internal/controller/lifecycle/transformer_cluster_deletion.go b/internal/controller/lifecycle/transformer_cluster_deletion.go new file mode 100644 index 000000000..fc3d9c01a --- /dev/null +++ b/internal/controller/lifecycle/transformer_cluster_deletion.go @@ -0,0 +1,189 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "encoding/json" + "strings" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + policyv1 "k8s.io/api/policy/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" +) + +// ClusterDeletionTransformer handles cluster deletion +type ClusterDeletionTransformer struct{} + +var _ graph.Transformer = &ClusterDeletionTransformer{} + +func (t *ClusterDeletionTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.OrigCluster + if !cluster.IsDeleting() { + return nil + } + root, err := ictrltypes.FindRootVertex(dag) + if err != nil { + return err + } + + // list all kinds to be deleted based on v1alpha1.TerminationPolicyType + var toDeleteKinds, toPreserveKinds []client.ObjectList + switch cluster.Spec.TerminationPolicy { + case appsv1alpha1.DoNotTerminate: + transCtx.EventRecorder.Eventf(cluster, corev1.EventTypeWarning, "DoNotTerminate", + "spec.terminationPolicy %s is preventing deletion.", cluster.Spec.TerminationPolicy) + return graph.ErrPrematureStop + case appsv1alpha1.Halt: + toDeleteKinds = kindsForHalt() + toPreserveKinds = []client.ObjectList{ + &corev1.PersistentVolumeClaimList{}, + &corev1.SecretList{}, + &corev1.ConfigMapList{}, + } + case appsv1alpha1.Delete: + toDeleteKinds = kindsForDelete() + case appsv1alpha1.WipeOut: + toDeleteKinds = kindsForWipeOut() + } + + transCtx.EventRecorder.Eventf(cluster, corev1.EventTypeNormal, constant.ReasonDeletingCR, "Deleting %s: %s", + strings.ToLower(cluster.GetObjectKind().GroupVersionKind().Kind), cluster.GetName()) + + // list all objects owned by this cluster in cache, and delete them all + // there is chance that objects leak occurs because of cache stale + // ignore the problem currently + // TODO: GC the leaked objects + ml := getAppInstanceML(*cluster) + + preserveObjects := func() error { + if len(toPreserveKinds) == 0 { + return nil + } + + objs, err := getClusterOwningObjects(transCtx, *cluster, ml, toPreserveKinds...) + if err != nil { + return err + } + // construct cluster spec JSON string + clusterSpec := cluster.DeepCopy() + clusterSpec.ObjectMeta = metav1.ObjectMeta{ + Name: cluster.GetName(), + UID: cluster.GetUID(), + } + clusterSpec.Status = appsv1alpha1.ClusterStatus{} + b, err := json.Marshal(*clusterSpec) + if err != nil { + return err + } + clusterJSON := string(b) + for _, o := range objs { + origObj := o.DeepCopyObject().(client.Object) + controllerutil.RemoveFinalizer(o, constant.DBClusterFinalizerName) + ownerRefs := o.GetOwnerReferences() + for i, ref := range ownerRefs { + if ref.Kind != appsv1alpha1.ClusterKind || + !strings.Contains(ref.APIVersion, appsv1alpha1.GroupVersion.Group) { + continue + } + ownerRefs = append(ownerRefs[:i], ownerRefs[i+1:]...) + break + } + o.SetOwnerReferences(ownerRefs) + annot := o.GetAnnotations() + if annot == nil { + annot = map[string]string{} + } + // annotated last-applied Cluster spec + annot[constant.LastAppliedClusterAnnotationKey] = clusterJSON + o.SetAnnotations(annot) + vertex := &ictrltypes.LifecycleVertex{Obj: o, ObjCopy: origObj, Action: ictrltypes.ActionUpdatePtr()} + dag.AddVertex(vertex) + dag.Connect(root, vertex) + } + return nil + } + // handle preserved objects update vertex + if err := preserveObjects(); err != nil { + return err + } + + // add objects deletion vertex + objs, err := getClusterOwningObjects(transCtx, *cluster, ml, toDeleteKinds...) + if err != nil { + return err + } + for _, o := range objs { + vertex := &ictrltypes.LifecycleVertex{Obj: o, Action: ictrltypes.ActionDeletePtr()} + dag.AddVertex(vertex) + dag.Connect(root, vertex) + } + root.Action = ictrltypes.ActionDeletePtr() + + // fast return, that is stopping the plan.Build() stage and jump to plan.Execute() directly + return graph.ErrPrematureStop +} + +func kindsForDoNotTerminate() []client.ObjectList { + return []client.ObjectList{} +} + +func kindsForHalt() []client.ObjectList { + kinds := kindsForDoNotTerminate() + kindsPlus := []client.ObjectList{ + &policyv1.PodDisruptionBudgetList{}, + &corev1.ServiceList{}, + &appsv1.StatefulSetList{}, + &appsv1.DeploymentList{}, + &corev1.ServiceList{}, + &policyv1.PodDisruptionBudgetList{}, + } + return append(kinds, kindsPlus...) +} + +func kindsForDelete() []client.ObjectList { + kinds := kindsForHalt() + kindsPlus := []client.ObjectList{ + &corev1.SecretList{}, + &corev1.ConfigMapList{}, + &corev1.PersistentVolumeClaimList{}, + &dataprotectionv1alpha1.BackupPolicyList{}, + &batchv1.JobList{}, + } + return append(kinds, kindsPlus...) +} + +func kindsForWipeOut() []client.ObjectList { + kinds := kindsForDelete() + kindsPlus := []client.ObjectList{ + &dataprotectionv1alpha1.BackupList{}, + } + return append(kinds, kindsPlus...) +} diff --git a/internal/controller/lifecycle/transformer_cluster_status.go b/internal/controller/lifecycle/transformer_cluster_status.go index abe0c72b8..4e05367e3 100644 --- a/internal/controller/lifecycle/transformer_cluster_status.go +++ b/internal/controller/lifecycle/transformer_cluster_status.go @@ -1,40 +1,33 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle import ( - "fmt" - "reflect" - "golang.org/x/exp/slices" - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/tools/record" - "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/util" opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" - "github.com/apecloud/kubeblocks/internal/constant" - "github.com/apecloud/kubeblocks/internal/controller/component" "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" ) // phaseSyncLevel defines a phase synchronization level to notify the status synchronizer how to handle cluster phase. @@ -47,11 +40,7 @@ const ( clusterExistFailedOrAbnormal // cluster exists failed or abnormal component ) -type clusterStatusTransformer struct { - cc clusterRefResources - cli client.Client - ctx intctrlutil.RequestCtx - recorder record.EventRecorder +type ClusterStatusTransformer struct { // phaseSyncLevel defines a phase synchronization level to indicate how to handle cluster phase. phaseSyncLevel phaseSyncLevel // existsAbnormalOrFailed indicates whether the cluster exists abnormal or failed component. @@ -62,120 +51,92 @@ type clusterStatusTransformer struct { replicasNotReadyCompNames map[string]struct{} } -func newClusterStatusTransformer(ctx intctrlutil.RequestCtx, - cli client.Client, - recorder record.EventRecorder, - cc clusterRefResources) *clusterStatusTransformer { - return &clusterStatusTransformer{ - ctx: ctx, - cc: cc, - cli: cli, - recorder: recorder, - phaseSyncLevel: clusterPhaseNoChange, - notReadyCompNames: map[string]struct{}{}, - replicasNotReadyCompNames: map[string]struct{}{}, - } -} -func (c *clusterStatusTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) +var _ graph.Transformer = &ClusterStatusTransformer{} + +func (t *ClusterStatusTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + origCluster := transCtx.OrigCluster + cluster := transCtx.Cluster + rootVertex, err := ictrltypes.FindRootVertex(dag) if err != nil { return err } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - - updateComponentPhase := func() { - vertices := findAllNot[*appsv1alpha1.Cluster](dag) - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - if v.immutable || v.action == nil || *v.action != CREATE { - continue - } - switch v.obj.(type) { - case *appsv1.StatefulSet, *appsv1.Deployment: - updateComponentPhaseWithOperation(cluster, v.obj.GetLabels()[constant.KBAppComponentLabelKey]) - } - } + + updateObservedGeneration := func() { + cluster.Status.ObservedGeneration = cluster.Generation + cluster.Status.ClusterDefGeneration = transCtx.ClusterDef.Generation + } + + initClusterStatusParams := func() { + t.phaseSyncLevel = clusterPhaseNoChange + t.notReadyCompNames = map[string]struct{}{} + t.replicasNotReadyCompNames = map[string]struct{}{} } switch { - case isClusterDeleting(*origCluster): + case origCluster.IsDeleting(): // if cluster is deleting, set root(cluster) vertex.action to DELETE - rootVertex.action = actionPtr(DELETE) - case isClusterUpdating(*origCluster): - c.ctx.Log.Info("update cluster status after applying resources ") - defer func() { - // update components' phase in cluster.status - updateComponentPhase() - rootVertex.action = actionPtr(STATUS) - rootVertex.immutable = reflect.DeepEqual(cluster.Status, origCluster.Status) - }() - cluster.Status.ObservedGeneration = cluster.Generation - cluster.Status.ClusterDefGeneration = c.cc.cd.Generation - applyResourcesCondition := newApplyResourcesCondition(cluster.Generation) - oldApplyCondition := meta.FindStatusCondition(cluster.Status.Conditions, applyResourcesCondition.Type) - if !conditionIsChanged(oldApplyCondition, applyResourcesCondition) { - return nil - } - meta.SetStatusCondition(&cluster.Status.Conditions, applyResourcesCondition) - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - c.recorder.Event(cluster, corev1.EventTypeNormal, applyResourcesCondition.Reason, applyResourcesCondition.Message) - return nil - }) - case isClusterStatusUpdating(*origCluster): - defer func() { - rootVertex.action = actionPtr(STATUS) - rootVertex.immutable = reflect.DeepEqual(cluster.Status, origCluster.Status) - }() - // checks if the controller is handling the garbage of restore. - if err := c.handleGarbageOfRestoreBeforeRunning(cluster); err != nil { - return err + rootVertex.Action = ictrltypes.ActionPtr(ictrltypes.DELETE) + // TODO(refactor): move from object action, check it again + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*ictrltypes.LifecycleVertex) + if *v.Action == ictrltypes.CREATE { + v.Action = ictrltypes.ActionPtr(ictrltypes.NOOP) + } else { + v.Action = ictrltypes.ActionPtr(ictrltypes.DELETE) + } } + case origCluster.IsUpdating(): + transCtx.Logger.Info("update cluster status after applying resources ") + updateObservedGeneration() + rootVertex.Action = ictrltypes.ActionStatusPtr() + case origCluster.IsStatusUpdating(): + initClusterStatusParams() + defer func() { rootVertex.Action = ictrltypes.ActionPtr(ictrltypes.STATUS) }() // reconcile the phase and conditions of the Cluster.status - if err := c.reconcileClusterStatus(cluster, rootVertex); err != nil { + if err := t.reconcileClusterStatus(transCtx, dag, cluster); err != nil { return err } - c.cleanupAnnotationsAfterRunning(cluster) } return nil } // reconcileClusterStatus reconciles phase and conditions of the Cluster.status. -func (c *clusterStatusTransformer) reconcileClusterStatus(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) error { +func (t *ClusterStatusTransformer) reconcileClusterStatus(transCtx *ClusterTransformContext, dag *graph.DAG, cluster *appsv1alpha1.Cluster) error { if len(cluster.Status.Components) == 0 { return nil } // removes the invalid component of status.components which is deleted from spec.components. - c.removeInvalidCompStatus(cluster) + t.removeInvalidCompStatus(cluster) // do analysis of Cluster.Status.component and update the results to status synchronizer. - c.doAnalysisAndUpdateSynchronizer(cluster) + t.doAnalysisAndUpdateSynchronizer(dag, cluster) // sync the LatestOpsRequestProcessed condition. - c.syncOpsRequestProcessedCondition(cluster, rootVertex) + t.syncOpsRequestProcessedCondition(cluster) // handle the ready condition. - c.syncReadyConditionForCluster(cluster, rootVertex) + t.syncReadyConditionForCluster(cluster) // sync the cluster phase. - switch c.phaseSyncLevel { + switch t.phaseSyncLevel { case clusterIsRunning: if cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { - c.syncClusterPhaseToRunning(cluster, rootVertex) + t.syncClusterPhaseToRunning(cluster) } case clusterIsStopped: if cluster.Status.Phase != appsv1alpha1.StoppedClusterPhase { - c.syncClusterPhaseToStopped(cluster, rootVertex) + t.syncClusterPhaseToStopped(cluster) } case clusterExistFailedOrAbnormal: - c.handleExistAbnormalOrFailed(cluster, rootVertex) + t.handleExistAbnormalOrFailed(transCtx, cluster) } return nil } // removeInvalidCompStatus removes the invalid component of status.components which is deleted from spec.components. -func (c *clusterStatusTransformer) removeInvalidCompStatus(cluster *appsv1alpha1.Cluster) { +func (t *ClusterStatusTransformer) removeInvalidCompStatus(cluster *appsv1alpha1.Cluster) { // remove the invalid component in status.components when the component is deleted from spec.components. tmpCompsStatus := map[string]appsv1alpha1.ClusterComponentStatus{} compsStatus := cluster.Status.Components @@ -188,8 +149,8 @@ func (c *clusterStatusTransformer) removeInvalidCompStatus(cluster *appsv1alpha1 cluster.Status.Components = tmpCompsStatus } -// doAnalysisAndUpdateSynchronizer analyses the Cluster.Status.Components and updates the results to the synchronizer. -func (c *clusterStatusTransformer) doAnalysisAndUpdateSynchronizer(cluster *appsv1alpha1.Cluster) { +// doAnalysisAndUpdateSynchronizer analyzes the Cluster.Status.Components and updates the results to the synchronizer. +func (t *ClusterStatusTransformer) doAnalysisAndUpdateSynchronizer(dag *graph.DAG, cluster *appsv1alpha1.Cluster) { var ( runningCompCount int stoppedCompCount int @@ -197,33 +158,35 @@ func (c *clusterStatusTransformer) doAnalysisAndUpdateSynchronizer(cluster *apps // analysis the status of components and calculate the cluster phase. for k, v := range cluster.Status.Components { if v.PodsReady == nil || !*v.PodsReady { - c.replicasNotReadyCompNames[k] = struct{}{} - c.notReadyCompNames[k] = struct{}{} + t.replicasNotReadyCompNames[k] = struct{}{} + t.notReadyCompNames[k] = struct{}{} } switch v.Phase { case appsv1alpha1.AbnormalClusterCompPhase, appsv1alpha1.FailedClusterCompPhase: - c.existsAbnormalOrFailed, c.notReadyCompNames[k] = true, struct{}{} + t.existsAbnormalOrFailed, t.notReadyCompNames[k] = true, struct{}{} case appsv1alpha1.RunningClusterCompPhase: + // if !isComponentInHorizontalScaling(dag, k) { runningCompCount += 1 + // } case appsv1alpha1.StoppedClusterCompPhase: stoppedCompCount += 1 } } - if c.existsAbnormalOrFailed { - c.phaseSyncLevel = clusterExistFailedOrAbnormal + if t.existsAbnormalOrFailed { + t.phaseSyncLevel = clusterExistFailedOrAbnormal return } switch len(cluster.Status.Components) { case runningCompCount: - c.phaseSyncLevel = clusterIsRunning + t.phaseSyncLevel = clusterIsRunning case stoppedCompCount: // cluster is Stopped when cluster is not Running and all components are Stopped or Running - c.phaseSyncLevel = clusterIsStopped + t.phaseSyncLevel = clusterIsStopped } } // handleOpsRequestProcessedCondition syncs the condition that OpsRequest has been processed. -func (c *clusterStatusTransformer) syncOpsRequestProcessedCondition(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) { +func (t *ClusterStatusTransformer) syncOpsRequestProcessedCondition(cluster *appsv1alpha1.Cluster) { opsCondition := meta.FindStatusCondition(cluster.Status.Conditions, appsv1alpha1.ConditionTypeLatestOpsRequestProcessed) if opsCondition == nil || opsCondition.Status == metav1.ConditionTrue { return @@ -238,182 +201,53 @@ func (c *clusterStatusTransformer) syncOpsRequestProcessedCondition(cluster *app return } meta.SetStatusCondition(&cluster.Status.Conditions, processedCondition) - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - // send an event when all pods of the components are ready. - c.recorder.Event(cluster, corev1.EventTypeNormal, processedCondition.Reason, processedCondition.Message) - return nil - }) } // syncReadyConditionForCluster syncs the cluster conditions with ClusterReady and ReplicasReady type. -func (c *clusterStatusTransformer) syncReadyConditionForCluster(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) { - if len(c.replicasNotReadyCompNames) == 0 { - oldReplicasReadyCondition := meta.FindStatusCondition(cluster.Status.Conditions, appsv1alpha1.ConditionTypeReplicasReady) +func (t *ClusterStatusTransformer) syncReadyConditionForCluster(cluster *appsv1alpha1.Cluster) { + if len(t.replicasNotReadyCompNames) == 0 { // if all replicas of cluster are ready, set ReasonAllReplicasReady to status.conditions readyCondition := newAllReplicasPodsReadyConditions() - if oldReplicasReadyCondition == nil || oldReplicasReadyCondition.Status == metav1.ConditionFalse { - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - // send an event when all pods of the components are ready. - c.recorder.Event(cluster, corev1.EventTypeNormal, readyCondition.Reason, readyCondition.Message) - return nil - }) - } meta.SetStatusCondition(&cluster.Status.Conditions, readyCondition) } else { - meta.SetStatusCondition(&cluster.Status.Conditions, newReplicasNotReadyCondition(c.replicasNotReadyCompNames)) + meta.SetStatusCondition(&cluster.Status.Conditions, newReplicasNotReadyCondition(t.replicasNotReadyCompNames)) } - if len(c.notReadyCompNames) > 0 { - meta.SetStatusCondition(&cluster.Status.Conditions, newComponentsNotReadyCondition(c.notReadyCompNames)) + if len(t.notReadyCompNames) > 0 { + meta.SetStatusCondition(&cluster.Status.Conditions, newComponentsNotReadyCondition(t.notReadyCompNames)) } } // syncClusterPhaseToRunning syncs the cluster phase to Running. -func (c *clusterStatusTransformer) syncClusterPhaseToRunning(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) { +func (t *ClusterStatusTransformer) syncClusterPhaseToRunning(cluster *appsv1alpha1.Cluster) { cluster.Status.Phase = appsv1alpha1.RunningClusterPhase meta.SetStatusCondition(&cluster.Status.Conditions, newClusterReadyCondition(cluster.Name)) - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - message := fmt.Sprintf("Cluster: %s is ready, current phase is Running", cluster.Name) - c.recorder.Event(cluster, corev1.EventTypeNormal, string(appsv1alpha1.RunningClusterPhase), message) - return opsutil.MarkRunningOpsRequestAnnotation(c.ctx.Ctx, c.cli, cluster) - }) } // syncClusterToStopped syncs the cluster phase to Stopped. -func (c *clusterStatusTransformer) syncClusterPhaseToStopped(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) { +func (t *ClusterStatusTransformer) syncClusterPhaseToStopped(cluster *appsv1alpha1.Cluster) { cluster.Status.Phase = appsv1alpha1.StoppedClusterPhase - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - message := fmt.Sprintf("Cluster: %s stopped successfully.", cluster.Name) - c.recorder.Event(cluster, corev1.EventTypeNormal, string(cluster.Status.Phase), message) - return opsutil.MarkRunningOpsRequestAnnotation(c.ctx.Ctx, c.cli, cluster) - }) } // handleExistAbnormalOrFailed handles the cluster status when some components are not ready. -func (c *clusterStatusTransformer) handleExistAbnormalOrFailed(cluster *appsv1alpha1.Cluster, rootVertex *lifecycleVertex) { - oldPhase := cluster.Status.Phase +func (t *ClusterStatusTransformer) handleExistAbnormalOrFailed(transCtx *ClusterTransformContext, cluster *appsv1alpha1.Cluster) { componentMap, clusterAvailabilityEffectMap, _ := getComponentRelatedInfo(cluster, - c.cc.cd, "") + *transCtx.ClusterDef, "") // handle the cluster status when some components are not ready. handleClusterPhaseWhenCompsNotReady(cluster, componentMap, clusterAvailabilityEffectMap) - currPhase := cluster.Status.Phase - if slices.Contains(appsv1alpha1.GetClusterFailedPhases(), currPhase) && oldPhase != currPhase { - rootVertex.postHandleAfterStatusPatch = append(rootVertex.postHandleAfterStatusPatch, func() error { - message := fmt.Sprintf("Cluster: %s is %s, check according to the components message", - cluster.Name, currPhase) - c.recorder.Event(cluster, corev1.EventTypeWarning, string(cluster.Status.Phase), message) - return opsutil.MarkRunningOpsRequestAnnotation(c.ctx.Ctx, c.cli, cluster) - }) - } -} - -// cleanupAnnotationsAfterRunning cleans up the cluster annotations after cluster is Running. -func (c *clusterStatusTransformer) cleanupAnnotationsAfterRunning(cluster *appsv1alpha1.Cluster) { - if !slices.Contains(appsv1alpha1.GetClusterTerminalPhases(), cluster.Status.Phase) { - return - } - if _, ok := cluster.Annotations[constant.RestoreFromBackUpAnnotationKey]; !ok { - return - } - delete(cluster.Annotations, constant.RestoreFromBackUpAnnotationKey) -} - -// REVIEW: this handling is rather hackish, call for refactor. -// handleRestoreGarbageBeforeRunning handles the garbage for restore before cluster phase changes to Running. -// @return ErrNoOps if no operation -// Deprecated: to be removed by PITR feature. -func (c *clusterStatusTransformer) handleGarbageOfRestoreBeforeRunning(cluster *appsv1alpha1.Cluster) error { - clusterBackupResourceMap, err := getClusterBackupSourceMap(cluster) - if err != nil { - return err - } - if clusterBackupResourceMap == nil { - return nil - } - // check if all components are running. - for _, v := range cluster.Status.Components { - if v.Phase != appsv1alpha1.RunningClusterCompPhase { - return nil - } - } - // remove the garbage for restore if the cluster restores from backup. - return c.removeGarbageWithRestore(cluster, clusterBackupResourceMap) -} - -// REVIEW: this handling is rather hackish, call for refactor. -// removeGarbageWithRestore removes the garbage for restore when all components are Running. -// @return ErrNoOps if no operation -// Deprecated: -func (c *clusterStatusTransformer) removeGarbageWithRestore( - cluster *appsv1alpha1.Cluster, - clusterBackupResourceMap map[string]string) error { - var ( - err error - ) - for k, v := range clusterBackupResourceMap { - // remove the init container for restore - if _, err = c.removeStsInitContainerForRestore(cluster, k, v); err != nil { - return err - } - } - return nil -} - -// removeStsInitContainerForRestore removes the statefulSet's init container which restores data from backup. -func (c *clusterStatusTransformer) removeStsInitContainerForRestore( - cluster *appsv1alpha1.Cluster, - componentName, - backupName string) (bool, error) { - // get the sts list of component - stsList := &appsv1.StatefulSetList{} - if err := util.GetObjectListByComponentName(c.ctx.Ctx, c.cli, *cluster, stsList, componentName); err != nil { - return false, err - } - var doRemoveInitContainers bool - for _, sts := range stsList.Items { - initContainers := sts.Spec.Template.Spec.InitContainers - restoreInitContainerName := component.GetRestoredInitContainerName(backupName) - restoreInitContainerIndex, _ := intctrlutil.GetContainerByName(initContainers, restoreInitContainerName) - if restoreInitContainerIndex == -1 { - continue - } - doRemoveInitContainers = true - initContainers = append(initContainers[:restoreInitContainerIndex], initContainers[restoreInitContainerIndex+1:]...) - sts.Spec.Template.Spec.InitContainers = initContainers - if err := c.cli.Update(c.ctx.Ctx, &sts); err != nil { - return false, err - } - } - if doRemoveInitContainers { - // if need to remove init container, reset component to Creating. - compStatus := cluster.Status.Components[componentName] - compStatus.Phase = appsv1alpha1.CreatingClusterCompPhase - cluster.Status.Components[componentName] = compStatus - } - return doRemoveInitContainers, nil } // handleClusterPhaseWhenCompsNotReady handles the Cluster.status.phase when some components are Abnormal or Failed. -// REVIEW: seem duplicated handling -// Deprecated: func handleClusterPhaseWhenCompsNotReady(cluster *appsv1alpha1.Cluster, componentMap map[string]string, clusterAvailabilityEffectMap map[string]bool) { var ( - clusterIsFailed bool - failedCompCount int - isVolumeExpanding bool + clusterIsFailed bool + failedCompCount int ) - opsRecords, _ := opsutil.GetOpsRequestSliceFromCluster(cluster) - if len(opsRecords) != 0 && opsRecords[0].Type == appsv1alpha1.VolumeExpansionType { - isVolumeExpanding = true - } for k, v := range cluster.Status.Components { - // determine whether other components are still doing operation, i.e., create/restart/scaling. - // waiting for operation to complete except for volumeExpansion operation. - // because this operation will not affect cluster availability. - if !slices.Contains(appsv1alpha1.GetComponentTerminalPhases(), v.Phase) && !isVolumeExpanding { + if !slices.Contains(appsv1alpha1.GetComponentTerminalPhases(), v.Phase) { return } if v.Phase == appsv1alpha1.FailedClusterCompPhase { @@ -434,16 +268,14 @@ func handleClusterPhaseWhenCompsNotReady(cluster *appsv1alpha1.Cluster, } } -// getClusterAvailabilityEffect whether the component will affect the cluster availability. -// if the component can affect and be Failed, the cluster will be Failed too. +// getClusterAvailabilityEffect checks whether the component affect the cluster availability. +// if the component affects the availability and being Failed, the cluster is also Failed. func getClusterAvailabilityEffect(componentDef *appsv1alpha1.ClusterComponentDefinition) bool { switch componentDef.WorkloadType { - case appsv1alpha1.Consensus: - return true - case appsv1alpha1.Replication: + case appsv1alpha1.Consensus, appsv1alpha1.Replication: return true default: - return componentDef.MaxUnavailable != nil + return componentDef.GetMaxUnavailable() != nil } } diff --git a/internal/controller/lifecycle/transformer_component.go b/internal/controller/lifecycle/transformer_component.go new file mode 100644 index 000000000..3536c3f74 --- /dev/null +++ b/internal/controller/lifecycle/transformer_component.go @@ -0,0 +1,159 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "k8s.io/apimachinery/pkg/util/sets" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/controllers/apps/components" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + ictrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// ComponentTransformer transforms all components to a K8s objects DAG +type ComponentTransformer struct { + client.Client +} + +var _ graph.Transformer = &ComponentTransformer{} + +func (c *ComponentTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + origCluster := transCtx.OrigCluster + cluster := transCtx.Cluster + if origCluster.IsDeleting() { + return nil + } + + clusterDef := transCtx.ClusterDef + clusterVer := transCtx.ClusterVer + reqCtx := ictrlutil.RequestCtx{ + Ctx: transCtx.Context, + Log: transCtx.Logger, + Recorder: transCtx.EventRecorder, + } + + var err error + dags4Component := make([]*graph.DAG, 0) + if cluster.IsStatusUpdating() { + // status existed components + err = c.transform4StatusUpdate(reqCtx, clusterDef, clusterVer, cluster, &dags4Component) + } else { + // create new components or update existed components + err = c.transform4SpecUpdate(reqCtx, clusterDef, clusterVer, cluster, &dags4Component) + } + if err != nil && !ictrlutil.IsDelayedRequeueError(err) { + return err + } + + for _, subDag := range dags4Component { + for _, v := range subDag.Vertices() { + node, ok := v.(*ictrltypes.LifecycleVertex) + if !ok { + panic("runtime error, unexpected lifecycle vertex type") + } + if node.Obj == nil { + panic("runtime error, nil vertex object") + } + } + dag.Merge(subDag) + } + return err +} + +func (c *ComponentTransformer) transform4SpecUpdate(reqCtx ictrlutil.RequestCtx, clusterDef *appsv1alpha1.ClusterDefinition, + clusterVer *appsv1alpha1.ClusterVersion, cluster *appsv1alpha1.Cluster, dags *[]*graph.DAG) error { + compSpecMap := make(map[string]*appsv1alpha1.ClusterComponentSpec) + for _, spec := range cluster.Spec.ComponentSpecs { + compSpecMap[spec.Name] = &spec + } + compProto := sets.KeySet(compSpecMap) + // TODO(refactor): should review that whether it is reasonable to use component status + compStatus := sets.KeySet(cluster.Status.Components) + + createSet := compProto.Difference(compStatus) + updateSet := compProto.Intersection(compStatus) + deleteSet := compStatus.Difference(compProto) + + for compName := range createSet { + dag := graph.NewDAG() + comp, err := components.NewComponent(reqCtx, c.Client, clusterDef, clusterVer, cluster, compName, dag) + if err != nil { + return err + } + if err := comp.Create(reqCtx, c.Client); err != nil { + return err + } + *dags = append(*dags, dag) + } + + for compName := range deleteSet { + dag := graph.NewDAG() + comp, err := components.NewComponent(reqCtx, c.Client, clusterDef, clusterVer, cluster, compName, dag) + if err != nil { + return err + } + if comp != nil { + if err := comp.Delete(reqCtx, c.Client); err != nil { + return err + } + } + *dags = append(*dags, dag) + } + + for compName := range updateSet { + dag := graph.NewDAG() + comp, err := components.NewComponent(reqCtx, c.Client, clusterDef, clusterVer, cluster, compName, dag) + if err != nil { + return err + } + if err := comp.Update(reqCtx, c.Client); err != nil { + return err + } + *dags = append(*dags, dag) + } + + return nil +} + +func (c *ComponentTransformer) transform4StatusUpdate(reqCtx ictrlutil.RequestCtx, clusterDef *appsv1alpha1.ClusterDefinition, + clusterVer *appsv1alpha1.ClusterVersion, cluster *appsv1alpha1.Cluster, dags *[]*graph.DAG) error { + var delayedError error + for _, compSpec := range cluster.Spec.ComponentSpecs { + dag := graph.NewDAG() + comp, err := components.NewComponent(reqCtx, c.Client, clusterDef, clusterVer, cluster, compSpec.Name, dag) + if err != nil { + return err + } + if err := comp.Status(reqCtx, c.Client); err != nil { + if !ictrlutil.IsDelayedRequeueError(err) { + return err + } + if delayedError == nil { + delayedError = err + } + } + *dags = append(*dags, dag) + } + return delayedError +} diff --git a/internal/controller/lifecycle/transformer_config.go b/internal/controller/lifecycle/transformer_config.go index 18d2e774b..4ec52d619 100644 --- a/internal/controller/lifecycle/transformer_config.go +++ b/internal/controller/lifecycle/transformer_config.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle @@ -19,25 +22,24 @@ package lifecycle import ( corev1 "k8s.io/api/core/v1" + cfgcore "github.com/apecloud/kubeblocks/internal/configuration" "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" ) -// configTransformer makes all config related ConfigMaps immutable -type configTransformer struct{} +// ConfigTransformer makes all config related ConfigMaps immutable +type ConfigTransformer struct{} -func (c *configTransformer) Transform(dag *graph.DAG) error { - cmVertices := findAll[*corev1.ConfigMap](dag) - isConfig := func(cm *corev1.ConfigMap) bool { - // TODO: we should find a way to know if cm is a true config - // TODO: the main problem is we can't separate script from config, - // TODO: as componentDef.ConfigSpec defines them in same way - return false - } - for _, vertex := range cmVertices { - v, _ := vertex.(*lifecycleVertex) - cm, _ := v.obj.(*corev1.ConfigMap) - if isConfig(cm) { - v.immutable = true +var _ graph.Transformer = &ConfigTransformer{} + +func (c *ConfigTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + for _, vertex := range ictrltypes.FindAll[*corev1.ConfigMap](dag) { + v, _ := vertex.(*ictrltypes.LifecycleVertex) + cm, _ := v.Obj.(*corev1.ConfigMap) + // Note: Disable updating of the config resources. + // Labels and Annotations have the necessary meta information for controller. + if cfgcore.IsSchedulableConfigResource(cm) { + v.Immutable = true } } return nil diff --git a/internal/controller/lifecycle/transformer_credential.go b/internal/controller/lifecycle/transformer_credential.go deleted file mode 100644 index 4f58e5796..000000000 --- a/internal/controller/lifecycle/transformer_credential.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - corev1 "k8s.io/api/core/v1" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/graph" -) - -// credentialTransformer puts the credential Secret at the beginning of the DAG -type credentialTransformer struct{} - -func (c *credentialTransformer) Transform(dag *graph.DAG) error { - var secretVertices, noneRootVertices []graph.Vertex - secretVertices = findAll[*corev1.Secret](dag) - noneRootVertices = findAllNot[*appsv1alpha1.Cluster](dag) - for _, secretVertex := range secretVertices { - secret, _ := secretVertex.(*lifecycleVertex) - secret.immutable = true - for _, vertex := range noneRootVertices { - v, _ := vertex.(*lifecycleVertex) - // connect all none secret vertices to all secret vertices - if _, ok := v.obj.(*corev1.Secret); !ok { - dag.Connect(vertex, secretVertex) - } - } - } - return nil -} diff --git a/internal/controller/lifecycle/transformer_do_not_terminate.go b/internal/controller/lifecycle/transformer_do_not_terminate.go deleted file mode 100644 index d68a7ddcd..000000000 --- a/internal/controller/lifecycle/transformer_do_not_terminate.go +++ /dev/null @@ -1,45 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/graph" -) - -type doNotTerminateTransformer struct{} - -func (d *doNotTerminateTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - cluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - - if cluster.DeletionTimestamp.IsZero() { - return nil - } - if cluster.Spec.TerminationPolicy != appsv1alpha1.DoNotTerminate { - return nil - } - vertices := findAllNot[*appsv1alpha1.Cluster](dag) - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - v.immutable = true - } - return nil -} diff --git a/internal/controller/lifecycle/transformer_fill_class.go b/internal/controller/lifecycle/transformer_fill_class.go index 55ee0c104..195043756 100644 --- a/internal/controller/lifecycle/transformer_fill_class.go +++ b/internal/controller/lifecycle/transformer_fill_class.go @@ -1,157 +1,120 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + package lifecycle import ( - "encoding/json" "fmt" "sort" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" "github.com/apecloud/kubeblocks/internal/class" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// fixClusterLabelsTransformer fill the class related info to cluster -type fillClass struct { - cc clusterRefResources - cli client.Client - ctx intctrlutil.RequestCtx -} +// FillClassTransformer fills the class related info to cluster +type FillClassTransformer struct{} -func (r *fillClass) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err +var _ graph.Transformer = &FillClassTransformer{} + +func (r *FillClassTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + if cluster.IsDeleting() { + return nil } - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - return r.fillClass(r.ctx, cluster, r.cc.cd) + return r.fillClass(transCtx) } -func (r *fillClass) fillClass(reqCtx intctrlutil.RequestCtx, cluster *appsv1alpha1.Cluster, clusterDefinition appsv1alpha1.ClusterDefinition) error { +func (r *FillClassTransformer) fillClass(transCtx *ClusterTransformContext) error { + cluster := transCtx.Cluster + clusterDefinition := transCtx.ClusterDef + var ( - value = cluster.GetAnnotations()[constant.ClassAnnotationKey] - componentClassMapping = make(map[string]string) - cmList corev1.ConfigMapList + classDefinitionList appsv1alpha1.ComponentClassDefinitionList ) - if value != "" { - if err := json.Unmarshal([]byte(value), &componentClassMapping); err != nil { - return err - } - } - cmLabels := []client.ListOption{ + ml := []client.ListOption{ client.MatchingLabels{constant.ClusterDefLabelKey: clusterDefinition.Name}, - client.HasLabels{constant.ClassProviderLabelKey}, } - if err := r.cli.List(reqCtx.Ctx, &cmList, cmLabels...); err != nil { + if err := transCtx.Client.List(transCtx.Context, &classDefinitionList, ml...); err != nil { return err } - compClasses, err := class.ParseClasses(&cmList) + compClasses, err := class.GetClasses(classDefinitionList) if err != nil { return err } - var classFamilyList appsv1alpha1.ClassFamilyList - if err = r.cli.List(reqCtx.Ctx, &classFamilyList); err != nil { + var constraintList appsv1alpha1.ComponentResourceConstraintList + if err = transCtx.Client.List(transCtx.Context, &constraintList); err != nil { return err } - // TODO use this function to get matched class families if class is not specified and component has no classes - _ = func(comp appsv1alpha1.ClusterComponentSpec) *class.ComponentClass { - var candidates []class.ClassModelWithFamilyName - for _, family := range classFamilyList.Items { - models := family.FindMatchingModels(&comp.Resources) - for _, model := range models { - candidates = append(candidates, class.ClassModelWithFamilyName{Family: family.Name, Model: model}) + // TODO use this function to get matched resource constraints if class is not specified and component has no classes + _ = func(comp appsv1alpha1.ClusterComponentSpec) *appsv1alpha1.ComponentClassInstance { + var candidates []class.ConstraintWithName + for _, item := range constraintList.Items { + constraints := item.FindMatchingConstraints(&comp.Resources) + for _, constraint := range constraints { + candidates = append(candidates, class.ConstraintWithName{Name: item.Name, Constraint: constraint}) } } if len(candidates) == 0 { return nil } - sort.Sort(class.ByModelList(candidates)) + sort.Sort(class.ByConstraintList(candidates)) candidate := candidates[0] - cpu, memory := class.GetMinCPUAndMemory(candidate.Model) - cls := &class.ComponentClass{ - Name: fmt.Sprintf("%s-%vc%vg", candidate.Family, cpu.AsDec().String(), memory.AsDec().String()), - CPU: *cpu, - Memory: *memory, + cpu, memory := class.GetMinCPUAndMemory(candidate.Constraint) + name := fmt.Sprintf("%s-%vc%vg", candidate.Name, cpu.AsDec().String(), memory.AsDec().String()) + cls := &appsv1alpha1.ComponentClassInstance{ + ComponentClass: appsv1alpha1.ComponentClass{ + Name: name, + CPU: *cpu, + Memory: *memory, + }, } return cls } - matchComponentClass := func(comp appsv1alpha1.ClusterComponentSpec, classes map[string]*class.ComponentClass) *class.ComponentClass { - filters := class.Filters(make(map[string]resource.Quantity)) - if !comp.Resources.Requests.Cpu().IsZero() { - filters[corev1.ResourceCPU.String()] = *comp.Resources.Requests.Cpu() - } - if !comp.Resources.Requests.Memory().IsZero() { - filters[corev1.ResourceMemory.String()] = *comp.Resources.Requests.Memory() - } - return class.ChooseComponentClasses(classes, filters) - } - for idx, comp := range cluster.Spec.ComponentSpecs { - classes := compClasses[comp.ComponentDefRef] - - var cls *class.ComponentClass - className, ok := componentClassMapping[comp.Name] - // TODO another case if len(classFamilyList.Items) > 0, use matchClassFamilies to find matching class family: - switch { - case ok: - cls = classes[className] - if cls == nil { - return fmt.Errorf("unknown component class %s", className) - } - case classes != nil: - cls = matchComponentClass(comp, classes) - if cls == nil { - return fmt.Errorf("can not find matching class for component %s", comp.Name) - } + cls, err := class.ValidateComponentClass(&comp, compClasses) + if err != nil { + return err } if cls == nil { // TODO reconsider handling policy for this case continue } - componentClassMapping[comp.Name] = cls.Name + + comp.ClassDefRef = &appsv1alpha1.ClassDefRef{Class: cls.Name} requests := corev1.ResourceList{ corev1.ResourceCPU: cls.CPU, corev1.ResourceMemory: cls.Memory, } requests.DeepCopyInto(&comp.Resources.Requests) requests.DeepCopyInto(&comp.Resources.Limits) - var volumes []appsv1alpha1.ClusterComponentVolumeClaimTemplate - if len(comp.VolumeClaimTemplates) > 0 { - volumes = comp.VolumeClaimTemplates - } else { - volumes = buildVolumeClaimByClass(cls) - } - comp.VolumeClaimTemplates = volumes + cluster.Spec.ComponentSpecs[idx] = comp } return nil } - -func buildVolumeClaimByClass(cls *class.ComponentClass) []appsv1alpha1.ClusterComponentVolumeClaimTemplate { - var volumes []appsv1alpha1.ClusterComponentVolumeClaimTemplate - for _, disk := range cls.Storage { - volume := appsv1alpha1.ClusterComponentVolumeClaimTemplate{ - Name: disk.Name, - Spec: appsv1alpha1.PersistentVolumeClaimSpec{ - // TODO define access mode in class - AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, - Resources: corev1.ResourceRequirements{ - Requests: corev1.ResourceList{ - corev1.ResourceStorage: disk.Size, - }, - }, - }, - } - volumes = append(volumes, volume) - } - return volumes -} diff --git a/internal/controller/lifecycle/transformer_fix_cluster_labels.go b/internal/controller/lifecycle/transformer_fix_cluster_labels.go deleted file mode 100644 index 83b04dc21..000000000 --- a/internal/controller/lifecycle/transformer_fix_cluster_labels.go +++ /dev/null @@ -1,31 +0,0 @@ -package lifecycle - -import ( - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/graph" -) - -// fixClusterLabelsTransformer should patch the label first to prevent the label from being modified by the user. -type fixClusterLabelsTransformer struct{} - -func (f *fixClusterLabelsTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - labels := cluster.Labels - if labels == nil { - labels = map[string]string{} - } - cdLabelName := labels[clusterDefLabelKey] - cvLabelName := labels[clusterVersionLabelKey] - cdName, cvName := cluster.Spec.ClusterDefRef, cluster.Spec.ClusterVersionRef - if cdLabelName == cdName && cvLabelName == cvName { - return nil - } - labels[clusterDefLabelKey] = cdName - labels[clusterVersionLabelKey] = cvName - cluster.Labels = labels - return nil -} diff --git a/internal/controller/lifecycle/transformer_halt_recovering.go b/internal/controller/lifecycle/transformer_halt_recovering.go new file mode 100644 index 000000000..6b930e691 --- /dev/null +++ b/internal/controller/lifecycle/transformer_halt_recovering.go @@ -0,0 +1,213 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "encoding/json" + "fmt" + + "github.com/authzed/controller-idioms/hash" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +type HaltRecoveryTransformer struct{} + +func (t *HaltRecoveryTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + + if cluster.Status.ObservedGeneration != 0 { + // skip handling for cluster.status.observedGeneration > 0 + return nil + } + + listOptions := []client.ListOption{ + client.InNamespace(cluster.Namespace), + client.MatchingLabels{ + constant.AppInstanceLabelKey: cluster.Name, + }, + } + + pvcList := &corev1.PersistentVolumeClaimList{} + if err := transCtx.Client.List(transCtx.Context, pvcList, listOptions...); err != nil { + return newRequeueError(requeueDuration, err.Error()) + } + + if len(pvcList.Items) == 0 { + return nil + } + + emitError := func(newCondition metav1.Condition) error { + if newCondition.LastTransitionTime.IsZero() { + newCondition.LastTransitionTime = metav1.Now() + } + newCondition.Status = metav1.ConditionFalse + oldCondition := meta.FindStatusCondition(cluster.Status.Conditions, newCondition.Type) + if oldCondition == nil { + cluster.Status.Conditions = append(cluster.Status.Conditions, newCondition) + } else { + *oldCondition = newCondition + } + transCtx.EventRecorder.Event(transCtx.Cluster, corev1.EventTypeWarning, newCondition.Reason, newCondition.Message) + return graph.ErrPrematureStop + } + + // halt recovering from last applied record stored in pvc's annotation + l, ok := pvcList.Items[0].Annotations[constant.LastAppliedClusterAnnotationKey] + if !ok || l == "" { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "UncleanedResources", + Message: fmt.Sprintf("found uncleaned resources, requires manual deletion, check with `kubectl -n %s get pvc,secret,cm -l %s=%s`", + cluster.Namespace, constant.AppInstanceLabelKey, cluster.Name), + }) + } + + lc := &appsv1alpha1.Cluster{} + if err := json.Unmarshal([]byte(l), lc); err != nil { + return newRequeueError(requeueDuration, err.Error()) + } + + // skip if same cluster UID + if lc.UID == cluster.UID { + return nil + } + + // check clusterDefRef equality + if cluster.Spec.ClusterDefRef != lc.Spec.ClusterDefRef { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.clusterDefRef %s", lc.Spec.ClusterDefRef), + }) + } + + // check clusterVersionRef equality but allow clusters.apps.kubeblocks.io/allow-inconsistent-cv=true annotation override + if cluster.Spec.ClusterVersionRef != lc.Spec.ClusterVersionRef && + cluster.Annotations[constant.HaltRecoveryAllowInconsistentCVAnnotKey] != trueVal { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.clusterVersionRef %s; add '%s=true' annotation if void this check", + lc.Spec.ClusterVersionRef, constant.HaltRecoveryAllowInconsistentCVAnnotKey), + }) + } + + // check component len equality + if l := len(lc.Spec.ComponentSpecs); l != len(cluster.Spec.ComponentSpecs) { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("inconsistent spec.componentSpecs counts to last applied cluster.spec.componentSpecs (len=%d)", l), + }) + } + + // check every components' equality + for _, comp := range cluster.Spec.ComponentSpecs { + found := false + for _, lastUsedComp := range lc.Spec.ComponentSpecs { + // only need to verify [name, componentDefRef, replicas] for equality + if comp.Name != lastUsedComp.Name { + continue + } + if comp.ComponentDefRef != lastUsedComp.ComponentDefRef { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].componentDefRef=%s", + comp.Name, lastUsedComp.ComponentDefRef), + }) + } + if comp.Replicas != lastUsedComp.Replicas { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].replicas=%d", + comp.Name, lastUsedComp.Replicas), + }) + } + + // following only check resource related spec., will skip check if HaltRecoveryAllowInconsistentResAnnotKey + // annotation is specified + if cluster.Annotations[constant.HaltRecoveryAllowInconsistentResAnnotKey] == trueVal { + found = true + break + } + if hash.Object(comp.VolumeClaimTemplates) != hash.Object(lastUsedComp.VolumeClaimTemplates) { + objJSON, _ := json.Marshal(&lastUsedComp.VolumeClaimTemplates) + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].volumeClaimTemplates=%s; add '%s=true' annotation to void this check", + comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey), + }) + } + + if lastUsedComp.ClassDefRef != nil { + if comp.ClassDefRef == nil || hash.Object(*comp.ClassDefRef) != hash.Object(*lastUsedComp.ClassDefRef) { + objJSON, _ := json.Marshal(lastUsedComp.ClassDefRef) + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].classDefRef=%s; add '%s=true' annotation to void this check", + comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey), + }) + } + } else if comp.ClassDefRef != nil { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].classDefRef=null; add '%s=true' annotation to void this check", + comp.Name, constant.HaltRecoveryAllowInconsistentResAnnotKey), + }) + } + + if hash.Object(comp.Resources) != hash.Object(lastUsedComp.Resources) { + objJSON, _ := json.Marshal(&lastUsedComp.Resources) + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("not equal to last applied cluster.spec.componetSpecs[%s].resources=%s; add '%s=true' annotation to void this check", + comp.Name, objJSON, constant.HaltRecoveryAllowInconsistentResAnnotKey), + }) + } + found = true + break + } + if !found { + return emitError(metav1.Condition{ + Type: appsv1alpha1.ConditionTypeHaltRecovery, + Reason: "HaltRecoveryFailed", + Message: fmt.Sprintf("cluster.spec.componetSpecs[%s] not found in last applied cluster", + comp.Name), + }) + } + } + return nil +} + +var _ graph.Transformer = &HaltRecoveryTransformer{} diff --git a/internal/controller/lifecycle/transformer_init.go b/internal/controller/lifecycle/transformer_init.go index e8f753a1b..9c0e64839 100644 --- a/internal/controller/lifecycle/transformer_init.go +++ b/internal/controller/lifecycle/transformer_init.go @@ -1,24 +1,32 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle import ( + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + opsutil "github.com/apecloud/kubeblocks/controllers/apps/operations/util" "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" ) type initTransformer struct { @@ -26,9 +34,49 @@ type initTransformer struct { originCluster *appsv1alpha1.Cluster } -func (i *initTransformer) Transform(dag *graph.DAG) error { +var _ graph.Transformer = &initTransformer{} + +func (t *initTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { // put the cluster object first, it will be root vertex of DAG - rootVertex := &lifecycleVertex{obj: i.cluster, oriObj: i.originCluster} + rootVertex := &ictrltypes.LifecycleVertex{Obj: t.cluster, ObjCopy: t.originCluster, Action: ictrltypes.ActionStatusPtr()} dag.AddVertex(rootVertex) + + if !t.cluster.IsDeleting() { + t.handleLatestOpsRequestProcessingCondition() + } + if t.cluster.IsUpdating() { + t.handleClusterPhase() + } return nil } + +func (t *initTransformer) handleClusterPhase() { + clusterPhase := t.cluster.Status.Phase + if clusterPhase == "" { + t.cluster.Status.Phase = appsv1alpha1.CreatingClusterPhase + } else if clusterPhase != appsv1alpha1.CreatingClusterPhase { + t.cluster.Status.Phase = appsv1alpha1.SpecReconcilingClusterPhase + } +} + +// updateLatestOpsRequestProcessingCondition handles the latest opsRequest processing condition. +func (t *initTransformer) handleLatestOpsRequestProcessingCondition() { + opsRecords, _ := opsutil.GetOpsRequestSliceFromCluster(t.cluster) + if len(opsRecords) == 0 { + return + } + ops := opsRecords[0] + opsBehaviour, ok := appsv1alpha1.OpsRequestBehaviourMapper[ops.Type] + if !ok { + return + } + opsCondition := newOpsRequestProcessingCondition(ops.Name, string(ops.Type), opsBehaviour.ProcessingReasonInClusterCondition) + oldCondition := meta.FindStatusCondition(t.cluster.Status.Conditions, opsCondition.Type) + if oldCondition == nil { + // if this condition not exists, insert it to the first position. + opsCondition.LastTransitionTime = metav1.Now() + t.cluster.Status.Conditions = append([]metav1.Condition{opsCondition}, t.cluster.Status.Conditions...) + } else { + meta.SetStatusCondition(&t.cluster.Status.Conditions, opsCondition) + } +} diff --git a/internal/controller/lifecycle/transformer_object_action.go b/internal/controller/lifecycle/transformer_object_action.go deleted file mode 100644 index e20d62702..000000000 --- a/internal/controller/lifecycle/transformer_object_action.go +++ /dev/null @@ -1,197 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "reflect" - "strings" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - policyv1 "k8s.io/api/policy/v1" - "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - client2 "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -// objectActionTransformer reads all Vertex.Obj in cache and compute the diff DAG. -type objectActionTransformer struct { - cli client2.ReadonlyClient - ctx intctrlutil.RequestCtx -} - -func ownKinds() []client.ObjectList { - return []client.ObjectList{ - &appsv1.StatefulSetList{}, - &appsv1.DeploymentList{}, - &corev1.ServiceList{}, - &corev1.SecretList{}, - &corev1.ConfigMapList{}, - &corev1.PersistentVolumeClaimList{}, - &policyv1.PodDisruptionBudgetList{}, - } -} - -// read all objects owned by our cluster -func (c *objectActionTransformer) readCacheSnapshot(cluster appsv1alpha1.Cluster) (clusterSnapshot, error) { - // list what kinds of object cluster owns - kinds := ownKinds() - snapshot := make(clusterSnapshot) - ml := client.MatchingLabels{constant.AppInstanceLabelKey: cluster.GetName()} - inNS := client.InNamespace(cluster.Namespace) - for _, list := range kinds { - if err := c.cli.List(c.ctx.Ctx, list, inNS, ml); err != nil { - return nil, err - } - // reflect get list.Items - items := reflect.ValueOf(list).Elem().FieldByName("Items") - l := items.Len() - for i := 0; i < l; i++ { - // get the underlying object - object := items.Index(i).Addr().Interface().(client.Object) - // put to snapshot if owned by our cluster - if isOwnerOf(&cluster, object, scheme) { - name, err := getGVKName(object, scheme) - if err != nil { - return nil, err - } - snapshot[*name] = object - } - } - } - - return snapshot, nil -} - -func (c *objectActionTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - - // get the old objects snapshot - oldSnapshot, err := c.readCacheSnapshot(*origCluster) - if err != nil { - return err - } - - // we have the target objects snapshot in dag - newNameVertices := make(map[gvkName]graph.Vertex) - for _, vertex := range dag.Vertices() { - v, _ := vertex.(*lifecycleVertex) - if v == rootVertex { - // ignore root vertex, i.e, cluster object. - continue - } - name, err := getGVKName(v.obj, scheme) - if err != nil { - return err - } - newNameVertices[*name] = vertex - } - - // now compute the diff between old and target snapshot and generate the plan - oldNameSet := sets.KeySet(oldSnapshot) - newNameSet := sets.KeySet(newNameVertices) - - createSet := newNameSet.Difference(oldNameSet) - updateSet := newNameSet.Intersection(oldNameSet) - deleteSet := oldNameSet.Difference(newNameSet) - - createNewVertices := func() { - for name := range createSet { - v, _ := newNameVertices[name].(*lifecycleVertex) - v.action = actionPtr(CREATE) - } - } - updateVertices := func() { - for name := range updateSet { - v, _ := newNameVertices[name].(*lifecycleVertex) - v.oriObj = oldSnapshot[name] - v.action = actionPtr(UPDATE) - } - } - deleteOrphanVertices := func() { - for name := range deleteSet { - v := &lifecycleVertex{ - obj: oldSnapshot[name], - oriObj: oldSnapshot[name], - isOrphan: true, - action: actionPtr(DELETE), - } - dag.AddVertex(v) - dag.Connect(rootVertex, v) - } - } - - filterSecretsCreatedBySystemAccountController := func() { - defaultAccounts := []appsv1alpha1.AccountName{ - appsv1alpha1.AdminAccount, - appsv1alpha1.DataprotectionAccount, - appsv1alpha1.ProbeAccount, - appsv1alpha1.MonitorAccount, - appsv1alpha1.ReplicatorAccount, - } - secretVertices := findAll[*corev1.Secret](dag) - for _, vertex := range secretVertices { - v, _ := vertex.(*lifecycleVertex) - secret, _ := v.obj.(*corev1.Secret) - for _, account := range defaultAccounts { - if strings.Contains(secret.Name, string(account)) { - dag.RemoveVertex(vertex) - break - } - } - } - } - - // generate the plan - switch { - case isClusterDeleting(*origCluster): - for _, vertex := range dag.Vertices() { - v, _ := vertex.(*lifecycleVertex) - v.action = actionPtr(DELETE) - } - deleteOrphanVertices() - case isClusterStatusUpdating(*origCluster): - defer func() { - vertices := findAllNot[*appsv1alpha1.Cluster](dag) - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - v.immutable = true - } - }() - fallthrough - case isClusterUpdating(*origCluster): - // vertices to be created - createNewVertices() - // vertices to be updated - updateVertices() - // vertices to be deleted - deleteOrphanVertices() - // filter secrets created by system account controller - filterSecretsCreatedBySystemAccountController() - } - - return nil -} diff --git a/internal/controller/lifecycle/transformer_ownership.go b/internal/controller/lifecycle/transformer_ownership.go index b59a08758..4dbd8af70 100644 --- a/internal/controller/lifecycle/transformer_ownership.go +++ b/internal/controller/lifecycle/transformer_ownership.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package lifecycle @@ -20,26 +23,31 @@ import ( "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// ownershipTransformer add finalizer to all none cluster objects -type ownershipTransformer struct { - finalizer string -} +// OwnershipTransformer adds finalizer to all none cluster objects +type OwnershipTransformer struct{} + +var _ graph.Transformer = &OwnershipTransformer{} -func (f *ownershipTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) +func (f *OwnershipTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + rootVertex, err := ictrltypes.FindRootVertex(dag) if err != nil { return err } - vertices := findAllNot[*appsv1alpha1.Cluster](dag) + vertices := ictrltypes.FindAllNot[*appsv1alpha1.Cluster](dag) - controllerutil.AddFinalizer(rootVertex.obj, dbClusterFinalizerName) + controllerutil.AddFinalizer(rootVertex.Obj, constant.DBClusterFinalizerName) for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - if err := intctrlutil.SetOwnership(rootVertex.obj, v.obj, scheme, dbClusterFinalizerName); err != nil { + v, _ := vertex.(*ictrltypes.LifecycleVertex) + if err := intctrlutil.SetOwnership(rootVertex.Obj, v.Obj, scheme, constant.DBClusterFinalizerName); err != nil { + if _, ok := err.(*controllerutil.AlreadyOwnedError); ok { + continue + } return err } } diff --git a/internal/controller/lifecycle/transformer_rpl_set_horizontal_scaling.go b/internal/controller/lifecycle/transformer_rpl_set_horizontal_scaling.go deleted file mode 100644 index 2ddb6275e..000000000 --- a/internal/controller/lifecycle/transformer_rpl_set_horizontal_scaling.go +++ /dev/null @@ -1,99 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - appsv1 "k8s.io/api/apps/v1" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" - "github.com/apecloud/kubeblocks/internal/constant" - "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -type rplSetHorizontalScalingTransformer struct { - cr clusterRefResources - cli client.Client - ctx intctrlutil.RequestCtx -} - -func (r *rplSetHorizontalScalingTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - - if isClusterDeleting(*origCluster) { - return nil - } - - hasScaling, err := r.hasReplicationSetHScaling(*cluster) - if err != nil { - return err - } - if !hasScaling { - return nil - } - vertices := findAll[*appsv1.StatefulSet](dag) - // stsList is used to handle statefulSets horizontal scaling when workloadType is replication - var stsList []*appsv1.StatefulSet - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - stsList = append(stsList, v.obj.(*appsv1.StatefulSet)) - } - if err := replicationset.HandleReplicationSet(r.ctx.Ctx, r.cli, cluster, stsList); err != nil { - return err - } - - return nil -} - -// TODO: fix stale cache problem -// TODO: if sts created in last reconcile-loop not present in cache, hasReplicationSetHScaling return false positive -func (r *rplSetHorizontalScalingTransformer) hasReplicationSetHScaling(cluster appsv1alpha1.Cluster) (bool, error) { - stsList, err := r.listAllStsOwnedByCluster(cluster) - if err != nil { - return false, err - } - if len(stsList) == 0 { - return false, err - } - - for _, compDef := range r.cr.cd.Spec.ComponentDefs { - if compDef.WorkloadType == appsv1alpha1.Replication { - return true, nil - } - } - - return false, nil -} - -func (r *rplSetHorizontalScalingTransformer) listAllStsOwnedByCluster(cluster appsv1alpha1.Cluster) ([]appsv1.StatefulSet, error) { - stsList := &appsv1.StatefulSetList{} - if err := r.cli.List(r.ctx.Ctx, stsList, - client.MatchingLabels{constant.AppInstanceLabelKey: cluster.Name}, - client.InNamespace(cluster.Namespace)); err != nil { - return nil, err - } - allSts := make([]appsv1.StatefulSet, 0) - allSts = append(allSts, stsList.Items...) - return allSts, nil -} diff --git a/internal/controller/lifecycle/transformer_secret.go b/internal/controller/lifecycle/transformer_secret.go new file mode 100644 index 000000000..86dd0374d --- /dev/null +++ b/internal/controller/lifecycle/transformer_secret.go @@ -0,0 +1,53 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + corev1 "k8s.io/api/core/v1" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" + ictrltypes "github.com/apecloud/kubeblocks/internal/controller/types" +) + +// SecretTransformer puts all the secrets at the beginning of the DAG +type SecretTransformer struct{} + +var _ graph.Transformer = &SecretTransformer{} + +func (c *SecretTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + var secretVertices, noneRootVertices []graph.Vertex + secretVertices = ictrltypes.FindAll[*corev1.Secret](dag) + noneRootVertices = ictrltypes.FindAllNot[*appsv1alpha1.Cluster](dag) + for _, secretVertex := range secretVertices { + secret, _ := secretVertex.(*ictrltypes.LifecycleVertex) + secret.Immutable = true + for _, vertex := range noneRootVertices { + v, _ := vertex.(*ictrltypes.LifecycleVertex) + // connect all none secret vertices to all secret vertices + if _, ok := v.Obj.(*corev1.Secret); !ok { + if *v.Action != *ictrltypes.ActionDeletePtr() { + dag.Connect(vertex, secretVertex) + } + } + } + } + return nil +} diff --git a/internal/controller/lifecycle/transformer_sts_horizontal_scaling.go b/internal/controller/lifecycle/transformer_sts_horizontal_scaling.go deleted file mode 100644 index 015c26dcf..000000000 --- a/internal/controller/lifecycle/transformer_sts_horizontal_scaling.go +++ /dev/null @@ -1,780 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "context" - "fmt" - "strings" - "time" - - snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" - "github.com/pkg/errors" - appsv1 "k8s.io/api/apps/v1" - batchv1 "k8s.io/api/batch/v1" - corev1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/types" - "sigs.k8s.io/controller-runtime/pkg/client" - "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" - "github.com/apecloud/kubeblocks/internal/constant" - "github.com/apecloud/kubeblocks/internal/controller/builder" - types2 "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/component" - "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -type stsHorizontalScalingTransformer struct { - cr clusterRefResources - cli types2.ReadonlyClient - ctx intctrlutil.RequestCtx -} - -func (s *stsHorizontalScalingTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - - if isClusterDeleting(*origCluster) { - return nil - } - - handleHorizontalScaling := func(vertex *lifecycleVertex) error { - stsObj, _ := vertex.oriObj.(*appsv1.StatefulSet) - stsProto, _ := vertex.obj.(*appsv1.StatefulSet) - if *stsObj.Spec.Replicas == *stsProto.Spec.Replicas { - return nil - } - - key := client.ObjectKey{ - Namespace: stsProto.GetNamespace(), - Name: stsProto.GetName(), - } - snapshotKey := types.NamespacedName{ - Namespace: stsObj.Namespace, - Name: stsObj.Name + "-scaling", - } - // find component of current statefulset - componentName := stsObj.Labels[constant.KBAppComponentLabelKey] - components := mergeComponentsList(s.ctx, - *cluster, - s.cr.cd, - s.cr.cd.Spec.ComponentDefs, - cluster.Spec.ComponentSpecs) - comp := getComponent(components, componentName) - if comp == nil { - s.ctx.Recorder.Eventf(cluster, - corev1.EventTypeWarning, - "HorizontalScaleFailed", - "component %s not found", - componentName) - return nil - } - cleanCronJobs := func() error { - for i := *stsObj.Spec.Replicas; i < *stsProto.Spec.Replicas; i++ { - for _, vct := range stsObj.Spec.VolumeClaimTemplates { - pvcKey := types.NamespacedName{ - Namespace: key.Namespace, - Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), - } - // delete deletion cronjob if exists - cronJobKey := pvcKey - cronJobKey.Name = "delete-pvc-" + pvcKey.Name - cronJob := &batchv1.CronJob{} - if err := s.cli.Get(s.ctx.Ctx, cronJobKey, cronJob); err != nil { - return client.IgnoreNotFound(err) - } - v := &lifecycleVertex{ - obj: cronJob, - action: actionPtr(DELETE), - } - dag.AddVertex(v) - dag.Connect(vertex, v) - } - } - return nil - } - - checkAllPVCsExist := func() (bool, error) { - for i := *stsObj.Spec.Replicas; i < *stsProto.Spec.Replicas; i++ { - for _, vct := range stsObj.Spec.VolumeClaimTemplates { - pvcKey := types.NamespacedName{ - Namespace: key.Namespace, - Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), - } - // check pvc existence - pvcExists, err := isPVCExists(s.cli, s.ctx.Ctx, pvcKey) - if err != nil { - return true, err - } - if !pvcExists { - return false, nil - } - } - } - return true, nil - } - - checkAllPVCBoundIfNeeded := func() (bool, error) { - if comp.HorizontalScalePolicy == nil || - comp.HorizontalScalePolicy.Type != appsv1alpha1.HScaleDataClonePolicyFromSnapshot || - !isSnapshotAvailable(s.cli, s.ctx.Ctx) { - return true, nil - } - return isAllPVCBound(s.cli, s.ctx.Ctx, stsObj) - } - - cleanBackupResourcesIfNeeded := func() error { - if comp.HorizontalScalePolicy == nil || - comp.HorizontalScalePolicy.Type != appsv1alpha1.HScaleDataClonePolicyFromSnapshot || - !isSnapshotAvailable(s.cli, s.ctx.Ctx) { - return nil - } - // if all pvc bounded, clean backup resources - return deleteSnapshot(s.cli, s.ctx, snapshotKey, cluster, comp, dag, rootVertex) - } - - scaleOut := func() error { - if err := cleanCronJobs(); err != nil { - return err - } - allPVCsExist, err := checkAllPVCsExist() - if err != nil { - return err - } - if !allPVCsExist { - // do backup according to component's horizontal scale policy - if err := doBackup(s.ctx, s.cli, comp, snapshotKey, dag, rootVertex, vertex); err != nil { - return err - } - vertex.immutable = true - return nil - } - // check all pvc bound, requeue if not all ready - allPVCBounded, err := checkAllPVCBoundIfNeeded() - if err != nil { - return err - } - if !allPVCBounded { - vertex.immutable = true - return nil - } - // clean backup resources. - // there will not be any backup resources other than scale out. - if err := cleanBackupResourcesIfNeeded(); err != nil { - return err - } - - // pvcs are ready, stateful_set.replicas should be updated - vertex.immutable = false - - return nil - - } - - scaleIn := func() error { - // scale in, if scale in to 0, do not delete pvc - if *stsProto.Spec.Replicas == 0 || len(stsObj.Spec.VolumeClaimTemplates) == 0 { - return nil - } - for i := *stsProto.Spec.Replicas; i < *stsObj.Spec.Replicas; i++ { - for _, vct := range stsObj.Spec.VolumeClaimTemplates { - pvcKey := types.NamespacedName{ - Namespace: key.Namespace, - Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), - } - // create cronjob to delete pvc after 30 minutes - if err := checkedCreateDeletePVCCronJob(s.cli, s.ctx, pvcKey, stsObj, cluster, dag, rootVertex); err != nil { - return err - } - } - } - return nil - } - - // when horizontal scaling up, sometimes db needs backup to sync data from master, - // log is not reliable enough since it can be recycled - var err error - switch { - // scale out - case *stsObj.Spec.Replicas < *stsProto.Spec.Replicas: - err = scaleOut() - case *stsObj.Spec.Replicas > *stsProto.Spec.Replicas: - err = scaleIn() - } - if err != nil { - return err - } - - if *stsObj.Spec.Replicas != *stsProto.Spec.Replicas { - s.ctx.Recorder.Eventf(cluster, - corev1.EventTypeNormal, - "HorizontalScale", - "Start horizontal scale component %s from %d to %d", - comp.Name, - *stsObj.Spec.Replicas, - *stsProto.Spec.Replicas) - } - - return nil - } - - vertices := findAll[*appsv1.StatefulSet](dag) - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - if v.obj == nil || v.oriObj == nil { - continue - } - if err := handleHorizontalScaling(v); err != nil { - return err - } - } - return nil -} - -func isPVCExists(cli types2.ReadonlyClient, - ctx context.Context, - pvcKey types.NamespacedName) (bool, error) { - pvc := corev1.PersistentVolumeClaim{} - if err := cli.Get(ctx, pvcKey, &pvc); err != nil { - return false, client.IgnoreNotFound(err) - } - return true, nil -} - -func mergeComponentsList(reqCtx intctrlutil.RequestCtx, - cluster appsv1alpha1.Cluster, - clusterDef appsv1alpha1.ClusterDefinition, - clusterCompDefList []appsv1alpha1.ClusterComponentDefinition, - clusterCompSpecList []appsv1alpha1.ClusterComponentSpec) []component.SynthesizedComponent { - var compList []component.SynthesizedComponent - for _, compDef := range clusterCompDefList { - for _, compSpec := range clusterCompSpecList { - if compSpec.ComponentDefRef != compDef.Name { - continue - } - comp := component.BuildComponent(reqCtx, cluster, clusterDef, compDef, compSpec) - compList = append(compList, *comp) - } - } - return compList -} - -func getComponent(componentList []component.SynthesizedComponent, name string) *component.SynthesizedComponent { - for _, comp := range componentList { - if comp.Name == name { - return &comp - } - } - return nil -} - -func doBackup(reqCtx intctrlutil.RequestCtx, - cli types2.ReadonlyClient, - component *component.SynthesizedComponent, - snapshotKey types.NamespacedName, - dag *graph.DAG, - root *lifecycleVertex, - vertex *lifecycleVertex) error { - cluster, _ := root.obj.(*appsv1alpha1.Cluster) - stsObj, _ := vertex.oriObj.(*appsv1.StatefulSet) - stsProto, _ := vertex.obj.(*appsv1.StatefulSet) - ctx := reqCtx.Ctx - - if component.HorizontalScalePolicy == nil { - return nil - } - // do backup according to component's horizontal scale policy - switch component.HorizontalScalePolicy.Type { - // use backup tool such as xtrabackup - case appsv1alpha1.HScaleDataClonePolicyFromBackup: - // TODO: db core not support yet, leave it empty - reqCtx.Recorder.Eventf(cluster, - corev1.EventTypeWarning, - "HorizontalScaleFailed", - "scale with backup tool not support yet") - // use volume snapshot - case appsv1alpha1.HScaleDataClonePolicyFromSnapshot: - if !isSnapshotAvailable(cli, ctx) { - reqCtx.Recorder.Eventf(cluster, - corev1.EventTypeWarning, - "HorizontalScaleFailed", - "volume snapshot not support") - // TODO: add ut - return errors.Errorf("volume snapshot not support") - } - vcts := component.VolumeClaimTemplates - if len(vcts) == 0 { - reqCtx.Recorder.Eventf(cluster, - corev1.EventTypeNormal, - "HorizontalScale", - "no VolumeClaimTemplates, no need to do data clone.") - break - } - vsExists, err := isVolumeSnapshotExists(cli, ctx, cluster, component) - if err != nil { - return err - } - // if volumesnapshot not exist, do snapshot to create it. - if !vsExists { - if err := doSnapshot(cli, - reqCtx, - cluster, - snapshotKey, - stsObj, - vcts, - component.HorizontalScalePolicy.BackupTemplateSelector, - dag, - root); err != nil { - return err - } - break - } - // volumesnapshot exists, then check if it is ready to use. - ready, err := isVolumeSnapshotReadyToUse(cli, ctx, cluster, component) - if err != nil { - return err - } - // volumesnapshot not ready, wait for it to be ready by reconciling. - if !ready { - break - } - // if volumesnapshot ready, - // create pvc from snapshot for every new pod - for i := *stsObj.Spec.Replicas; i < *stsProto.Spec.Replicas; i++ { - vct := vcts[0] - for _, tmpVct := range vcts { - if tmpVct.Name == component.HorizontalScalePolicy.VolumeMountsName { - vct = tmpVct - break - } - } - // sync vct.spec.resources from component - for _, tmpVct := range component.VolumeClaimTemplates { - if vct.Name == tmpVct.Name { - vct.Spec.Resources = tmpVct.Spec.Resources - break - } - } - pvcKey := types.NamespacedName{ - Namespace: stsObj.Namespace, - Name: fmt.Sprintf("%s-%s-%d", - vct.Name, - stsObj.Name, - i), - } - if err := checkedCreatePVCFromSnapshot(cli, - ctx, - pvcKey, - cluster, - component, - vct, - stsObj, - dag, - root); err != nil { - reqCtx.Log.Error(err, "checkedCreatePVCFromSnapshot failed") - return err - } - } - // do nothing - case appsv1alpha1.HScaleDataClonePolicyNone: - break - } - return nil -} - -// TODO: handle unfinished jobs from previous scale in -func checkedCreateDeletePVCCronJob(cli types2.ReadonlyClient, - reqCtx intctrlutil.RequestCtx, - pvcKey types.NamespacedName, - stsObj *appsv1.StatefulSet, - cluster *appsv1alpha1.Cluster, - dag *graph.DAG, - root graph.Vertex) error { - ctx := reqCtx.Ctx - now := time.Now() - // hack: delete after 30 minutes - t := now.Add(30 * 60 * time.Second) - schedule := timeToSchedule(t) - cronJob, err := builder.BuildCronJob(pvcKey, schedule, stsObj) - if err != nil { - return err - } - job := &batchv1.CronJob{} - if err := cli.Get(ctx, client.ObjectKeyFromObject(cronJob), job); err != nil { - if !apierrors.IsNotFound(err) { - return err - } - vertex := &lifecycleVertex{obj: cronJob, action: actionPtr(CREATE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - reqCtx.Recorder.Eventf(cluster, - corev1.EventTypeNormal, - "CronJobCreate", - "create cronjob to delete pvc/%s", - pvcKey.Name) - } - - return nil -} - -func timeToSchedule(t time.Time) string { - utc := t.UTC() - return fmt.Sprintf("%d %d %d %d *", utc.Minute(), utc.Hour(), utc.Day(), utc.Month()) -} - -// check volume snapshot available -func isSnapshotAvailable(cli types2.ReadonlyClient, ctx context.Context) bool { - vsList := snapshotv1.VolumeSnapshotList{} - getVSErr := cli.List(ctx, &vsList) - return getVSErr == nil -} - -func isAllPVCBound(cli types2.ReadonlyClient, - ctx context.Context, - stsObj *appsv1.StatefulSet) (bool, error) { - if len(stsObj.Spec.VolumeClaimTemplates) == 0 { - return true, nil - } - for i := 0; i < int(*stsObj.Spec.Replicas); i++ { - pvcKey := types.NamespacedName{ - Namespace: stsObj.Namespace, - Name: fmt.Sprintf("%s-%s-%d", stsObj.Spec.VolumeClaimTemplates[0].Name, stsObj.Name, i), - } - pvc := corev1.PersistentVolumeClaim{} - // check pvc existence - if err := cli.Get(ctx, pvcKey, &pvc); err != nil { - return false, err - } - if pvc.Status.Phase != corev1.ClaimBound { - return false, nil - } - } - return true, nil -} - -func deleteSnapshot(cli types2.ReadonlyClient, - reqCtx intctrlutil.RequestCtx, - snapshotKey types.NamespacedName, - cluster *appsv1alpha1.Cluster, - component *component.SynthesizedComponent, - dag *graph.DAG, - root graph.Vertex) error { - ctx := reqCtx.Ctx - if err := deleteBackup(ctx, cli, cluster.Name, component.Name, dag, root); err != nil { - return client.IgnoreNotFound(err) - } - reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "BackupJobDelete", "Delete backupJob/%s", snapshotKey.Name) - vs := &snapshotv1.VolumeSnapshot{} - if err := cli.Get(ctx, snapshotKey, vs); err != nil { - return client.IgnoreNotFound(err) - } - vertex := &lifecycleVertex{obj: vs, oriObj: vs, action: actionPtr(DELETE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "VolumeSnapshotDelete", "Delete volumeSnapshot/%s", snapshotKey.Name) - return nil -} - -// deleteBackup will delete all backup related resources created during horizontal scaling, -func deleteBackup(ctx context.Context, cli types2.ReadonlyClient, clusterName string, componentName string, dag *graph.DAG, root graph.Vertex) error { - - ml := getBackupMatchingLabels(clusterName, componentName) - - deleteBackupPolicy := func() error { - backupPolicyList := dataprotectionv1alpha1.BackupPolicyList{} - if err := cli.List(ctx, &backupPolicyList, ml); err != nil { - return err - } - for _, backupPolicy := range backupPolicyList.Items { - vertex := &lifecycleVertex{obj: &backupPolicy, oriObj: &backupPolicy, action: actionPtr(DELETE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - } - return nil - } - - deleteRelatedBackups := func() error { - backupList := dataprotectionv1alpha1.BackupList{} - if err := cli.List(ctx, &backupList, ml); err != nil { - return err - } - for _, backup := range backupList.Items { - vertex := &lifecycleVertex{obj: &backup, oriObj: &backup, action: actionPtr(DELETE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - } - return nil - } - - if err := deleteBackupPolicy(); err != nil { - return err - } - - return deleteRelatedBackups() -} - -func getBackupMatchingLabels(clusterName string, componentName string) client.MatchingLabels { - return client.MatchingLabels{ - constant.AppInstanceLabelKey: clusterName, - constant.KBAppComponentLabelKey: componentName, - constant.KBManagedByKey: "cluster", // the resources are managed by which controller - } -} - -// check snapshot existence -func isVolumeSnapshotExists(cli types2.ReadonlyClient, - ctx context.Context, - cluster *appsv1alpha1.Cluster, - component *component.SynthesizedComponent) (bool, error) { - ml := getBackupMatchingLabels(cluster.Name, component.Name) - vsList := snapshotv1.VolumeSnapshotList{} - if err := cli.List(ctx, &vsList, ml); err != nil { - return false, client.IgnoreNotFound(err) - } - for _, vs := range vsList.Items { - // when do h-scale very shortly after last h-scale, - // the last volume snapshot could not be deleted completely - if vs.DeletionTimestamp.IsZero() { - return true, nil - } - } - return false, nil -} - -func doSnapshot(cli types2.ReadonlyClient, - reqCtx intctrlutil.RequestCtx, - cluster *appsv1alpha1.Cluster, - snapshotKey types.NamespacedName, - stsObj *appsv1.StatefulSet, - vcts []corev1.PersistentVolumeClaimTemplate, - backupTemplateSelector map[string]string, - dag *graph.DAG, - root graph.Vertex) error { - - ctx := reqCtx.Ctx - - ml := client.MatchingLabels(backupTemplateSelector) - backupPolicyTemplateList := dataprotectionv1alpha1.BackupPolicyTemplateList{} - // find backuppolicytemplate by clusterdefinition - if err := cli.List(ctx, &backupPolicyTemplateList, ml); err != nil { - return err - } - if len(backupPolicyTemplateList.Items) > 0 { - // if there is backuppolicytemplate created by provider - // create backupjob CR, will ignore error if already exists - err := createBackup(reqCtx, cli, stsObj, &backupPolicyTemplateList.Items[0], snapshotKey, cluster, dag, root) - if err != nil { - return err - } - } else { - // no backuppolicytemplate, then try native volumesnapshot - pvcName := strings.Join([]string{vcts[0].Name, stsObj.Name, "0"}, "-") - snapshot, err := builder.BuildVolumeSnapshot(snapshotKey, pvcName, stsObj) - if err != nil { - return err - } - if err := controllerutil.SetControllerReference(cluster, snapshot, scheme); err != nil { - return err - } - vertex := &lifecycleVertex{obj: snapshot, action: actionPtr(CREATE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - - scheme, _ := appsv1alpha1.SchemeBuilder.Build() - // TODO: SetOwnership - if err := controllerutil.SetControllerReference(cluster, snapshot, scheme); err != nil { - return err - } - reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "VolumeSnapshotCreate", "Create volumesnapshot/%s", snapshotKey.Name) - } - return nil -} - -// check snapshot ready to use -func isVolumeSnapshotReadyToUse(cli types2.ReadonlyClient, - ctx context.Context, - cluster *appsv1alpha1.Cluster, - component *component.SynthesizedComponent) (bool, error) { - ml := getBackupMatchingLabels(cluster.Name, component.Name) - vsList := snapshotv1.VolumeSnapshotList{} - if err := cli.List(ctx, &vsList, ml); err != nil { - return false, client.IgnoreNotFound(err) - } - if len(vsList.Items) == 0 || vsList.Items[0].Status == nil { - return false, nil - } - status := vsList.Items[0].Status - if status.Error != nil { - return false, errors.New("VolumeSnapshot/" + vsList.Items[0].Name + ": " + *status.Error.Message) - } - if status.ReadyToUse == nil { - return false, nil - } - return *status.ReadyToUse, nil -} - -func checkedCreatePVCFromSnapshot(cli types2.ReadonlyClient, - ctx context.Context, - pvcKey types.NamespacedName, - cluster *appsv1alpha1.Cluster, - component *component.SynthesizedComponent, - vct corev1.PersistentVolumeClaimTemplate, - stsObj *appsv1.StatefulSet, - dag *graph.DAG, - root graph.Vertex) error { - pvc := corev1.PersistentVolumeClaim{} - // check pvc existence - if err := cli.Get(ctx, pvcKey, &pvc); err != nil { - if !apierrors.IsNotFound(err) { - return err - } - ml := getBackupMatchingLabels(cluster.Name, component.Name) - vsList := snapshotv1.VolumeSnapshotList{} - if err := cli.List(ctx, &vsList, ml); err != nil { - return err - } - if len(vsList.Items) == 0 { - return errors.Errorf("volumesnapshot not found in cluster %s component %s", cluster.Name, component.Name) - } - // exclude volumes that are deleting - vsName := "" - for _, vs := range vsList.Items { - if vs.DeletionTimestamp != nil { - continue - } - vsName = vs.Name - break - } - return createPVCFromSnapshot(vct, stsObj, pvcKey, vsName, component, dag, root) - } - return nil -} - -// createBackup create backup resources required to do backup, -func createBackup(reqCtx intctrlutil.RequestCtx, - cli types2.ReadonlyClient, - sts *appsv1.StatefulSet, - backupPolicyTemplate *dataprotectionv1alpha1.BackupPolicyTemplate, - backupKey types.NamespacedName, - cluster *appsv1alpha1.Cluster, - dag *graph.DAG, - root graph.Vertex) error { - ctx := reqCtx.Ctx - - createBackupPolicy := func() (backupPolicyName string, Vertex *lifecycleVertex, err error) { - backupPolicyName = "" - backupPolicyList := dataprotectionv1alpha1.BackupPolicyList{} - ml := getBackupMatchingLabels(cluster.Name, sts.Labels[constant.KBAppComponentLabelKey]) - if err = cli.List(ctx, &backupPolicyList, ml); err != nil { - return - } - if len(backupPolicyList.Items) > 0 { - backupPolicyName = backupPolicyList.Items[0].Name - return - } - backupPolicy, err := builder.BuildBackupPolicy(sts, backupPolicyTemplate, backupKey) - if err != nil { - return - } - if err = controllerutil.SetControllerReference(cluster, backupPolicy, scheme); err != nil { - return - } - backupPolicyName = backupPolicy.Name - vertex := &lifecycleVertex{obj: backupPolicy, action: actionPtr(CREATE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - return - } - - createBackup := func(backupPolicyName string, policyVertex *lifecycleVertex) error { - backupPolicy := &dataprotectionv1alpha1.BackupPolicy{} - if err := cli.Get(ctx, client.ObjectKey{Namespace: backupKey.Namespace, Name: backupPolicyName}, backupPolicy); err != nil && !apierrors.IsNotFound(err) { - return err - } - // wait for backupPolicy created - if len(backupPolicy.Name) == 0 { - return nil - } - backupList := dataprotectionv1alpha1.BackupList{} - ml := getBackupMatchingLabels(cluster.Name, sts.Labels[constant.KBAppComponentLabelKey]) - if err := cli.List(ctx, &backupList, ml); err != nil { - return err - } - if len(backupList.Items) > 0 { - // check backup status, if failed return error - if backupList.Items[0].Status.Phase == dataprotectionv1alpha1.BackupFailed { - reqCtx.Recorder.Eventf(cluster, corev1.EventTypeWarning, - "HorizontalScaleFailed", "backup %s status failed", backupKey.Name) - return errors.Errorf("cluster %s h-scale failed, backup error: %s", - cluster.Name, backupList.Items[0].Status.FailureReason) - } - return nil - } - backup, err := builder.BuildBackup(sts, backupPolicyName, backupKey) - if err != nil { - return err - } - if err := controllerutil.SetControllerReference(cluster, backup, scheme); err != nil { - return err - } - vertex := &lifecycleVertex{obj: backup, action: actionPtr(CREATE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - return nil - } - - backupPolicyName, policyVertex, err := createBackupPolicy() - if err != nil { - return err - } - if err := createBackup(backupPolicyName, policyVertex); err != nil { - return err - } - - reqCtx.Recorder.Eventf(cluster, corev1.EventTypeNormal, "BackupJobCreate", "Create backupJob/%s", backupKey.Name) - return nil -} - -func createPVCFromSnapshot(vct corev1.PersistentVolumeClaimTemplate, - sts *appsv1.StatefulSet, - pvcKey types.NamespacedName, - snapshotName string, - component *component.SynthesizedComponent, - dag *graph.DAG, - root graph.Vertex) error { - pvc, err := builder.BuildPVCFromSnapshot(sts, vct, pvcKey, snapshotName, component) - if err != nil { - return err - } - rootVertex, _ := root.(*lifecycleVertex) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - if err = intctrlutil.SetOwnership(cluster, pvc, scheme, dbClusterFinalizerName); err != nil { - return err - } - vertex := &lifecycleVertex{obj: pvc, action: actionPtr(CREATE)} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - return nil -} diff --git a/internal/controller/lifecycle/transformer_sts_horizontal_scaling_test.go b/internal/controller/lifecycle/transformer_sts_horizontal_scaling_test.go deleted file mode 100644 index ba172bab4..000000000 --- a/internal/controller/lifecycle/transformer_sts_horizontal_scaling_test.go +++ /dev/null @@ -1,245 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - . "github.com/onsi/ginkgo/v2" -) - -var _ = Describe("sts horizontal scaling test", func() { - // TODO: refactor the following ut - // - // ctx := context.Background() - // newReqCtx := func() intctrlutil.RequestCtx { - // reqCtx := intctrlutil.RequestCtx{ - // Ctx: ctx, - // Log: logger, - // Recorder: clusterRecorder, - // } - // return reqCtx - // } - // - // newVolumeSnapshot := func(clusterName, componentName string) *snapshotv1.VolumeSnapshot { - // vsYAML := ` - // - // apiVersion: snapshot.storage.k8s.io/v1 - // kind: VolumeSnapshot - // metadata: - // - // labels: - // app.kubernetes.io/name: mysql-apecloud-mysql - // backupjobs.dataprotection.kubeblocks.io/name: wesql-01-replicasets-scaling-qf6cr - // backuppolicies.dataprotection.kubeblocks.io/name: wesql-01-replicasets-scaling-hcxps - // dataprotection.kubeblocks.io/backup-type: snapshot - // name: test-volume-snapshot - // namespace: default - // - // spec: - // - // source: - // persistentVolumeClaimName: data-wesql-01-replicasets-0 - // volumeSnapshotClassName: csi-aws-ebs-snapclass - // - // ` - // - // vs := snapshotv1.VolumeSnapshot{} - // Expect(yaml.Unmarshal([]byte(vsYAML), &vs)).ShouldNot(HaveOccurred()) - // labels := map[string]string{ - // constant.KBManagedByKey: "cluster", - // constant.AppInstanceLabelKey: clusterName, - // constant.KBAppComponentLabelKey: componentName, - // } - // for k, v := range labels { - // vs.Labels[k] = v - // } - // return &vs - // } - // - // Context("with HorizontalScalePolicy set to CloneFromSnapshot and VolumeSnapshot exists", func() { - // It("determines return value of doBackup according to whether VolumeSnapshot is ReadyToUse", func() { - // By("prepare cluster and construct component") - // reqCtx := newReqCtx() - // cluster, clusterDef, clusterVersion, _ := newAllFieldsClusterObj(nil, nil, false) - // component := component.BuildComponent( - // reqCtx, - // *cluster, - // *clusterDef, - // clusterDef.Spec.ComponentDefs[0], - // cluster.Spec.ComponentSpecs[0], - // &clusterVersion.Spec.ComponentVersions[0]) - // Expect(component).ShouldNot(BeNil()) - // component.HorizontalScalePolicy = &appsv1alpha1.HorizontalScalePolicy{ - // Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, - // VolumeMountsName: "data", - // } - // - // By("prepare VolumeSnapshot and set ReadyToUse to true") - // vs := newVolumeSnapshot(cluster.Name, mysqlCompName) - // Expect(testCtx.CreateObj(ctx, vs)).Should(Succeed()) - // Expect(testapps.ChangeObjStatus(&testCtx, vs, func() { - // t := true - // vs.Status = &snapshotv1.VolumeSnapshotStatus{ReadyToUse: &t} - // })).Should(Succeed()) - // - // // prepare doBackup input parameters - // snapshotKey := types.NamespacedName{ - // Namespace: "default", - // Name: "test-snapshot", - // } - // sts := newStsObj() - // stsProto := *sts.DeepCopy() - // r := int32(3) - // stsProto.Spec.Replicas = &r - // - // By("doBackup should return requeue=false") - // shouldRequeue, err := doBackup(reqCtx, k8sClient, cluster, component, sts, &stsProto, snapshotKey) - // Expect(err).ShouldNot(HaveOccurred()) - // Expect(shouldRequeue).Should(BeFalse()) - // - // By("Set ReadyToUse to nil, doBackup should return requeue=true") - // Expect(testapps.ChangeObjStatus(&testCtx, vs, func() { - // vs.Status = &snapshotv1.VolumeSnapshotStatus{ReadyToUse: nil} - // })).Should(Succeed()) - // shouldRequeue, err = doBackup(reqCtx, k8sClient, cluster, component, sts, &stsProto, snapshotKey) - // Expect(err).ShouldNot(HaveOccurred()) - // Expect(shouldRequeue).Should(BeTrue()) - // }) - // - // // REIVEW: this test seems always failed - // It("should do backup to create volumesnapshot when there exists a deleting volumesnapshot", func() { - // By("prepare cluster and construct component") - // reqCtx := newReqCtx() - // cluster, clusterDef, clusterVersion, _ := newAllFieldsClusterObj(nil, nil, false) - // component := component.BuildComponent( - // reqCtx, - // *cluster, - // *clusterDef, - // clusterDef.Spec.ComponentDefs[0], - // cluster.Spec.ComponentSpecs[0], - // &clusterVersion.Spec.ComponentVersions[0]) - // Expect(component).ShouldNot(BeNil()) - // component.HorizontalScalePolicy = &appsv1alpha1.HorizontalScalePolicy{ - // Type: appsv1alpha1.HScaleDataClonePolicyFromSnapshot, - // VolumeMountsName: "data", - // } - // - // By("prepare VolumeSnapshot and set finalizer to prevent it from deletion") - // vs := newVolumeSnapshot(cluster.Name, mysqlCompName) - // Expect(testCtx.CreateObj(ctx, vs)).Should(Succeed()) - // Expect(testapps.ChangeObj(&testCtx, vs, func() { - // vs.Finalizers = append(vs.Finalizers, "test-finalizer") - // })).Should(Succeed()) - // - // By("deleting volume snapshot") - // Expect(k8sClient.Delete(ctx, vs)).Should(Succeed()) - // - // By("checking DeletionTimestamp exists") - // Eventually(func(g Gomega) { - // tmpVS := snapshotv1.VolumeSnapshot{} - // g.Expect(k8sClient.Get(ctx, types.NamespacedName{Namespace: vs.Namespace, Name: vs.Name}, &tmpVS)).Should(Succeed()) - // g.Expect(tmpVS.DeletionTimestamp).ShouldNot(BeNil()) - // }).Should(Succeed()) - // - // // prepare doBackup input parameters - // snapshotKey := types.NamespacedName{ - // Namespace: "default", - // Name: "test-snapshot", - // } - // sts := newStsObj() - // stsProto := *sts.DeepCopy() - // r := int32(3) - // stsProto.Spec.Replicas = &r - // - // By("doBackup should create volumesnapshot and return requeue=true") - // shouldRequeue, err := doBackup(reqCtx, k8sClient, cluster, component, sts, &stsProto, snapshotKey) - // Expect(err).ShouldNot(HaveOccurred()) - // Expect(shouldRequeue).Should(BeTrue()) - // - // newVS := snapshotv1.VolumeSnapshot{} - // By("checking volumesnapshot created by doBackup exists") - // Eventually(func(g Gomega) { - // g.Expect(k8sClient.Get(ctx, snapshotKey, &newVS)).Should(Succeed()) - // }).Should(Succeed()) - // - // By("mocking volumesnapshot status ready") - // Expect(testapps.ChangeObjStatus(&testCtx, &newVS, func() { - // t := true - // newVS.Status = &snapshotv1.VolumeSnapshotStatus{ReadyToUse: &t} - // })).Should(Succeed()) - // - // By("do backup again, this time should create pvcs") - // shouldRequeue, err = doBackup(reqCtx, k8sClient, cluster, component, sts, &stsProto, snapshotKey) - // - // By("checking not requeue, since create pvc is the last step of doBackup") - // Expect(shouldRequeue).Should(BeFalse()) - // Expect(err).ShouldNot(HaveOccurred()) - // - // By("checking pvcs reference right volumesnapshot") - // Eventually(func(g Gomega) { - // for i := *stsProto.Spec.Replicas - 1; i > *sts.Spec.Replicas; i-- { - // pvc := &corev1.PersistentVolumeClaim{} - // g.Expect(k8sClient.Get(ctx, - // types.NamespacedName{ - // Namespace: cluster.Namespace, - // Name: fmt.Sprintf("%s-%s-%d", testapps.DataVolumeName, sts.Name, i)}, - // pvc)).Should(Succeed()) - // g.Expect(pvc.Spec.DataSource.Name).Should(Equal(snapshotKey.Name)) - // } - // }).Should(Succeed()) - // - // By("remove finalizer to clean up") - // Expect(testapps.ChangeObj(&testCtx, vs, func() { - // vs.SetFinalizers(vs.Finalizers[:len(vs.Finalizers)-1]) - // })).Should(Succeed()) - // }) - // }) - // - // Context("backup test", func() { - // It("should not delete backups not created by lifecycle", func() { - // backupPolicyName := "test-backup-policy" - // backupName := "test-backup-job" - // - // By("creating a backup as user do") - // backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). - // SetTTL("168h0m0s"). - // SetBackupPolicyName(backupPolicyName). - // SetBackupType(dataprotectionv1alpha1.BackupTypeSnapshot). - // AddAppInstanceLabel(clusterName). - // AddAppComponentLabel(mysqlCompName). - // AddAppManangedByLabel(). - // Create(&testCtx).GetObject() - // backupKey := client.ObjectKeyFromObject(backup) - // - // By("checking backup exists") - // Eventually(func(g Gomega) { - // tmpBackup := dataprotectionv1alpha1.Backup{} - // g.Expect(k8sClient.Get(ctx, backupKey, &tmpBackup)).Should(Succeed()) - // g.Expect(tmpBackup.Labels[constant.AppInstanceLabelKey]).NotTo(Equal("")) - // g.Expect(tmpBackup.Labels[constant.KBAppComponentLabelKey]).NotTo(Equal("")) - // }).Should(Succeed()) - // - // By("call deleteBackup in lifecycle which should only delete backups created by itself") - // Expect(deleteBackup(ctx, k8sClient, clusterName, mysqlCompName)) - // - // By("checking backup does not be deleted") - // Consistently(func(g Gomega) { - // tmpBackup := dataprotectionv1alpha1.Backup{} - // Expect(k8sClient.Get(ctx, backupKey, &tmpBackup)).Should(Succeed()) - // }).Should(Succeed()) - // }) - // }) -}) diff --git a/internal/controller/lifecycle/transformer_sts_pvc.go b/internal/controller/lifecycle/transformer_sts_pvc.go deleted file mode 100644 index 0385c62d6..000000000 --- a/internal/controller/lifecycle/transformer_sts_pvc.go +++ /dev/null @@ -1,105 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "fmt" - - appsv1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/types" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - types2 "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/graph" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -type stsPVCTransformer struct { - cli types2.ReadonlyClient - ctx intctrlutil.RequestCtx -} - -func (s *stsPVCTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - - if isClusterDeleting(*origCluster) { - return nil - } - - handlePVCUpdate := func(vertex *lifecycleVertex) error { - stsObj, _ := vertex.oriObj.(*appsv1.StatefulSet) - stsProto, _ := vertex.obj.(*appsv1.StatefulSet) - // check stsObj.Spec.VolumeClaimTemplates storage - // request size and find attached PVC and patch request - // storage size - for _, vct := range stsObj.Spec.VolumeClaimTemplates { - var vctProto *corev1.PersistentVolumeClaim - for _, v := range stsProto.Spec.VolumeClaimTemplates { - if v.Name == vct.Name { - vctProto = &v - break - } - } - - // REVIEW: how could VCT proto is nil? - if vctProto == nil { - continue - } - - if vct.Spec.Resources.Requests[corev1.ResourceStorage] == vctProto.Spec.Resources.Requests[corev1.ResourceStorage] { - continue - } - - for i := *stsObj.Spec.Replicas - 1; i >= 0; i-- { - pvc := &corev1.PersistentVolumeClaim{} - pvcKey := types.NamespacedName{ - Namespace: stsObj.Namespace, - Name: fmt.Sprintf("%s-%s-%d", vct.Name, stsObj.Name, i), - } - if err := s.cli.Get(s.ctx.Ctx, pvcKey, pvc); err != nil { - return err - } - obj := pvc.DeepCopy() - obj.Spec.Resources.Requests[corev1.ResourceStorage] = vctProto.Spec.Resources.Requests[corev1.ResourceStorage] - v := &lifecycleVertex{ - obj: obj, - oriObj: pvc, - action: actionPtr(UPDATE), - } - dag.AddVertex(v) - dag.Connect(vertex, v) - } - } - return nil - } - - vertices := findAll[*appsv1.StatefulSet](dag) - for _, vertex := range vertices { - v, _ := vertex.(*lifecycleVertex) - if v.obj != nil && v.oriObj != nil && v.action != nil && *v.action == UPDATE { - if err := handlePVCUpdate(v); err != nil { - return err - } - } - } - return nil -} diff --git a/internal/controller/lifecycle/transformer_tls_certs.go b/internal/controller/lifecycle/transformer_tls_certs.go deleted file mode 100644 index c6a1adc92..000000000 --- a/internal/controller/lifecycle/transformer_tls_certs.go +++ /dev/null @@ -1,84 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 lifecycle - -import ( - "fmt" - - "github.com/pkg/errors" - corev1 "k8s.io/api/core/v1" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/graph" - "github.com/apecloud/kubeblocks/internal/controller/plan" - intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" -) - -type tlsCertsTransformer struct { - cr clusterRefResources - cli client.ReadonlyClient - ctx intctrlutil.RequestCtx -} - -func (t *tlsCertsTransformer) Transform(dag *graph.DAG) error { - rootVertex, err := findRootVertex(dag) - if err != nil { - return err - } - origCluster, _ := rootVertex.oriObj.(*appsv1alpha1.Cluster) - cluster, _ := rootVertex.obj.(*appsv1alpha1.Cluster) - // return fast when cluster is deleting - if isClusterDeleting(*origCluster) { - return nil - } - - var secretList []*corev1.Secret - for _, comp := range cluster.Spec.ComponentSpecs { - if !comp.TLS { - continue - } - if comp.Issuer == nil { - return errors.New("issuer shouldn't be nil when tls enabled") - } - - switch comp.Issuer.Name { - case appsv1alpha1.IssuerUserProvided: - if err := plan.CheckTLSSecretRef(t.ctx, t.cli, cluster.Namespace, comp.Issuer.SecretRef); err != nil { - return err - } - case appsv1alpha1.IssuerKubeBlocks: - secret, err := plan.ComposeTLSSecret(cluster.Namespace, cluster.Name, comp.Name) - if err != nil { - return err - } - secretList = append(secretList, secret) - } - } - - root := dag.Root() - if root == nil { - return fmt.Errorf("root vertex not found: %v", dag) - } - for _, secret := range secretList { - vertex := &lifecycleVertex{obj: secret} - dag.AddVertex(vertex) - dag.Connect(root, vertex) - } - - return nil -} diff --git a/internal/controller/lifecycle/transformer_validate_and_load_ref_resources.go b/internal/controller/lifecycle/transformer_validate_and_load_ref_resources.go new file mode 100644 index 000000000..ec4100bda --- /dev/null +++ b/internal/controller/lifecycle/transformer_validate_and_load_ref_resources.go @@ -0,0 +1,91 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "errors" + "fmt" + + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +// ValidateAndLoadRefResourcesTransformer handles referenced resources'(cd & cv) validation and load them into context +type ValidateAndLoadRefResourcesTransformer struct{} + +func (t *ValidateAndLoadRefResourcesTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + if cluster.IsDeleting() { + return nil + } + + var err error + defer func() { + setProvisioningStartedCondition(&cluster.Status.Conditions, cluster.Name, cluster.Generation, err) + }() + + validateExistence := func(key client.ObjectKey, object client.Object) error { + err = transCtx.Client.Get(transCtx.Context, key, object) + if err != nil { + return newRequeueError(requeueDuration, err.Error()) + } + return nil + } + + // validate cd & cv's existence + // if we can't get the referenced cd & cv, set provisioning condition failed, and jump to plan.Execute() + cd := &appsv1alpha1.ClusterDefinition{} + if err = validateExistence(types.NamespacedName{Name: cluster.Spec.ClusterDefRef}, cd); err != nil { + return err + } + var cv *appsv1alpha1.ClusterVersion + if len(cluster.Spec.ClusterVersionRef) > 0 { + cv = &appsv1alpha1.ClusterVersion{} + if err = validateExistence(types.NamespacedName{Name: cluster.Spec.ClusterVersionRef}, cv); err != nil { + return err + } + } + + // validate cd & cv's availability + // if wrong phase, set provisioning condition failed, and jump to plan.Execute() + if cd.Status.Phase != appsv1alpha1.AvailablePhase || (cv != nil && cv.Status.Phase != appsv1alpha1.AvailablePhase) { + message := fmt.Sprintf("ref resource is unavailable, this problem needs to be solved first. cd: %s", cd.Name) + if cv != nil { + message = fmt.Sprintf("%s, cv: %s", message, cv.Name) + } + err = errors.New(message) + return newRequeueError(requeueDuration, message) + } + + // inject cd & cv into the shared ctx + transCtx.ClusterDef = cd + transCtx.ClusterVer = cv + if cv == nil { + transCtx.ClusterVer = &appsv1alpha1.ClusterVersion{} + } + + return nil +} + +var _ graph.Transformer = &ValidateAndLoadRefResourcesTransformer{} diff --git a/internal/controller/lifecycle/transformer_validate_enable_logs.go b/internal/controller/lifecycle/transformer_validate_enable_logs.go new file mode 100644 index 000000000..72748e043 --- /dev/null +++ b/internal/controller/lifecycle/transformer_validate_enable_logs.go @@ -0,0 +1,46 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +// ValidateEnableLogsTransformer validates config and sends warning event log if necessary +type ValidateEnableLogsTransformer struct{} + +func (e *ValidateEnableLogsTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + transCtx, _ := ctx.(*ClusterTransformContext) + cluster := transCtx.Cluster + if cluster.IsDeleting() { + return nil + } + + // validate config and send warning event log if necessary + err := cluster.Spec.ValidateEnabledLogs(transCtx.ClusterDef) + setProvisioningStartedCondition(&cluster.Status.Conditions, cluster.Name, cluster.Generation, err) + if err != nil { + return newRequeueError(requeueDuration, err.Error()) + } + + return nil +} + +var _ graph.Transformer = &ValidateEnableLogsTransformer{} diff --git a/internal/controller/lifecycle/transformers_parallel.go b/internal/controller/lifecycle/transformers_parallel.go new file mode 100644 index 000000000..09fab43fd --- /dev/null +++ b/internal/controller/lifecycle/transformers_parallel.go @@ -0,0 +1,52 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package lifecycle + +import ( + "fmt" + "sync" + + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +type ParallelTransformers struct { + transformers []graph.Transformer +} + +var _ graph.Transformer = &ParallelTransformers{} + +func (t *ParallelTransformers) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + var group sync.WaitGroup + var errs error + for _, transformer := range t.transformers { + transformer := transformer + group.Add(1) + go func() { + err := transformer.Transform(ctx, dag) + if err != nil { + // TODO: sync.Mutex errs + errs = fmt.Errorf("%v; %v", errs, err) + } + group.Done() + }() + } + group.Wait() + return errs +} diff --git a/internal/controller/model/parallel_transformer.go b/internal/controller/model/parallel_transformer.go new file mode 100644 index 000000000..686f13e59 --- /dev/null +++ b/internal/controller/model/parallel_transformer.go @@ -0,0 +1,52 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package model + +import ( + "fmt" + "sync" + + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +type ParallelTransformer struct { + Transformers []graph.Transformer +} + +func (t *ParallelTransformer) Transform(ctx graph.TransformContext, dag *graph.DAG) error { + var group sync.WaitGroup + var errs error + for _, transformer := range t.Transformers { + transformer := transformer + group.Add(1) + go func() { + err := transformer.Transform(ctx, dag) + if err != nil { + // TODO: sync.Mutex errs + errs = fmt.Errorf("%v; %v", errs, err) + } + group.Done() + }() + } + group.Wait() + return errs +} + +var _ graph.Transformer = &ParallelTransformer{} diff --git a/internal/controller/model/suite_test.go b/internal/controller/model/suite_test.go new file mode 100644 index 000000000..598010d38 --- /dev/null +++ b/internal/controller/model/suite_test.go @@ -0,0 +1,48 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package model + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +// These tests use Ginkgo (BDD-style Go testing framework). Refer to +// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. + +func init() { +} + +func TestAPIs(t *testing.T) { + RegisterFailHandler(Fail) + + RunSpecs(t, "Model Suite") +} + +var _ = BeforeSuite(func() { + go func() { + defer GinkgoRecover() + }() +}) + +var _ = AfterSuite(func() { +}) diff --git a/internal/controller/model/transform_types.go b/internal/controller/model/transform_types.go new file mode 100644 index 000000000..ec7e13731 --- /dev/null +++ b/internal/controller/model/transform_types.go @@ -0,0 +1,114 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package model + +import ( + "fmt" + "time" + + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + clientgoscheme "k8s.io/client-go/kubernetes/scheme" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// TODO: copy from lifecycle.transform_types, should replace lifecycle's def + +var ( + scheme = runtime.NewScheme() +) + +func init() { + utilruntime.Must(clientgoscheme.AddToScheme(scheme)) +} + +type Action string + +const ( + CREATE = Action("CREATE") + UPDATE = Action("UPDATE") + DELETE = Action("DELETE") + STATUS = Action("STATUS") +) + +const ( + AppInstanceLabelKey = "app.kubernetes.io/instance" + KBManagedByKey = "apps.kubeblocks.io/managed-by" + RoleLabelKey = "kubeblocks.io/role" + ConsensusSetAccessModeLabelKey = "cs.apps.kubeblocks.io/access-mode" +) + +// RequeueDuration default reconcile requeue after duration +var RequeueDuration = time.Millisecond * 100 + +type GVKName struct { + gvk schema.GroupVersionKind + ns, name string +} + +// ObjectVertex describes expected object spec and how to reach it +// obj always represents the expected part: new object in Create/Update action and old object in Delete action +// oriObj is set in Update action +// all transformers doing their object manipulation works on obj.spec +// the root vertex(i.e. the cluster vertex) will be treated specially: +// as all its meta, spec and status can be updated in one reconciliation loop +// Update is ignored when immutable=true +// orphan object will be force deleted when action is DELETE +type ObjectVertex struct { + Obj client.Object + OriObj client.Object + Immutable bool + IsOrphan bool + Action *Action +} + +func (v ObjectVertex) String() string { + if v.Action == nil { + return fmt.Sprintf("{obj:%T, name: %s, immutable: %v, orphan: %v, action: nil}", + v.Obj, v.Obj.GetName(), v.Immutable, v.IsOrphan) + } + return fmt.Sprintf("{obj:%T, name: %s, immutable: %v, orphan: %v, action: %v}", + v.Obj, v.Obj.GetName(), v.Immutable, v.IsOrphan, *v.Action) +} + +type ObjectSnapshot map[GVKName]client.Object + +type RequeueError interface { + RequeueAfter() time.Duration + Reason() string +} + +type realRequeueError struct { + reason string + requeueAfter time.Duration +} + +func (r *realRequeueError) Error() string { + return fmt.Sprintf("requeue after: %v as: %s", r.requeueAfter, r.reason) +} + +func (r *realRequeueError) RequeueAfter() time.Duration { + return r.requeueAfter +} + +func (r *realRequeueError) Reason() string { + return r.reason +} diff --git a/internal/controller/model/transform_utils.go b/internal/controller/model/transform_utils.go new file mode 100644 index 000000000..39c698d7a --- /dev/null +++ b/internal/controller/model/transform_utils.go @@ -0,0 +1,276 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package model + +import ( + "fmt" + "reflect" + "time" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + utilruntime "k8s.io/apimachinery/pkg/util/runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/apiutil" + + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +func FindAll[T interface{}](dag *graph.DAG) []graph.Vertex { + vertices := make([]graph.Vertex, 0) + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*ObjectVertex) + if _, ok := v.Obj.(T); ok { + vertices = append(vertices, vertex) + } + } + return vertices +} + +func FindAllNot[T interface{}](dag *graph.DAG) []graph.Vertex { + vertices := make([]graph.Vertex, 0) + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*ObjectVertex) + if _, ok := v.Obj.(T); !ok { + vertices = append(vertices, vertex) + } + } + return vertices +} + +func FindRootVertex(dag *graph.DAG) (*ObjectVertex, error) { + root := dag.Root() + if root == nil { + return nil, fmt.Errorf("root vertex not found: %v", dag) + } + rootVertex, _ := root.(*ObjectVertex) + return rootVertex, nil +} + +func GetGVKName(object client.Object) (*GVKName, error) { + gvk, err := apiutil.GVKForObject(object, scheme) + if err != nil { + return nil, err + } + return &GVKName{ + gvk: gvk, + ns: object.GetNamespace(), + name: object.GetName(), + }, nil +} + +func AddScheme(addToScheme func(*runtime.Scheme) error) { + utilruntime.Must(addToScheme(scheme)) +} + +func GetScheme() *runtime.Scheme { + return scheme +} + +func IsOwnerOf(owner, obj client.Object) bool { + ro, ok := owner.(runtime.Object) + if !ok { + return false + } + gvk, err := apiutil.GVKForObject(ro, scheme) + if err != nil { + return false + } + ref := metav1.OwnerReference{ + APIVersion: gvk.GroupVersion().String(), + Kind: gvk.Kind, + UID: owner.GetUID(), + Name: owner.GetName(), + } + owners := obj.GetOwnerReferences() + referSameObject := func(a, b metav1.OwnerReference) bool { + aGV, err := schema.ParseGroupVersion(a.APIVersion) + if err != nil { + return false + } + + bGV, err := schema.ParseGroupVersion(b.APIVersion) + if err != nil { + return false + } + + return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name + } + for _, ownerRef := range owners { + if referSameObject(ownerRef, ref) { + return true + } + } + return false +} + +func ActionPtr(action Action) *Action { + return &action +} + +func NewRequeueError(after time.Duration, reason string) error { + return &realRequeueError{ + reason: reason, + requeueAfter: after, + } +} + +func IsObjectDeleting(object client.Object) bool { + return !object.GetDeletionTimestamp().IsZero() +} + +func IsObjectUpdating(object client.Object) bool { + value := reflect.ValueOf(object) + if value.Kind() == reflect.Ptr { + value = value.Elem() + } + if value.Kind() != reflect.Struct { + return false + } + status := value.FieldByName("Status") + if !status.IsValid() { + return false + } + observedGeneration := status.FieldByName("ObservedGeneration") + if !observedGeneration.IsValid() { + return false + } + generation := value.FieldByName("Generation") + if !generation.IsValid() { + return false + } + return observedGeneration.Interface() != generation.Interface() +} + +func IsObjectStatusUpdating(object client.Object) bool { + return !IsObjectDeleting(object) && !IsObjectUpdating(object) +} + +// ReadCacheSnapshot reads all objects owned by our cluster +func ReadCacheSnapshot(transCtx graph.TransformContext, root client.Object, ml client.MatchingLabels, kinds ...client.ObjectList) (ObjectSnapshot, error) { + // list what kinds of object cluster owns + snapshot := make(ObjectSnapshot) + inNS := client.InNamespace(root.GetNamespace()) + for _, list := range kinds { + if err := transCtx.GetClient().List(transCtx.GetContext(), list, inNS, ml); err != nil { + return nil, err + } + // reflect get list.Items + items := reflect.ValueOf(list).Elem().FieldByName("Items") + l := items.Len() + for i := 0; i < l; i++ { + // get the underlying object + object := items.Index(i).Addr().Interface().(client.Object) + name, err := GetGVKName(object) + if err != nil { + return nil, err + } + snapshot[*name] = object + } + } + + return snapshot, nil +} + +func PrepareCreate(dag *graph.DAG, object client.Object) { + vertex := &ObjectVertex{ + Obj: object, + Action: ActionPtr(CREATE), + } + dag.AddConnectRoot(vertex) +} + +func PrepareUpdate(dag *graph.DAG, objectOld, objectNew client.Object) { + vertex := &ObjectVertex{ + Obj: objectNew, + OriObj: objectOld, + Action: ActionPtr(UPDATE), + } + dag.AddConnectRoot(vertex) +} + +func PrepareDelete(dag *graph.DAG, object client.Object) { + vertex := &ObjectVertex{ + Obj: object, + Action: ActionPtr(DELETE), + } + dag.AddConnectRoot(vertex) +} + +func PrepareStatus(dag *graph.DAG, objectOld, objectNew client.Object) { + vertex := &ObjectVertex{ + Obj: objectNew, + OriObj: objectOld, + Action: ActionPtr(STATUS), + } + dag.AddVertex(vertex) +} + +func PrepareRootUpdate(dag *graph.DAG) error { + root, err := FindRootVertex(dag) + if err != nil { + return err + } + root.Action = ActionPtr(UPDATE) + return nil +} + +func PrepareRootDelete(dag *graph.DAG) error { + root, err := FindRootVertex(dag) + if err != nil { + return err + } + root.Action = ActionPtr(DELETE) + return nil +} + +func PrepareRootStatus(dag *graph.DAG) error { + root, err := FindRootVertex(dag) + if err != nil { + return err + } + root.Action = ActionPtr(STATUS) + return nil +} + +func DependOn(dag *graph.DAG, object client.Object, dependency ...client.Object) { + objectVertex := findMatchedVertex(dag, object) + if objectVertex == nil { + return + } + for _, d := range dependency { + v := findMatchedVertex(dag, d) + if v != nil { + dag.Connect(objectVertex, v) + } + } +} + +func findMatchedVertex(dag *graph.DAG, object client.Object) graph.Vertex { + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*ObjectVertex) + if v.Obj == object || v.OriObj == object { + return vertex + } + // TODO(free6om): compare by type and objectKey + } + return nil +} diff --git a/internal/controller/model/transform_utils_test.go b/internal/controller/model/transform_utils_test.go new file mode 100644 index 000000000..dc5927793 --- /dev/null +++ b/internal/controller/model/transform_utils_test.go @@ -0,0 +1,49 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package model + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + apps "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" +) + +var _ = Describe("transform utils test", func() { + Context("test IsObjectUpdating", func() { + It("should return false if generation equals", func() { + object := &apps.StatefulSet{} + object.Generation = 1 + object.Status.ObservedGeneration = 1 + Expect(IsObjectUpdating(object)).Should(BeFalse()) + }) + It("should return true if generation doesn't equal", func() { + object := &apps.StatefulSet{} + object.Generation = 2 + object.Status.ObservedGeneration = 1 + Expect(IsObjectUpdating(object)).Should(BeTrue()) + }) + It("should return false if fields not exist", func() { + object := &corev1.Secret{} + Expect(IsObjectUpdating(object)).Should(BeFalse()) + }) + }) +}) diff --git a/internal/controller/plan/builtin_env.go b/internal/controller/plan/builtin_env.go index 18e8593cf..486518c03 100644 --- a/internal/controller/plan/builtin_env.go +++ b/internal/controller/plan/builtin_env.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -30,7 +33,6 @@ import ( cfgcore "github.com/apecloud/kubeblocks/internal/configuration" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/component" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" "github.com/apecloud/kubeblocks/internal/generics" ) @@ -42,19 +44,28 @@ type envWrapper struct { *configTemplateBuilder // configmap or secret not yet submitted. - localObjects *intctrltypes.ReconcileTask + localObjects []coreclient.Object + clusterName string + clusterUID string + componentName string // cache remoted configmap and secret. cache map[schema.GroupVersionKind]map[coreclient.ObjectKey]coreclient.Object } const maxReferenceCount = 10 -func wrapGetEnvByName(templateBuilder *configTemplateBuilder, localObjects *intctrltypes.ReconcileTask) envBuildInFunc { +func wrapGetEnvByName(templateBuilder *configTemplateBuilder, component *component.SynthesizedComponent, localObjs []coreclient.Object) envBuildInFunc { wrapper := &envWrapper{ configTemplateBuilder: templateBuilder, - localObjects: localObjects, + localObjects: localObjs, cache: make(map[schema.GroupVersionKind]map[coreclient.ObjectKey]coreclient.Object), } + // hack for test cases of cli update cmd... + if component != nil { + wrapper.clusterName = component.ClusterName + wrapper.clusterUID = component.ClusterUID + wrapper.componentName = component.Name + } return func(args interface{}, envName string) (string, error) { container, err := fromJSONObject[corev1.Container](args) if err != nil { @@ -161,7 +172,7 @@ func (w *envWrapper) secretValue(secretRef *corev1.SecretKeySelector, container func (w *envWrapper) configMapValue(configmapRef *corev1.ConfigMapKeySelector, container *corev1.Container) (string, error) { if w.cli == nil { - return "", cfgcore.MakeError("not support configmap[%s] value in local mode, cli is nil", configmapRef.Name) + return "", cfgcore.MakeError("not supported configmap[%s] value in local mode, cli is nil", configmapRef.Name) } cmName, err := w.checkAndReplaceEnv(configmapRef.Name, container) @@ -186,10 +197,7 @@ func (w *envWrapper) getResourceFromLocal(key coreclient.ObjectKey, gvk schema.G if v, ok := w.cache[gvk][key]; ok { return v } - if w.localObjects == nil { - return nil - } - return w.localObjects.GetLocalResourceWithObjectKey(key, gvk) + return findMatchedLocalObject(w.localObjects, key, gvk) } var envPlaceHolderRegexp = regexp.MustCompile(`\$\(\w+\)`) @@ -224,18 +232,19 @@ func (w *envWrapper) checkAndReplaceEnv(value string, container *corev1.Containe func (w *envWrapper) doEnvReplace(replacedVars *set.LinkedHashSetString, oldValue string, container *corev1.Container) (string, error) { var ( - clusterName = w.localObjects.Cluster.Name - componentName = w.localObjects.Component.Name - builtInEnvMap = component.GetReplacementMapForBuiltInEnv(clusterName, componentName) + clusterName = w.clusterName + clusterUID = w.clusterUID + componentName = w.componentName + builtInEnvMap = component.GetReplacementMapForBuiltInEnv(clusterName, clusterUID, componentName) ) - builtInEnvMap[constant.ConnCredentialPlaceHolder] = component.GenerateConnCredential(w.localObjects.Cluster.Name) + builtInEnvMap[constant.KBConnCredentialPlaceHolder] = component.GenerateConnCredential(clusterName) kbInnerEnvReplaceFn := func(envName string, strToReplace string) string { return strings.ReplaceAll(strToReplace, envName, builtInEnvMap[envName]) } if !w.incAndCheckReferenceCount() { - return "", cfgcore.MakeError("too many reference count, maybe there is a loop reference: [%s] more than %d times ", oldValue, w.referenceCount) + return "", cfgcore.MakeError("too many reference count, maybe there is a cycled reference: [%s] more than %d times ", oldValue, w.referenceCount) } replacedValue := oldValue @@ -275,8 +284,10 @@ func (w *envWrapper) incAndCheckReferenceCount() bool { func getResourceObject[T generics.Object, PT generics.PObject[T]](w *envWrapper, obj PT, key coreclient.ObjectKey) (PT, error) { gvk := generics.ToGVK(obj) object := w.getResourceFromLocal(key, gvk) - if v, ok := object.(PT); ok { - return v, nil + if object != nil { + if v, ok := object.(PT); ok { + return v, nil + } } if err := w.cli.Get(w.ctx, key, obj); err != nil { return nil, err diff --git a/internal/controller/plan/builtin_env_test.go b/internal/controller/plan/builtin_env_test.go index c4bf0c374..61574151d 100644 --- a/internal/controller/plan/builtin_env_test.go +++ b/internal/controller/plan/builtin_env_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -29,7 +32,6 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ctrlcomp "github.com/apecloud/kubeblocks/internal/controller/component" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" testutil "github.com/apecloud/kubeblocks/internal/testutil/k8s" ) @@ -69,14 +71,13 @@ bootstrap: Namespace: "default", }, Data: map[string]string{ - "KB_MYSQL_0_HOSTNAME": "my-mysql-0.my-mysql-headless", - "KB_MYSQL_FOLLOWERS": "", - "KB_MYSQL_LEADER": "my-mysql-0", - "KB_MYSQL_N": "1", - "KB_MYSQL_RECREATE": "false", - "LOOP_REFERENCE_A": "$(LOOP_REFERENCE_B)", - "LOOP_REFERENCE_B": "$(LOOP_REFERENCE_C)", - "LOOP_REFERENCE_C": "$(LOOP_REFERENCE_A)", + "KB_0_HOSTNAME": "my-mysql-0.my-mysql-headless", + "KB_FOLLOWERS": "", + "KB_LEADER": "my-mysql-0", + "KB_REPLICA_COUNT": "1", + "LOOP_REFERENCE_A": "$(LOOP_REFERENCE_B)", + "LOOP_REFERENCE_B": "$(LOOP_REFERENCE_C)", + "LOOP_REFERENCE_C": "$(LOOP_REFERENCE_A)", }}, &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ @@ -188,18 +189,20 @@ bootstrap: }, }, { - Name: "invalid_contaienr", + Name: "invalid_container", }, }, } - component = &ctrlcomp.SynthesizedComponent{ - Name: "mysql", - } cluster = &appsv1alpha1.Cluster{ ObjectMeta: metav1.ObjectMeta{ Name: "my", + UID: "b006a20c-fb03-441c-bffa-2605cad7e297", }, } + component = &ctrlcomp.SynthesizedComponent{ + Name: "mysql", + ClusterName: cluster.Name, + } cfgTemplate = []appsv1alpha1.ComponentConfigSpec{{ ComponentTemplateSpec: appsv1alpha1.ComponentTemplateSpec{ Name: "mysql-config-8.0.2", @@ -229,29 +232,30 @@ bootstrap: nil, ctx, mockClient.Client(), ) - task := intctrltypes.InitReconcileTask(nil, nil, cluster, component) - task.AppendResource(&corev1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "patroni-template-config", - Namespace: "default", - }, - Data: map[string]string{ - "postgresql.yaml": patroniTemplate, - }}) - Expect(cfgBuilder.injectBuiltInObjectsAndFunctions(podSpec, cfgTemplate, component, task)).Should(BeNil()) + localObjs := []coreclient.Object{ + &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "patroni-template-config", + Namespace: "default", + }, + Data: map[string]string{ + "postgresql.yaml": patroniTemplate, + }}, + } + Expect(cfgBuilder.injectBuiltInObjectsAndFunctions(podSpec, cfgTemplate, component, localObjs)).Should(BeNil()) rendered, err := cfgBuilder.render(map[string]string{ // KB_CLUSTER_NAME, KB_COMP_NAME from env // MYSQL_USER,MYSQL_PASSWORD from valueFrom secret key // SPILO_CONFIGURATION from valueFrom configmap key - // KB_MYSQL_LEADER from envFrom configmap + // KB_LEADER from envFrom configmap // MEMORY_SIZE, CPU from resourceFieldRef "my": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"KB_CLUSTER_NAME\" }}", "mysql": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"KB_COMP_NAME\" }}", "root": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"MYSQL_USER\" }}", "4zrqfl2r": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"MYSQL_PASSWORD\" }}", patroniTemplate: "{{ getEnvByName ( index $.podSpec.containers 0 ) \"SPILO_CONFIGURATION\" }}", - "my-mysql-0": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"KB_MYSQL_LEADER\" }}", + "my-mysql-0": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"KB_LEADER\" }}", strconv.Itoa(4): "{{ getEnvByName ( index $.podSpec.containers 0 ) \"CPU\" }}", strconv.Itoa(8 * 1024 * 1024 * 1024): "{{ getEnvByName ( index $.podSpec.containers 0 ) \"MEMORY_SIZE\" }}", @@ -272,7 +276,7 @@ bootstrap: "error_loop_reference": "{{ getEnvByName ( index $.podSpec.containers 0 ) \"LOOP_REFERENCE_A\" }}", }) Expect(err).ShouldNot(Succeed()) - Expect(err.Error()).Should(ContainSubstring("too many reference count, maybe there is a loop reference")) + Expect(err.Error()).Should(ContainSubstring("too many reference count, maybe there is a cycled reference")) }) }) }) diff --git a/internal/controller/plan/builtin_functions.go b/internal/controller/plan/builtin_functions.go index 183c1e393..5e0f28558 100644 --- a/internal/controller/plan/builtin_functions.go +++ b/internal/controller/plan/builtin_functions.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -22,9 +25,12 @@ import ( "math" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" "github.com/apecloud/kubeblocks/internal/controller/builder" + "github.com/apecloud/kubeblocks/internal/controller/component" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/gotemplate" ) func toJSONObject[T corev1.VolumeSource | corev1.Container | corev1.ContainerPort](obj T) (interface{}, error) { @@ -158,9 +164,7 @@ func calDBPoolSize(args interface{}) (string, error) { } -// getPodContainerByName for general built-in -// User overwrite podSpec of Cluster CR, the correctness of access via index cannot be guaranteed -// if User modify name of container, pray users don't +// getPodContainerByName gets pod container by name func getPodContainerByName(args []interface{}, containerName string) (interface{}, error) { containers, err := fromJSONArray[corev1.Container](args) if err != nil { @@ -174,7 +178,7 @@ func getPodContainerByName(args []interface{}, containerName string) (interface{ return nil, nil } -// getVolumeMountPathByName for general built-in +// getVolumeMountPathByName gets volume mount path by name func getVolumeMountPathByName(args interface{}, volumeName string) (string, error) { container, err := fromJSONObject[corev1.Container](args) if err != nil { @@ -188,7 +192,7 @@ func getVolumeMountPathByName(args interface{}, volumeName string) (string, erro return "", nil } -// getPVCByName for general built-in +// getPVCByName gets pvc by name func getPVCByName(args []interface{}, volumeName string) (interface{}, error) { volumes, err := fromJSONArray[corev1.Volume](args) if err != nil { @@ -202,7 +206,16 @@ func getPVCByName(args []interface{}, volumeName string) (interface{}, error) { return nil, nil } -// getContainerMemory for general built-in +// getContainerCPU gets container cpu limit +func getContainerCPU(args interface{}) (int64, error) { + container, err := fromJSONObject[corev1.Container](args) + if err != nil { + return 0, err + } + return intctrlutil.GetCoreNum(*container), nil +} + +// getContainerMemory gets container memory limit func getContainerMemory(args interface{}) (int64, error) { container, err := fromJSONObject[corev1.Container](args) if err != nil { @@ -211,13 +224,22 @@ func getContainerMemory(args interface{}) (int64, error) { return intctrlutil.GetMemorySize(*container), nil } -// getArgByName for general built-in +// getContainerRequestMemory gets container memory request +func getContainerRequestMemory(args interface{}) (int64, error) { + container, err := fromJSONObject[corev1.Container](args) + if err != nil { + return 0, err + } + return intctrlutil.GetRequestMemorySize(*container), nil +} + +// getArgByName get arg by name func getArgByName(args interface{}, argName string) string { // TODO Support parse command args return emptyString } -// getPortByName for general built-in +// getPortByName get port by name func getPortByName(args interface{}, portName string) (interface{}, error) { container, err := fromJSONObject[corev1.Container](args) if err != nil { @@ -232,17 +254,37 @@ func getPortByName(args interface{}, portName string) (interface{}, error) { return nil, nil } -// getCAFile for general builtIn +// getCAFile gets CA file func getCAFile() string { return builder.MountPath + "/" + builder.CAName } -// getCertFile for general builtIn +// getCertFile gets cert file func getCertFile() string { return builder.MountPath + "/" + builder.CertName } -// getKeyFile for general builtIn +// getKeyFile gets key file func getKeyFile() string { return builder.MountPath + "/" + builder.KeyName } + +// BuiltInCustomFunctions builds a map of customized functions for KubeBlocks +func BuiltInCustomFunctions(c *configTemplateBuilder, component *component.SynthesizedComponent, localObjs []client.Object) *gotemplate.BuiltInObjectsFunc { + return &gotemplate.BuiltInObjectsFunc{ + builtInMysqlCalBufferFunctionName: calDBPoolSize, + builtInGetVolumeFunctionName: getVolumeMountPathByName, + builtInGetPvcFunctionName: getPVCByName, + builtInGetEnvFunctionName: wrapGetEnvByName(c, component, localObjs), + builtInGetPortFunctionName: getPortByName, + builtInGetArgFunctionName: getArgByName, + builtInGetContainerFunctionName: getPodContainerByName, + builtInGetContainerCPUFunctionName: getContainerCPU, + builtInGetContainerMemoryFunctionName: getContainerMemory, + builtInGetContainerRequestMemoryFunctionName: getContainerRequestMemory, + builtInGetCAFile: getCAFile, + builtInGetCertFile: getCertFile, + builtInGetKeyFile: getKeyFile, + } + +} diff --git a/internal/controller/plan/config_template.go b/internal/controller/plan/config_template.go index 88376f5e9..934453fad 100644 --- a/internal/controller/plan/config_template.go +++ b/internal/controller/plan/config_template.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -22,11 +25,11 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/client" + ictrlclient "github.com/apecloud/kubeblocks/internal/controller/client" "github.com/apecloud/kubeblocks/internal/controller/component" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/gotemplate" ) @@ -42,13 +45,15 @@ const ( // General Built-in functions const ( - builtInGetVolumeFunctionName = "getVolumePathByName" - builtInGetPvcFunctionName = "getPVCByName" - builtInGetEnvFunctionName = "getEnvByName" - builtInGetArgFunctionName = "getArgByName" - builtInGetPortFunctionName = "getPortByName" - builtInGetContainerFunctionName = "getContainerByName" - builtInGetContainerMemoryFunctionName = "getContainerMemory" + builtInGetVolumeFunctionName = "getVolumePathByName" + builtInGetPvcFunctionName = "getPVCByName" + builtInGetEnvFunctionName = "getEnvByName" + builtInGetArgFunctionName = "getArgByName" + builtInGetPortFunctionName = "getPortByName" + builtInGetContainerFunctionName = "getContainerByName" + builtInGetContainerCPUFunctionName = "getContainerCPU" + builtInGetContainerMemoryFunctionName = "getContainerMemory" + builtInGetContainerRequestMemoryFunctionName = "getContainerRequestMemory" // BuiltinMysqlCalBufferFunctionName Mysql Built-in // TODO: This function migrate to configuration template @@ -91,7 +96,7 @@ type configTemplateBuilder struct { podSpec *corev1.PodSpec ctx context.Context - cli client.ReadonlyClient + cli ictrlclient.ReadonlyClient } func newTemplateBuilder( @@ -99,7 +104,7 @@ func newTemplateBuilder( cluster *appsv1alpha1.Cluster, version *appsv1alpha1.ClusterVersion, ctx context.Context, - cli client.ReadonlyClient) *configTemplateBuilder { + cli ictrlclient.ReadonlyClient) *configTemplateBuilder { return &configTemplateBuilder{ namespace: namespace, clusterName: clusterName, @@ -159,31 +164,20 @@ func (c *configTemplateBuilder) injectBuiltInObjectsAndFunctions( podSpec *corev1.PodSpec, configs []appsv1alpha1.ComponentConfigSpec, component *component.SynthesizedComponent, - task *intctrltypes.ReconcileTask) error { + localObjs []client.Object) error { if err := c.injectBuiltInObjects(podSpec, component, configs); err != nil { return err } - if err := c.injectBuiltInFunctions(component, task); err != nil { + if err := c.injectBuiltInFunctions(component, localObjs); err != nil { return err } return nil } -func (c *configTemplateBuilder) injectBuiltInFunctions(component *component.SynthesizedComponent, task *intctrltypes.ReconcileTask) error { +func (c *configTemplateBuilder) injectBuiltInFunctions(component *component.SynthesizedComponent, localObjs []client.Object) error { // TODO add built-in function - c.builtInFunctions = &gotemplate.BuiltInObjectsFunc{ - builtInMysqlCalBufferFunctionName: calDBPoolSize, - builtInGetVolumeFunctionName: getVolumeMountPathByName, - builtInGetPvcFunctionName: getPVCByName, - builtInGetEnvFunctionName: wrapGetEnvByName(c, task), - builtInGetPortFunctionName: getPortByName, - builtInGetArgFunctionName: getArgByName, - builtInGetContainerFunctionName: getPodContainerByName, - builtInGetContainerMemoryFunctionName: getContainerMemory, - builtInGetCAFile: getCAFile, - builtInGetCertFile: getCertFile, - builtInGetKeyFile: getKeyFile, - } + c.builtInFunctions = BuiltInCustomFunctions(c, component, localObjs) + // other logic here return nil } @@ -197,7 +191,7 @@ func (c *configTemplateBuilder) injectBuiltInObjects(podSpec *corev1.PodSpec, co } } c.componentValues = &componentTemplateValues{ - TypeName: component.Type, + TypeName: component.CompDefName, Replicas: component.Replicas, Resource: resource, ConfigSpecs: configSpecs, diff --git a/internal/controller/plan/config_template_test.go b/internal/controller/plan/config_template_test.go index eef8b270a..e4957e8b3 100644 --- a/internal/controller/plan/config_template_test.go +++ b/internal/controller/plan/config_template_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -133,7 +136,7 @@ single_thread_memory = 294912 }, }, { - Name: "invalid_contaienr", + Name: "invalid_container", }, }, Volumes: []corev1.Volume{ @@ -152,7 +155,7 @@ single_thread_memory = 294912 component = &ctrlcomp.SynthesizedComponent{ ClusterDefName: "mysql-three-node-definition", Name: "replicasets", - Type: "replicasets", + CompDefName: "replicasets", Replicas: 5, } cfgTemplate = []appsv1alpha1.ComponentConfigSpec{{ @@ -179,8 +182,7 @@ single_thread_memory = 294912 }, nil, nil, nil) - Expect(cfgBuilder.injectBuiltInObjectsAndFunctions( - podSpec, cfgTemplate, component, nil)).Should(BeNil()) + Expect(cfgBuilder.injectBuiltInObjectsAndFunctions(podSpec, cfgTemplate, component, nil)).Should(BeNil()) cfgBuilder.componentValues.Resource = &ResourceDefinition{ MemorySize: 8 * 1024 * 1024 * 1024, diff --git a/internal/controller/plan/prepare.go b/internal/controller/plan/prepare.go index 04efdfdcd..ece8046d1 100644 --- a/internal/controller/plan/prepare.go +++ b/internal/controller/plan/prepare.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan @@ -19,309 +22,53 @@ package plan import ( "context" "fmt" - "math" "strings" "github.com/spf13/viper" - appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/log" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" - componentutil "github.com/apecloud/kubeblocks/controllers/apps/components/util" - cfgutil "github.com/apecloud/kubeblocks/controllers/apps/configuration" cfgcore "github.com/apecloud/kubeblocks/internal/configuration" cfgcm "github.com/apecloud/kubeblocks/internal/configuration/config_manager" + "github.com/apecloud/kubeblocks/internal/configuration/util" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/builder" "github.com/apecloud/kubeblocks/internal/controller/component" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" ) -// PrepareComponentResources generate all necessary sub-resources objects used in component, -// like Secret, ConfigMap, Service, StatefulSet, Deployment, Volume, PodDisruptionBudget etc. -// Generated resources are cached in task.applyObjs. -func PrepareComponentResources(reqCtx intctrlutil.RequestCtx, cli client.Client, task *intctrltypes.ReconcileTask) error { - workloadProcessor := func(customSetup func(*corev1.ConfigMap) (client.Object, error)) error { - envConfig, err := builder.BuildEnvConfig(task.GetBuilderParams(), reqCtx, cli) - if err != nil { - return err - } - task.AppendResource(envConfig) - - workload, err := customSetup(envConfig) - if err != nil { - return err - } - - defer func() { - // workload object should be appended last - task.AppendResource(workload) - }() - - svc, err := builder.BuildHeadlessSvc(task.GetBuilderParams()) - if err != nil { - return err - } - task.AppendResource(svc) - - var podSpec *corev1.PodSpec - sts, ok := workload.(*appsv1.StatefulSet) - if ok { - podSpec = &sts.Spec.Template.Spec - } else { - deploy, ok := workload.(*appsv1.Deployment) - if ok { - podSpec = &deploy.Spec.Template.Spec - } - } - if podSpec == nil { - return nil - } - - defer func() { - for _, cc := range []*[]corev1.Container{ - &podSpec.Containers, - &podSpec.InitContainers, - } { - volumes := podSpec.Volumes - for _, c := range *cc { - for _, v := range c.VolumeMounts { - // if persistence is not found, add emptyDir pod.spec.volumes[] - volumes, _ = intctrlutil.CreateOrUpdateVolume(volumes, v.Name, func(volumeName string) corev1.Volume { - return corev1.Volume{ - Name: v.Name, - VolumeSource: corev1.VolumeSource{ - EmptyDir: &corev1.EmptyDirVolumeSource{}, - }, - } - }, nil) - } - } - podSpec.Volumes = volumes - } - }() - - // render config template - configs, err := buildCfg(task, workload, podSpec, reqCtx.Ctx, cli) - if err != nil { - return err - } - if configs != nil { - task.AppendResource(configs...) - } - // end render config - - // tls certs secret volume and volumeMount - if err := updateTLSVolumeAndVolumeMount(podSpec, task.Cluster.Name, *task.Component); err != nil { - return err - } - return nil - } - - switch task.Component.WorkloadType { - case appsv1alpha1.Stateless: - if err := workloadProcessor( - func(envConfig *corev1.ConfigMap) (client.Object, error) { - return builder.BuildDeploy(reqCtx, task.GetBuilderParams()) - }); err != nil { - return err - } - case appsv1alpha1.Stateful: - if err := workloadProcessor( - func(envConfig *corev1.ConfigMap) (client.Object, error) { - return builder.BuildSts(reqCtx, task.GetBuilderParams(), envConfig.Name) - }); err != nil { - return err - } - case appsv1alpha1.Consensus: - if err := workloadProcessor( - func(envConfig *corev1.ConfigMap) (client.Object, error) { - return buildConsensusSet(reqCtx, task, envConfig.Name) - }); err != nil { - return err - } - case appsv1alpha1.Replication: - // get the number of existing statefulsets under the current component - var existStsList = &appsv1.StatefulSetList{} - if err := componentutil.GetObjectListByComponentName(reqCtx.Ctx, cli, *task.Cluster, existStsList, task.Component.Name); err != nil { - return err - } - - // If the statefulSets already exists, check whether there is an HA switching and the HA process is prioritized to handle. - // TODO(xingran) After refactoring, HA switching will be handled in the replicationSet controller. - if len(existStsList.Items) > 0 { - primaryIndexChanged, _, err := replicationset.CheckPrimaryIndexChanged(reqCtx.Ctx, cli, task.Cluster, - task.Component.Name, task.Component.GetPrimaryIndex()) - if err != nil { - return err - } - if primaryIndexChanged { - if err := replicationset.HandleReplicationSetHASwitch(reqCtx.Ctx, cli, task.Cluster, - componentutil.GetClusterComponentSpecByName(*task.Cluster, task.Component.Name)); err != nil { - return err - } - } - } - - // get the maximum value of params.component.Replicas and the number of existing statefulsets under the current component, - // then construct statefulsets for creating replicationSet or handling horizontal scaling of the replicationSet. - replicaCount := math.Max(float64(len(existStsList.Items)), float64(task.Component.Replicas)) - for index := int32(0); index < int32(replicaCount); index++ { - if err := workloadProcessor( - func(envConfig *corev1.ConfigMap) (client.Object, error) { - return buildReplicationSet(reqCtx, task, envConfig.Name, index) - }); err != nil { - return err - } - } - } - - if needBuildPDB(task) { - pdb, err := builder.BuildPDB(task.GetBuilderParams()) - if err != nil { - return err - } - task.AppendResource(pdb) - } - - svcList, err := builder.BuildSvcList(task.GetBuilderParams()) - if err != nil { - return err - } - for _, svc := range svcList { - if task.Component.WorkloadType == appsv1alpha1.Consensus { - addLeaderSelectorLabels(svc, task.Component) - } - if task.Component.WorkloadType == appsv1alpha1.Replication { - svc.Spec.Selector[constant.RoleLabelKey] = string(replicationset.Primary) - } - task.AppendResource(svc) - } - - return nil -} - -// needBuildPDB check whether the PodDisruptionBudget needs to be built -func needBuildPDB(task *intctrltypes.ReconcileTask) bool { - // TODO: add ut - comp := task.Component - return comp.WorkloadType == appsv1alpha1.Consensus && comp.MaxUnavailable != nil -} - -// TODO multi roles with same accessMode support -func addLeaderSelectorLabels(service *corev1.Service, component *component.SynthesizedComponent) { - leader := component.ConsensusSpec.Leader - if len(leader.Name) > 0 { - service.Spec.Selector[constant.RoleLabelKey] = leader.Name - } -} - -// buildConsensusSet build on a stateful set -func buildConsensusSet(reqCtx intctrlutil.RequestCtx, - task *intctrltypes.ReconcileTask, - envConfigName string) (*appsv1.StatefulSet, error) { - sts, err := builder.BuildSts(reqCtx, task.GetBuilderParams(), envConfigName) - if err != nil { - return sts, err - } - - sts.Spec.UpdateStrategy.Type = appsv1.OnDeleteStatefulSetStrategyType - return sts, err -} - -// buildReplicationSet builds a replication component on statefulSet. -func buildReplicationSet(reqCtx intctrlutil.RequestCtx, - task *intctrltypes.ReconcileTask, - envConfigName string, - stsIndex int32) (*appsv1.StatefulSet, error) { - sts, err := builder.BuildSts(reqCtx, task.GetBuilderParams(), envConfigName) - if err != nil { - return nil, err - } - // sts.Name renamed with suffix "-" for subsequent sts workload - if stsIndex != 0 { - sts.ObjectMeta.Name = fmt.Sprintf("%s-%d", sts.ObjectMeta.Name, stsIndex) - } - if stsIndex == task.Component.GetPrimaryIndex() { - sts.Labels[constant.RoleLabelKey] = string(replicationset.Primary) - } else { - sts.Labels[constant.RoleLabelKey] = string(replicationset.Secondary) - } - sts.Spec.UpdateStrategy.Type = appsv1.OnDeleteStatefulSetStrategyType - // build replicationSet persistentVolumeClaim manually - if err := buildReplicationSetPVC(task, sts); err != nil { - return sts, err - } - return sts, nil -} - -// buildReplicationSetPVC builds replicationSet persistentVolumeClaim manually, -// replicationSet does not manage pvc through volumeClaimTemplate defined on statefulSet, -// the purpose is convenient to convert between workloadTypes in the future (TODO). -func buildReplicationSetPVC(task *intctrltypes.ReconcileTask, sts *appsv1.StatefulSet) error { - // generate persistentVolumeClaim objects used by replicationSet's pod from component.VolumeClaimTemplates - // TODO: The pvc objects involved in all processes in the KubeBlocks will be reconstructed into a unified generation method - pvcMap := replicationset.GeneratePVCFromVolumeClaimTemplates(sts, task.Component.VolumeClaimTemplates) - for pvcTplName, pvc := range pvcMap { - builder.BuildPersistentVolumeClaimLabels(sts, pvc, task.Component, pvcTplName) - task.AppendResource(pvc) - } - - // binding persistentVolumeClaim to podSpec.Volumes - podSpec := &sts.Spec.Template.Spec - if podSpec == nil { - return nil - } - podVolumes := podSpec.Volumes - for _, pvc := range pvcMap { - volumeName := strings.Split(pvc.Name, "-")[0] - podVolumes, _ = intctrlutil.CreateOrUpdateVolume(podVolumes, volumeName, func(volumeName string) corev1.Volume { - return corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvc.Name, - }, - }, - } - }, nil) - } - podSpec.Volumes = podVolumes - return nil -} - -// buildCfg generate volumes for PodTemplate, volumeMount for container, rendered configTemplate and scriptTemplate, -// and generate configManager sidecar for the reconfigure operation. +// RenderConfigNScriptFiles generates volumes for PodTemplate, volumeMount for container, rendered configTemplate and scriptTemplate, +// and generates configManager sidecar for the reconfigure operation. // TODO rename this function, this function name is not very reasonable, but there is no suitable name. -func buildCfg(task *intctrltypes.ReconcileTask, +func RenderConfigNScriptFiles(clusterVersion *appsv1alpha1.ClusterVersion, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, obj client.Object, podSpec *corev1.PodSpec, + localObjs []client.Object, ctx context.Context, cli client.Client) ([]client.Object, error) { // Need to merge configTemplateRef of ClusterVersion.Components[*].ConfigTemplateRefs and // ClusterDefinition.Components[*].ConfigTemplateRefs - if len(task.Component.ConfigTemplates) == 0 && len(task.Component.ScriptTemplates) == 0 { + if len(component.ConfigTemplates) == 0 && len(component.ScriptTemplates) == 0 { return nil, nil } - clusterName := task.Cluster.Name - namespaceName := task.Cluster.Namespace - // New ConfigTemplateBuilder - templateBuilder := newTemplateBuilder(clusterName, namespaceName, task.Cluster, task.ClusterVersion, ctx, cli) + clusterName := cluster.Name + namespaceName := cluster.Namespace + templateBuilder := newTemplateBuilder(clusterName, namespaceName, cluster, clusterVersion, ctx, cli) // Prepare built-in objects and built-in functions - if err := templateBuilder.injectBuiltInObjectsAndFunctions(podSpec, task.Component.ConfigTemplates, task.Component, task); err != nil { + if err := templateBuilder.injectBuiltInObjectsAndFunctions(podSpec, component.ConfigTemplates, component, localObjs); err != nil { return nil, err } - renderWrapper := newTemplateRenderWrapper(templateBuilder, task.Cluster, task.GetBuilderParams(), ctx, cli) - if err := renderWrapper.renderConfigTemplate(task); err != nil { + renderWrapper := newTemplateRenderWrapper(templateBuilder, cluster, ctx, cli) + if err := renderWrapper.renderConfigTemplate(cluster, component, localObjs); err != nil { return nil, err } - if err := renderWrapper.renderScriptTemplate(task); err != nil { + if err := renderWrapper.renderScriptTemplate(cluster, component, localObjs); err != nil { return nil, err } @@ -334,7 +81,7 @@ func buildCfg(task *intctrltypes.ReconcileTask, return nil, cfgcore.WrapError(err, "failed to generate pod volume") } - if err := updateConfigManagerWithComponent(podSpec, task.Component.ConfigTemplates, ctx, cli, task.GetBuilderParams()); err != nil { + if err := updateConfigManagerWithComponent(podSpec, component.ConfigTemplates, ctx, cli, cluster, component); err != nil { return nil, cfgcore.WrapError(err, "failed to generate sidecar for configmap's reloader") } @@ -355,7 +102,7 @@ func updateResourceAnnotationsWithTemplate(obj client.Object, allTemplateAnnotat } // delete not exist configmap label - deletedLabels := cfgcore.MapKeyDifference(existLabels, allTemplateAnnotations) + deletedLabels := util.MapKeyDifference(existLabels, allTemplateAnnotations) for l := range deletedLabels.Iter() { delete(annotations, l) } @@ -368,7 +115,8 @@ func updateResourceAnnotationsWithTemplate(obj client.Object, allTemplateAnnotat // updateConfigManagerWithComponent build the configmgr sidecar container and update it // into PodSpec if configuration reload option is on -func updateConfigManagerWithComponent(podSpec *corev1.PodSpec, cfgTemplates []appsv1alpha1.ComponentConfigSpec, ctx context.Context, cli client.Client, params builder.BuilderParams) error { +func updateConfigManagerWithComponent(podSpec *corev1.PodSpec, cfgTemplates []appsv1alpha1.ComponentConfigSpec, + ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent) error { var ( err error @@ -379,7 +127,7 @@ func updateConfigManagerWithComponent(podSpec *corev1.PodSpec, cfgTemplates []ap if volumeDirs = getUsingVolumesByCfgTemplates(podSpec, cfgTemplates); len(volumeDirs) == 0 { return nil } - if buildParams, err = buildConfigManagerParams(cli, ctx, cfgTemplates, volumeDirs, params); err != nil { + if buildParams, err = buildConfigManagerParams(cli, ctx, cluster, component, cfgTemplates, volumeDirs); err != nil { return err } if buildParams == nil { @@ -440,7 +188,7 @@ func getUsingVolumesByCfgTemplates(podSpec *corev1.PodSpec, cfgTemplates []appsv for i := firstCfg; i < len(cfgTemplates); i++ { tpl := cfgTemplates[i] // Ignore config template, e.g scripts configmap - if !cfgutil.NeedReloadVolume(tpl) { + if !cfgcore.NeedReloadVolume(tpl) { continue } volume := intctrlutil.GetVolumeMountByVolume(container, tpl.VolumeName) @@ -451,7 +199,8 @@ func getUsingVolumesByCfgTemplates(podSpec *corev1.PodSpec, cfgTemplates []appsv return volumeDirs } -func buildConfigManagerParams(cli client.Client, ctx context.Context, configSpec []appsv1alpha1.ComponentConfigSpec, volumeDirs []corev1.VolumeMount, params builder.BuilderParams) (*cfgcm.CfgManagerBuildParams, error) { +func buildConfigManagerParams(cli client.Client, ctx context.Context, cluster *appsv1alpha1.Cluster, + comp *component.SynthesizedComponent, configSpec []appsv1alpha1.ComponentConfigSpec, volumeDirs []corev1.VolumeMount) (*cfgcm.CfgManagerBuildParams, error) { var ( err error reloadOptions *appsv1alpha1.ReloadOptions @@ -460,14 +209,14 @@ func buildConfigManagerParams(cli client.Client, ctx context.Context, configSpec configManagerParams := &cfgcm.CfgManagerBuildParams{ ManagerName: constant.ConfigSidecarName, - CharacterType: params.Component.CharacterType, - SecreteName: component.GenerateConnCredential(params.Cluster.Name), + CharacterType: comp.CharacterType, + SecreteName: component.GenerateConnCredential(cluster.Name), Image: viper.GetString(constant.KBToolsImage), Volumes: volumeDirs, - Cluster: params.Cluster, + Cluster: cluster, } - if reloadOptions, formatterConfig, err = cfgutil.GetReloadOptions(cli, ctx, configSpec); err != nil { + if reloadOptions, formatterConfig, err = cfgcore.GetReloadOptions(cli, ctx, configSpec); err != nil { return nil, err } if reloadOptions == nil || formatterConfig == nil { diff --git a/internal/controller/plan/prepare_test.go b/internal/controller/plan/prepare_test.go index ae6861e16..447dc177b 100644 --- a/internal/controller/plan/prepare_test.go +++ b/internal/controller/plan/prepare_test.go @@ -1,22 +1,26 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan import ( + "fmt" "reflect" . "github.com/onsi/ginkgo/v2" @@ -27,13 +31,192 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" "github.com/apecloud/kubeblocks/internal/controller/component" - "github.com/apecloud/kubeblocks/internal/controller/types" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" "github.com/apecloud/kubeblocks/internal/generics" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) +const ( + mysqlCompDefName = "replicasets" + mysqlCompName = "mysql" + nginxCompDefName = "nginx" + nginxCompName = "nginx" + redisCompDefName = "replicasets" + redisCompName = "redis" +) + +// buildComponentResources generate all necessary sub-resources objects used in component, +// like Secret, ConfigMap, Service, StatefulSet, Deployment, Volume, PodDisruptionBudget etc. +func buildComponentResources(reqCtx intctrlutil.RequestCtx, cli client.Client, + clusterDef *appsv1alpha1.ClusterDefinition, + clusterVer *appsv1alpha1.ClusterVersion, + cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent) ([]client.Object, error) { + resources := make([]client.Object, 0) + workloadProcessor := func(customSetup func(*corev1.ConfigMap) (client.Object, error)) error { + envConfig, err := builder.BuildEnvConfigLow(reqCtx, cli, cluster, component) + if err != nil { + return err + } + resources = append(resources, envConfig) + + workload, err := customSetup(envConfig) + if err != nil { + return err + } + + defer func() { + // workload object should be appended last + resources = append(resources, workload) + }() + + svc, err := builder.BuildHeadlessSvcLow(cluster, component) + if err != nil { + return err + } + resources = append(resources, svc) + + var podSpec *corev1.PodSpec + sts, ok := workload.(*appsv1.StatefulSet) + if ok { + podSpec = &sts.Spec.Template.Spec + } else { + deploy, ok := workload.(*appsv1.Deployment) + if ok { + podSpec = &deploy.Spec.Template.Spec + } + } + if podSpec == nil { + return nil + } + + defer func() { + for _, cc := range []*[]corev1.Container{ + &podSpec.Containers, + &podSpec.InitContainers, + } { + volumes := podSpec.Volumes + for _, c := range *cc { + for _, v := range c.VolumeMounts { + // if persistence is not found, add emptyDir pod.spec.volumes[] + volumes, _ = intctrlutil.CreateOrUpdateVolume(volumes, v.Name, func(volumeName string) corev1.Volume { + return corev1.Volume{ + Name: v.Name, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{}, + }, + } + }, nil) + } + } + podSpec.Volumes = volumes + } + }() + + // render config template + configs, err := RenderConfigNScriptFiles(clusterVer, cluster, component, workload, podSpec, nil, reqCtx.Ctx, cli) + if err != nil { + return err + } + if configs != nil { + resources = append(resources, configs...) + } + // end render config + + //// tls certs secret volume and volumeMount + // if err := updateTLSVolumeAndVolumeMount(podSpec, cluster.Name, *component); err != nil { + // return err + // } + return nil + } + + // pre-condition check + // if component.WorkloadType == appsv1alpha1.Replication { + // // get the number of existing pods under the current component + // var existPodList = &corev1.PodList{} + // if err := componentutil.GetObjectListByComponentName(reqCtx.Ctx, cli, *cluster, existPodList, component.Name); err != nil { + // return nil, err + // } + // + // // If the Pods already exists, check whether there is an HA switching and the HA process is prioritized to handle. + // // TODO: (xingran) After refactoring, HA switching will be handled in the replicationSet controller. + // if len(existPodList.Items) > 0 { + // primaryIndexChanged, _, err := replication.CheckPrimaryIndexChanged(reqCtx.Ctx, cli, cluster, + // component.Name, component.GetPrimaryIndex()) + // if err != nil { + // return nil, err + // } + // if primaryIndexChanged { + // if err := replication.HandleReplicationSetHASwitch(reqCtx.Ctx, cli, cluster, + // componentutil.GetClusterComponentSpecByName(*cluster, component.Name)); err != nil { + // return nil, err + // } + // } + // } + // } + + // TODO: may add a PDB transform to Create/Update/Delete. + // if no these handle, the cluster controller will occur an error during reconciling. + // conditional build PodDisruptionBudget + if component.MinAvailable != nil { + pdb, err := builder.BuildPDBLow(cluster, component) + if err != nil { + return nil, err + } + resources = append(resources, pdb) + } else { + panic("this shouldn't happen") + } + + svcList, err := builder.BuildSvcListWithCustomAttributes(cluster, component, func(svc *corev1.Service) { + switch component.WorkloadType { + case appsv1alpha1.Consensus: + addLeaderSelectorLabels(svc, component) + case appsv1alpha1.Replication: + svc.Spec.Selector[constant.RoleLabelKey] = "primary" + } + }) + if err != nil { + return nil, err + } + for _, svc := range svcList { + resources = append(resources, svc) + } + + // REVIEW/TODO: + // - need higher level abstraction handling + // - or move this module to part operator controller handling + switch component.WorkloadType { + case appsv1alpha1.Stateless: + if err := workloadProcessor( + func(envConfig *corev1.ConfigMap) (client.Object, error) { + return builder.BuildDeployLow(reqCtx, cluster, component) + }); err != nil { + return nil, err + } + case appsv1alpha1.Stateful, appsv1alpha1.Consensus, appsv1alpha1.Replication: + if err := workloadProcessor( + func(envConfig *corev1.ConfigMap) (client.Object, error) { + return builder.BuildStsLow(reqCtx, cluster, component, envConfig.Name) + }); err != nil { + return nil, err + } + } + + return resources, nil +} + +// TODO multi roles with same accessMode support +func addLeaderSelectorLabels(service *corev1.Service, component *component.SynthesizedComponent) { + leader := component.ConsensusSpec.Leader + if len(leader.Name) > 0 { + service.Spec.Selector[constant.RoleLabelKey] = leader.Name + } +} + var _ = Describe("Cluster Controller", func() { cleanEnv := func() { @@ -73,21 +256,17 @@ var _ = Describe("Cluster Controller", func() { ) Context("with Deployment workload", func() { - const ( - nginxCompType = "nginx" - nginxCompName = "nginx" - ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). + AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(nginxCompType). + AddComponentVersion(nginxCompDefName). AddContainerShort("nginx", testapps.NginxImage). GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(nginxCompType, nginxCompName). + AddComponent(nginxCompDefName, nginxCompName). GetObject() }) @@ -96,42 +275,45 @@ var _ = Describe("Cluster Controller", func() { Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) + Expect(err).Should(Succeed()) - resources := *task.Resources - Expect(len(resources)).Should(Equal(4)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("Deployment")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("Service")) + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) + + expects := []string{ + "PodDisruptionBudget", + "Service", + "ConfigMap", + "Service", + "Deployment", + } + Expect(resources).Should(HaveLen(len(expects))) + for i, v := range expects { + Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) + } }) }) Context("with Stateful workload and without config template", func() { - const ( - mysqlCompType = "replicasets" - mysqlCompName = "mysql" - ) BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). GetObject() pvcSpec := testapps.NewPVCSpec("1Gi") cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). GetObject() }) @@ -141,7 +323,7 @@ var _ = Describe("Cluster Controller", func() { Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, @@ -149,26 +331,31 @@ var _ = Describe("Cluster Controller", func() { cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0], ) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) + Expect(err).Should(Succeed()) - resources := *task.Resources - Expect(len(resources)).Should(Equal(4)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("StatefulSet")) + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) - container := clusterDef.Spec.ComponentDefs[0].PodSpec.Containers[0] - sts := resources[2].(*appsv1.StatefulSet) - Expect(len(sts.Spec.Template.Spec.Volumes)).Should(Equal(len(container.VolumeMounts))) + expects := []string{ + "PodDisruptionBudget", + "Service", + "ConfigMap", + "Service", + "StatefulSet", + } + Expect(resources).Should(HaveLen(len(expects))) + for i, v := range expects { + Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) + if v == "StatefulSet" { + container := clusterDef.Spec.ComponentDefs[0].PodSpec.Containers[0] + sts := resources[i].(*appsv1.StatefulSet) + Expect(len(sts.Spec.Template.Spec.Volumes)).Should(Equal(len(container.VolumeMounts))) + } + } }) }) Context("with Stateful workload and with config template", func() { - const ( - mysqlCompType = "replicasets" - mysqlCompName = "mysql" - ) BeforeEach(func() { cm := testapps.CreateCustomizedObj(&testCtx, "config/config-template.yaml", &corev1.ConfigMap{}, testCtx.UseDefaultNamespace()) @@ -177,17 +364,17 @@ var _ = Describe("Cluster Controller", func() { &appsv1alpha1.ConfigConstraint{}) clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddConfigTemplate(cm.Name, cm.Name, cfgTpl.Name, testCtx.DefaultNamespace, "mysql-config"). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). GetObject() pvcSpec := testapps.NewPVCSpec("1Gi") cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). GetObject() }) @@ -197,30 +384,34 @@ var _ = Describe("Cluster Controller", func() { Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) - - resources := *task.Resources - Expect(len(resources)).Should(Equal(5)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("StatefulSet")) + Expect(err).Should(Succeed()) + + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) + + expects := []string{ + "PodDisruptionBudget", + "Service", + "ConfigMap", + "Service", + "ConfigMap", + "StatefulSet", + } + Expect(resources).Should(HaveLen(len(expects))) + for i, v := range expects { + Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) + } }) }) Context("with Stateful workload and with config template and with config volume mount", func() { - const ( - mysqlCompType = "replicasets" - mysqlCompName = "mysql" - ) BeforeEach(func() { cm := testapps.CreateCustomizedObj(&testCtx, "config/config-template.yaml", &corev1.ConfigMap{}, testCtx.UseDefaultNamespace()) @@ -229,18 +420,18 @@ var _ = Describe("Cluster Controller", func() { &appsv1alpha1.ConfigConstraint{}) clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddConfigTemplate(cm.Name, cm.Name, cfgTpl.Name, testCtx.DefaultNamespace, "mysql-config"). AddContainerVolumeMounts("mysql", []corev1.VolumeMount{{Name: "mysql-config", MountPath: "/mnt/config"}}). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). GetObject() pvcSpec := testapps.NewPVCSpec("1Gi") cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). GetObject() }) @@ -250,40 +441,42 @@ var _ = Describe("Cluster Controller", func() { Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) - - resources := *task.Resources - Expect(len(resources)).Should(Equal(5)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("StatefulSet")) - + Expect(err).Should(Succeed()) + + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) + + expects := []string{ + "PodDisruptionBudget", + "Service", + "ConfigMap", + "Service", + "ConfigMap", + "StatefulSet", + } + Expect(resources).Should(HaveLen(len(expects))) + for i, v := range expects { + Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) + if v == "StatefulSet" { + sts := resources[i].(*appsv1.StatefulSet) + podSpec := sts.Spec.Template.Spec + Expect(len(podSpec.Containers)).Should(Equal(2)) + } + } originPodSpec := clusterDef.Spec.ComponentDefs[0].PodSpec Expect(len(originPodSpec.Containers)).Should(Equal(1)) - - sts := resources[3].(*appsv1.StatefulSet) - podSpec := sts.Spec.Template.Spec - Expect(len(podSpec.Containers)).Should(Equal(2)) }) }) // for test GetContainerWithVolumeMount Context("with Consensus workload and with external service", func() { - const ( - mysqlCompType = "replicasets" - mysqlCompName = "mysql" - nginxCompType = "proxy" - ) - var ( clusterDef *appsv1alpha1.ClusterDefinition clusterVersion *appsv1alpha1.ClusterVersion @@ -292,19 +485,19 @@ var _ = Describe("Cluster Controller", func() { BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). + AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort("mysql", testapps.ApeCloudMySQLImage). - AddComponent(nginxCompType). + AddComponentVersion(nginxCompDefName). AddContainerShort("nginx", testapps.NginxImage). GetObject() pvcSpec := testapps.NewPVCSpec("1Gi") cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). GetObject() }) @@ -314,33 +507,32 @@ var _ = Describe("Cluster Controller", func() { Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) - - resources := *task.Resources - Expect(len(resources)).Should(Equal(4)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("StatefulSet")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("Service")) + Expect(err).Should(Succeed()) + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) + expects := []string{ + "PodDisruptionBudget", + "Service", + "ConfigMap", + "Service", + "StatefulSet", + } + Expect(resources).Should(HaveLen(len(expects))) + for i, v := range expects { + Expect(reflect.TypeOf(resources[i]).String()).Should(ContainSubstring(v), fmt.Sprintf("failed at idx %d", i)) + } }) }) // for test GetContainerWithVolumeMount Context("with Replications workload without pvc", func() { - const ( - redisCompType = "replicasets" - redisCompName = "redis" - nginxCompType = "proxy" - ) - var ( clusterDef *appsv1alpha1.ClusterDefinition clusterVersion *appsv1alpha1.ClusterVersion @@ -349,112 +541,106 @@ var _ = Describe("Cluster Controller", func() { BeforeEach(func() { clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, redisCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). + AddComponentDef(testapps.ReplicationRedisComponent, redisCompDefName). + AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). GetObject() clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(redisCompType). + AddComponentVersion(redisCompDefName). AddContainerShort("redis", testapps.DefaultRedisImageName). - AddComponent(nginxCompType). + AddComponentVersion(nginxCompDefName). AddContainerShort("nginx", testapps.NginxImage). GetObject() cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDef.Name, clusterVersion.Name). - AddComponent(redisCompName, redisCompType). + AddComponent(redisCompName, redisCompDefName). SetReplicas(2). SetPrimaryIndex(0). GetObject() }) - It("should construct env, headless service, statefuset objects for each replica, besides an external service object", func() { + It("should construct env, headless service, statefuset object, besides an external service object", func() { reqCtx := intctrlutil.RequestCtx{ Ctx: ctx, Log: logger, } - component := component.BuildComponent( + component, err := component.BuildComponent( reqCtx, *cluster, *clusterDef, clusterDef.Spec.ComponentDefs[0], cluster.Spec.ComponentSpecs[0], &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) + Expect(err).Should(Succeed()) + + resources, err := buildComponentResources(reqCtx, testCtx.Cli, clusterDef, clusterVersion, cluster, component) + Expect(err).Should(Succeed()) - resources := *task.Resources - Expect(len(resources)).Should(Equal(7)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) + // REVIEW: (free6om) + // missing connection credential, TLS secret objs check? + Expect(resources).Should(HaveLen(5)) + Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("PodDisruptionBudget")) Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("StatefulSet")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[4]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[5]).String()).Should(ContainSubstring("StatefulSet")) - Expect(reflect.TypeOf(resources[6]).String()).Should(ContainSubstring("Service")) + Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("ConfigMap")) + Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("Service")) + Expect(reflect.TypeOf(resources[4]).String()).Should(ContainSubstring("StatefulSet")) }) }) - // for test GetContainerWithVolumeMount - Context("with Replications workload with pvc", func() { - const ( - redisCompType = "replicasets" - redisCompName = "redis" - nginxCompType = "proxy" - ) - - var ( - clusterDef *appsv1alpha1.ClusterDefinition - clusterVersion *appsv1alpha1.ClusterVersion - cluster *appsv1alpha1.Cluster - ) - - BeforeEach(func() { - clusterDef = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, redisCompType). - AddComponent(testapps.StatelessNginxComponent, nginxCompType). - GetObject() - clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(redisCompType). - AddContainerShort("redis", testapps.DefaultRedisImageName). - AddComponent(nginxCompType). - AddContainerShort("nginx", testapps.NginxImage). - GetObject() - pvcSpec := testapps.NewPVCSpec("1Gi") - cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, - clusterDef.Name, clusterVersion.Name). - AddComponent(redisCompName, redisCompType). - SetReplicas(2). - SetPrimaryIndex(0). - AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). - GetObject() - }) - - It("should construct pvc objects for each replica", func() { - reqCtx := intctrlutil.RequestCtx{ - Ctx: ctx, - Log: logger, - } - component := component.BuildComponent( - reqCtx, - *cluster, - *clusterDef, - clusterDef.Spec.ComponentDefs[0], - cluster.Spec.ComponentSpecs[0], - &clusterVersion.Spec.ComponentVersions[0]) - task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) - Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) - - resources := *task.Resources - Expect(len(resources)).Should(Equal(9)) - Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("PersistentVolumeClaim")) - Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("StatefulSet")) - Expect(reflect.TypeOf(resources[4]).String()).Should(ContainSubstring("ConfigMap")) - Expect(reflect.TypeOf(resources[5]).String()).Should(ContainSubstring("PersistentVolumeClaim")) - Expect(reflect.TypeOf(resources[6]).String()).Should(ContainSubstring("Service")) - Expect(reflect.TypeOf(resources[7]).String()).Should(ContainSubstring("StatefulSet")) - Expect(reflect.TypeOf(resources[8]).String()).Should(ContainSubstring("Service")) - }) - }) + // TODO: (free6om) + // uncomment following test case until pre-provisoned PVC work begin + // // for test GetContainerWithVolumeMount + // Context("with Replications workload with pvc", func() { + // var ( + // clusterDef *appsv1alpha1.ClusterDefinition + // clusterVersion *appsv1alpha1.ClusterVersion + // cluster *appsv1alpha1.Cluster + // ) + // + // BeforeEach(func() { + // clusterDef = testapps.NewClusterDefFactory(clusterDefName). + // AddComponentDef(testapps.ReplicationRedisComponent, redisCompDefName). + // AddComponentDef(testapps.StatelessNginxComponent, nginxCompDefName). + // GetObject() + // clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). + // AddComponentVersion(redisCompDefName). + // AddContainerShort("redis", testapps.DefaultRedisImageName). + // AddComponentVersion(nginxCompDefName). + // AddContainerShort("nginx", testapps.NginxImage). + // GetObject() + // pvcSpec := testapps.NewPVCSpec("1Gi") + // cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + // clusterDef.Name, clusterVersion.Name). + // AddComponentVersion(redisCompName, redisCompDefName). + // SetReplicas(2). + // SetPrimaryIndex(0). + // AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). + // GetObject() + // }) + // + // It("should construct pvc objects for each replica", func() { + // reqCtx := intctrlutil.RequestCtx{ + // Ctx: ctx, + // Log: logger, + // } + // component := component.BuildComponent( + // reqCtx, + // *cluster, + // *clusterDef, + // clusterDef.Spec.ComponentDefs[0], + // cluster.Spec.ComponentSpecs[0], + // &clusterVersion.Spec.ComponentVersions[0]) + // task := types.InitReconcileTask(clusterDef, clusterVersion, cluster, component) + // Expect(PrepareComponentResources(reqCtx, testCtx.Cli, task)).Should(Succeed()) + // + // resources := *task.Resources + // Expect(resources).Should(HaveLen(6)) + // Expect(reflect.TypeOf(resources[0]).String()).Should(ContainSubstring("ConfigMap")) + // Expect(reflect.TypeOf(resources[1]).String()).Should(ContainSubstring("Service")) + // Expect(reflect.TypeOf(resources[2]).String()).Should(ContainSubstring("PersistentVolumeClaim")) + // Expect(reflect.TypeOf(resources[3]).String()).Should(ContainSubstring("PersistentVolumeClaim")) + // Expect(reflect.TypeOf(resources[4]).String()).Should(ContainSubstring("StatefulSet")) + // Expect(reflect.TypeOf(resources[5]).String()).Should(ContainSubstring("Service")) + // }) + // }) }) diff --git a/internal/controller/plan/restore.go b/internal/controller/plan/restore.go new file mode 100644 index 000000000..4c7d39c1c --- /dev/null +++ b/internal/controller/plan/restore.go @@ -0,0 +1,770 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plan + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "time" + + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + k8sruntime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/builder" + "github.com/apecloud/kubeblocks/internal/controller/component" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" +) + +// RestoreManager restores manager functions +// 1. support datafile/snapshot restore +// 2. support point in time recovery (PITR) +type RestoreManager struct { + client.Client + Ctx context.Context + Cluster *appsv1alpha1.Cluster + Scheme *k8sruntime.Scheme + + // private + namespace string + restoreTime *metav1.Time + sourceCluster string +} + +const ( + backupVolumePATH = "/backupdata" +) + +// DoRestore prepares restore jobs +func DoRestore(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, schema *k8sruntime.Scheme) error { + if cluster.Status.ObservedGeneration > 1 { + return nil + } + + mgr := RestoreManager{ + Cluster: cluster, + Client: cli, + Ctx: ctx, + Scheme: schema, + } + + // check restore from backup + backupObj, err := mgr.getBackupObjectFromAnnotation(component) + if err != nil { + return err + } + if backupObj == nil { + return nil + } + + if err = mgr.createDataPVCs(component, backupObj); err != nil { + return err + } + jobs := make([]client.Object, 0) + if backupObj.Spec.BackupType == dpv1alpha1.BackupTypeDataFile { + dataFileJobs, err := mgr.buildDatafileRestoreJob(component, backupObj) + if err != nil { + return err + } + + logicJobs, err := mgr.buildLogicRestoreJob(component, backupObj) + if err != nil { + return err + } + jobs = append(jobs, dataFileJobs...) + jobs = append(jobs, logicJobs...) + } + + // create and waiting job finished + if err = mgr.createJobsAndWaiting(jobs); err != nil { + return err + } + + // do clean up + if err = mgr.cleanupClusterAnnotations(); err != nil { + return err + } + if err = mgr.cleanupJobs(jobs); err != nil { + return err + } + return nil +} + +// DoPITR prepares PITR jobs +func DoPITR(ctx context.Context, cli client.Client, cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, schema *k8sruntime.Scheme) error { + if cluster.Status.ObservedGeneration > 1 { + return nil + } + + pitrMgr := RestoreManager{ + Cluster: cluster, + Client: cli, + Ctx: ctx, + Scheme: schema, + } + + if need, err := pitrMgr.checkPITRAndInit(); err != nil { + return err + } else if !need { + return nil + } + + // get the latest backup from point in time + backupObj, err := pitrMgr.getLatestBaseBackup(component.Name) + if err != nil { + return err + } + + if err = pitrMgr.createDataPVCs(component, backupObj); err != nil { + return err + } + + jobs := make([]client.Object, 0) + if backupObj.Spec.BackupType == dpv1alpha1.BackupTypeDataFile { + dataFilejobs, err := pitrMgr.buildDatafileRestoreJob(component, backupObj) + if err != nil { + return err + } + // do create datafile restore job and check completed + if err = pitrMgr.createJobsAndWaiting(dataFilejobs); err != nil { + return err + } + jobs = append(jobs, dataFilejobs...) + } + + pitrJobs, err := pitrMgr.buildPITRRestoreJob(component) + if err != nil { + return err + } + + logfileBackup, err := pitrMgr.getLogfileBackup(component.Name) + if err != nil { + return err + } + logicJobs, err := pitrMgr.buildLogicRestoreJob(component, logfileBackup) + if err != nil { + return err + } + pitrJobs = append(pitrJobs, logicJobs...) + + // do create PITR job and check completed + if err = pitrMgr.createJobsAndWaiting(pitrJobs); err != nil { + return err + } + + // do clean up + if err = pitrMgr.cleanupClusterAnnotations(); err != nil { + return err + } + jobs = append(jobs, pitrJobs...) + if err = pitrMgr.cleanupJobs(jobs); err != nil { + return err + } + return nil +} + +func (p *RestoreManager) listCompletedBackups(componentName string) (backupItems []dpv1alpha1.Backup, err error) { + backups := dpv1alpha1.BackupList{} + if err := p.Client.List(p.Ctx, &backups, + client.InNamespace(p.namespace), + client.MatchingLabels(map[string]string{ + constant.AppInstanceLabelKey: p.sourceCluster, + constant.KBAppComponentLabelKey: componentName, + }), + ); err != nil { + return nil, err + } + + backupItems = []dpv1alpha1.Backup{} + for _, b := range backups.Items { + if b.Status.Phase == dpv1alpha1.BackupCompleted && b.Status.Manifests != nil && b.Status.Manifests.BackupLog != nil { + backupItems = append(backupItems, b) + } + } + return backupItems, nil +} + +// getSortedBackups sorts by StopTime +func (p *RestoreManager) getSortedBackups(componentName string, reverse bool) ([]dpv1alpha1.Backup, error) { + backups, err := p.listCompletedBackups(componentName) + if err != nil { + return backups, err + } + sort.Slice(backups, func(i, j int) bool { + if reverse { + i, j = j, i + } + if backups[i].Status.Manifests.BackupLog.StopTime == nil && backups[j].Status.Manifests.BackupLog.StopTime != nil { + return false + } + if backups[i].Status.Manifests.BackupLog.StopTime != nil && backups[j].Status.Manifests.BackupLog.StopTime == nil { + return true + } + if backups[i].Status.Manifests.BackupLog.StopTime.Equal(backups[j].Status.Manifests.BackupLog.StopTime) { + return backups[i].Name < backups[j].Name + } + return backups[i].Status.Manifests.BackupLog.StopTime.Before(backups[j].Status.Manifests.BackupLog.StopTime) + }) + return backups, nil +} + +// getLatestBaseBackup gets the latest baseBackup +func (p *RestoreManager) getLatestBaseBackup(componentName string) (*dpv1alpha1.Backup, error) { + // 1. sorts reverse backups + backups, err := p.getSortedBackups(componentName, true) + if err != nil { + return nil, err + } + + // 2. gets the latest backup object + var latestBackup *dpv1alpha1.Backup + for _, item := range backups { + if item.Spec.BackupType != dpv1alpha1.BackupTypeLogFile && + item.Status.Manifests.BackupLog.StopTime != nil && !p.restoreTime.Before(item.Status.Manifests.BackupLog.StopTime) { + latestBackup = &item + break + } + } + if latestBackup == nil { + return nil, errors.New("can not found latest base backup") + } + + return latestBackup, nil +} + +// checkPITRAndInit checks if cluster need to be restored +func (p *RestoreManager) checkPITRAndInit() (need bool, err error) { + // checks args if pitr supported + cluster := p.Cluster + if cluster.Annotations[constant.RestoreFromTimeAnnotationKey] == "" { + return false, nil + } + restoreTimeStr := cluster.Annotations[constant.RestoreFromTimeAnnotationKey] + sourceCuster := cluster.Annotations[constant.RestoreFromSrcClusterAnnotationKey] + if sourceCuster == "" { + return false, errors.New("need specify a source cluster name to recovery") + } + restoreTime := &metav1.Time{} + if err = restoreTime.UnmarshalQueryParameter(restoreTimeStr); err != nil { + return false, err + } + vctCount := 0 + for _, item := range cluster.Spec.ComponentSpecs { + vctCount += len(item.VolumeClaimTemplates) + } + if vctCount == 0 { + return false, errors.New("not support pitr without any volume claim templates") + } + + // init args + p.restoreTime = restoreTime + p.sourceCluster = sourceCuster + p.namespace = cluster.Namespace + return true, nil +} + +func getVolumeMount(spec *dpv1alpha1.BackupToolSpec) string { + dataVolumeMount := "/data" + // TODO: hack it because the mount path is not explicitly specified in cluster definition + for _, env := range spec.Env { + if env.Name == "VOLUME_DATA_DIR" { + dataVolumeMount = env.Value + break + } + } + return dataVolumeMount +} + +func (p *RestoreManager) getRecoveryInfo(componentName string) (*dpv1alpha1.BackupToolSpec, error) { + // gets scripts from backup template + toolList := dpv1alpha1.BackupToolList{} + // TODO: The reference PITR backup tool needs a stronger reference relationship, for now use label references + if err := p.Client.List(p.Ctx, &toolList, + client.MatchingLabels{ + constant.ClusterDefLabelKey: p.Cluster.Spec.ClusterDefRef, + constant.BackupToolTypeLabelKey: "pitr", + }); err != nil { + + return nil, err + } + if len(toolList.Items) == 0 { + return nil, errors.New("not support recovery because of non-existed pitr backupTool") + } + logfileBackup, err := p.getLogfileBackup(componentName) + if err != nil { + return nil, err + } + spec := &toolList.Items[0].Spec + timeFormat := time.RFC3339 + envTimeEnvIdx := -1 + for i, env := range spec.Env { + if env.Value == "$KB_RECOVERY_TIME" { + envTimeEnvIdx = i + } else if env.Name == "TIME_FORMAT" { + timeFormat = env.Value + } + } + if envTimeEnvIdx != -1 { + spec.Env[envTimeEnvIdx].Value = p.restoreTime.Time.UTC().Format(timeFormat) + } + backupDIR := logfileBackup.Name + if logfileBackup.Status.Manifests != nil && logfileBackup.Status.Manifests.BackupTool != nil { + backupDIR = logfileBackup.Status.Manifests.BackupTool.FilePath + } + headEnv := []corev1.EnvVar{ + {Name: "BACKUP_DIR", Value: backupVolumePATH + "/" + backupDIR}, + {Name: "BACKUP_NAME", Value: logfileBackup.Name}} + spec.Env = append(headEnv, spec.Env...) + return spec, nil +} + +func (p *RestoreManager) getLogfileBackup(componentName string) (*dpv1alpha1.Backup, error) { + incrementalBackupList := dpv1alpha1.BackupList{} + if err := p.Client.List(p.Ctx, &incrementalBackupList, + client.MatchingLabels{ + constant.AppInstanceLabelKey: p.sourceCluster, + constant.KBAppComponentLabelKey: componentName, + constant.BackupTypeLabelKeyKey: string(dpv1alpha1.BackupTypeLogFile), + }); err != nil { + return nil, err + } + if len(incrementalBackupList.Items) == 0 { + return nil, errors.New("not found logfile backups") + } + return &incrementalBackupList.Items[0], nil +} + +func (p *RestoreManager) getLogfilePVC(componentName string) (*corev1.PersistentVolumeClaim, error) { + logfileBackup, err := p.getLogfileBackup(componentName) + if err != nil { + return nil, err + } + pvcKey := types.NamespacedName{ + Name: logfileBackup.Status.PersistentVolumeClaimName, + Namespace: logfileBackup.Namespace, + } + pvc := corev1.PersistentVolumeClaim{} + if err := p.Client.Get(p.Ctx, pvcKey, &pvc); err != nil { + return nil, err + } + return &pvc, nil +} + +func (p *RestoreManager) getDataPVCs(componentName string) ([]corev1.PersistentVolumeClaim, error) { + dataPVCList := corev1.PersistentVolumeClaimList{} + pvcLabels := map[string]string{ + constant.AppInstanceLabelKey: p.Cluster.Name, + constant.KBAppComponentLabelKey: componentName, + constant.VolumeTypeLabelKey: string(appsv1alpha1.VolumeTypeData), + } + if err := p.Client.List(p.Ctx, &dataPVCList, + client.InNamespace(p.namespace), + client.MatchingLabels(pvcLabels)); err != nil { + return nil, err + } + return dataPVCList.Items, nil +} + +// When the pvc has been bound on the determined pod, +// this is a little different from the getDataPVCs function, +// we need to get the node name of the pvc according to the pod, +// and the job must be the same as the node name of the pvc +func (p *RestoreManager) getDataPVCsFromPods(componentName string) ([]corev1.PersistentVolumeClaim, error) { + podList := corev1.PodList{} + podLabels := map[string]string{ + constant.AppInstanceLabelKey: p.Cluster.Name, + constant.KBAppComponentLabelKey: componentName, + } + if err := p.Client.List(p.Ctx, &podList, + client.InNamespace(p.namespace), + client.MatchingLabels(podLabels)); err != nil { + return nil, err + } + dataPVCs := []corev1.PersistentVolumeClaim{} + for _, targetPod := range podList.Items { + for _, volume := range targetPod.Spec.Volumes { + if volume.PersistentVolumeClaim == nil { + continue + } + dataPVC := corev1.PersistentVolumeClaim{} + pvcKey := types.NamespacedName{Namespace: targetPod.Namespace, Name: volume.PersistentVolumeClaim.ClaimName} + if err := p.Client.Get(p.Ctx, pvcKey, &dataPVC); err != nil { + return nil, err + } + if dataPVC.Labels[constant.VolumeTypeLabelKey] != string(appsv1alpha1.VolumeTypeData) { + continue + } + if dataPVC.Annotations == nil { + dataPVC.Annotations = map[string]string{} + } + dataPVC.Annotations["pod-name"] = targetPod.Name + dataPVC.Annotations["node-name"] = targetPod.Spec.NodeName + dataPVCs = append(dataPVCs, dataPVC) + } + } + return dataPVCs, nil +} + +func (p *RestoreManager) createDataPVCs(synthesizedComponent *component.SynthesizedComponent, backup *dpv1alpha1.Backup) error { + // determines the data volume type + vctMap := map[string]corev1.PersistentVolumeClaimTemplate{} + for _, vct := range synthesizedComponent.VolumeClaimTemplates { + vctMap[vct.Name] = vct + } + var vct corev1.PersistentVolumeClaimTemplate + for _, vt := range synthesizedComponent.VolumeTypes { + if vt.Type == appsv1alpha1.VolumeTypeData { + vct = vctMap[vt.Name] + } + } + if vct.Name == "" { + return intctrlutil.NewNotFound("can not found any PersistentVolumeClaim of data type") + } + + sts := &appsv1.StatefulSet{} + sts.Labels = map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: p.Cluster.Name, + constant.KBAppComponentLabelKey: synthesizedComponent.Name, + constant.AppNameLabelKey: synthesizedComponent.Name, + } + snapshotName := "" + if backup != nil && backup.Spec.BackupType == dpv1alpha1.BackupTypeSnapshot { + snapshotName = backup.Name + } + for i := int32(0); i < synthesizedComponent.Replicas; i++ { + pvcName := fmt.Sprintf("%s-%s-%s-%d", vct.Name, p.Cluster.Name, synthesizedComponent.Name, i) + pvcKey := types.NamespacedName{Namespace: p.Cluster.Namespace, Name: pvcName} + pvc, err := builder.BuildPVCFromSnapshot(sts, vct, pvcKey, snapshotName, synthesizedComponent) + if err != nil { + return err + } + // Prevents halt recovery from checking uncleaned resources + if pvc.Annotations == nil { + pvc.Annotations = map[string]string{} + } + pvc.Annotations[constant.LastAppliedClusterAnnotationKey] = + fmt.Sprintf(`{"metadata":{"uid":"%s","name":"%s"}}`, p.Cluster.UID, p.Cluster.Name) + + if err = p.Client.Create(p.Ctx, pvc); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + } + return nil +} + +func (p *RestoreManager) getBackupObjectFromAnnotation(synthesizedComponent *component.SynthesizedComponent) (*dpv1alpha1.Backup, error) { + compBackupMapString := p.Cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] + if len(compBackupMapString) == 0 { + return nil, nil + } + compBackupMap := map[string]string{} + err := json.Unmarshal([]byte(compBackupMapString), &compBackupMap) + if err != nil { + return nil, err + } + backupSourceName, ok := compBackupMap[synthesizedComponent.Name] + if !ok { + return nil, nil + } + + backup := &dpv1alpha1.Backup{} + if err = p.Client.Get(p.Ctx, types.NamespacedName{Name: backupSourceName, Namespace: p.Cluster.Namespace}, backup); err != nil { + return nil, err + } + return backup, nil +} + +func (p *RestoreManager) buildDatafileRestoreJob(synthesizedComponent *component.SynthesizedComponent, backup *dpv1alpha1.Backup) (objs []client.Object, err error) { + backupToolKey := client.ObjectKey{Name: backup.Status.BackupToolName} + backupTool := dpv1alpha1.BackupTool{} + if err = p.Client.Get(p.Ctx, backupToolKey, &backupTool); err != nil { + return nil, err + } + + // builds backup volumes + backupVolumeName := fmt.Sprintf("%s-%s", synthesizedComponent.Name, backup.Status.PersistentVolumeClaimName) + remoteVolume := corev1.Volume{ + Name: backupVolumeName, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: backup.Status.PersistentVolumeClaimName, + }, + }, + } + + // builds volumeMounts + remoteVolumeMount := corev1.VolumeMount{} + remoteVolumeMount.Name = backupVolumeName + remoteVolumeMount.MountPath = "/" + backup.Name + allVolumeMounts := make([]corev1.VolumeMount, 0) + allVolumeMounts = append(allVolumeMounts, remoteVolumeMount) + allVolumeMounts = append(allVolumeMounts, synthesizedComponent.PodSpec.Containers[0].VolumeMounts...) + volumeMountMap := map[string]corev1.VolumeMount{} + for _, mount := range allVolumeMounts { + volumeMountMap[mount.Name] = mount + } + + // builds env + backupDataPath := fmt.Sprintf("/%s/%s", backup.Name, backup.Namespace) + manifests := backup.Status.Manifests + if manifests != nil && manifests.BackupTool != nil { + backupDataPath = fmt.Sprintf("/%s%s", backup.Name, manifests.BackupTool.FilePath) + } + env := []corev1.EnvVar{ + { + Name: "BACKUP_NAME", + Value: backup.Name, + }, { + Name: "BACKUP_DIR", + Value: backupDataPath, + }} + // merges env from backup tool. + env = append(env, backupTool.Spec.Env...) + objs = make([]client.Object, 0) + jobNamePrefix := fmt.Sprintf("base-%s-%s", p.Cluster.Name, synthesizedComponent.Name) + for i := int32(0); i < synthesizedComponent.Replicas; i++ { + // merge volume and volumeMounts + vct := synthesizedComponent.VolumeClaimTemplates[0] + dataVolume := corev1.Volume{ + Name: vct.Name, + VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ + ClaimName: fmt.Sprintf("%s-%s-%s-%d", vct.Name, p.Cluster.Name, synthesizedComponent.Name, i), + }, + }, + } + volumes := make([]corev1.Volume, 0) + volumes = append(volumes, remoteVolume, dataVolume) + volumes = append(volumes, synthesizedComponent.PodSpec.Volumes...) + volumeMounts := make([]corev1.VolumeMount, 0) + for _, volume := range volumes { + volumeMounts = append(volumeMounts, volumeMountMap[volume.Name]) + } + + jobName := fmt.Sprintf("%s-%d", jobNamePrefix, i) + job, err := builder.BuildRestoreJob(jobName, p.Cluster.Namespace, backupTool.Spec.Image, []string{"sh", "-c"}, + backupTool.Spec.Physical.RestoreCommands, volumes, volumeMounts, env, backupTool.Spec.Resources) + if err != nil { + return nil, err + } + if err = controllerutil.SetControllerReference(p.Cluster, job, p.Scheme); err != nil { + return nil, err + } + objs = append(objs, job) + } + return objs, nil +} + +func (p *RestoreManager) buildPITRRestoreJob(synthesizedComponent *component.SynthesizedComponent) (objs []client.Object, err error) { + commonLabels := map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: p.Cluster.Name, + constant.KBAppComponentLabelKey: synthesizedComponent.Name, + } + // gets data dir pvc name + dataPVCs, err := p.getDataPVCs(synthesizedComponent.Name) + if err != nil { + return objs, err + } + if len(dataPVCs) == 0 { + return objs, errors.New("not found data pvc") + } + recoveryInfo, err := p.getRecoveryInfo(synthesizedComponent.Name) + if err != nil { + return objs, err + } + // renders the pitrJob cue template + image := recoveryInfo.Image + if image == "" { + image = synthesizedComponent.PodSpec.Containers[0].Image + } + logfilePVC, err := p.getLogfilePVC(synthesizedComponent.Name) + if err != nil { + return objs, err + } + dataVolumeMount := getVolumeMount(recoveryInfo) + volumeMounts := []corev1.VolumeMount{ + {Name: "data", MountPath: dataVolumeMount}, + {Name: "log", MountPath: backupVolumePATH}, + } + // creates physical restore job + for _, dataPVC := range dataPVCs { + volumes := []corev1.Volume{ + {Name: "data", VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: dataPVC.GetName()}}}, + {Name: "log", VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: logfilePVC.GetName()}}}, + } + pitrJobName := fmt.Sprintf("pitr-phy-%s", dataPVC.GetName()) + pitrJob, err := builder.BuildRestoreJob(pitrJobName, p.namespace, image, []string{"sh", "-c"}, + recoveryInfo.Physical.RestoreCommands, volumes, volumeMounts, recoveryInfo.Env, recoveryInfo.Resources) + if err != nil { + return objs, err + } + if err = controllerutil.SetControllerReference(p.Cluster, pitrJob, p.Scheme); err != nil { + return nil, err + } + pitrJob.SetLabels(commonLabels) + // collect pvcs and jobs for later deletion + objs = append(objs, pitrJob) + } + + return objs, nil +} + +func (p *RestoreManager) buildLogicRestoreJob(synthesizedComponent *component.SynthesizedComponent, backup *dpv1alpha1.Backup) (objs []client.Object, err error) { + // creates logic restore job, usually imported after the cluster service is started + if p.Cluster.Status.Phase != appsv1alpha1.RunningClusterPhase { + return nil, nil + } + backupToolKey := client.ObjectKey{Name: backup.Status.BackupToolName} + backupTool := dpv1alpha1.BackupTool{} + if err = p.Client.Get(p.Ctx, backupToolKey, &backupTool); err != nil { + return nil, err + } + if len(backupTool.Spec.Logical.RestoreCommands) == 0 { + return nil, nil + } + + image := backupTool.Spec.Image + if image == "" { + image = synthesizedComponent.PodSpec.Containers[0].Image + } + commonLabels := map[string]string{ + constant.AppManagedByLabelKey: constant.AppName, + constant.AppInstanceLabelKey: p.Cluster.Name, + constant.KBAppComponentLabelKey: synthesizedComponent.Name, + } + + dataVolumeMount := getVolumeMount(&backupTool.Spec) + volumeMounts := []corev1.VolumeMount{ + {Name: "data", MountPath: dataVolumeMount}, + {Name: "backup-data", MountPath: backupVolumePATH}, + } + dataPVCsFromPods, err := p.getDataPVCsFromPods(synthesizedComponent.Name) + if err != nil { + return objs, err + } + for _, dataPVC := range dataPVCsFromPods { + volumes := []corev1.Volume{ + {Name: "data", VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: dataPVC.GetName()}}}, + {Name: "backup-data", VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: backup.Status.PersistentVolumeClaimName}}}, + } + logicJobName := fmt.Sprintf("restore-logic-%s", dataPVC.GetName()) + logicJob, err := builder.BuildRestoreJob(logicJobName, p.namespace, image, []string{"sh", "-c"}, + backupTool.Spec.Logical.RestoreCommands, volumes, volumeMounts, backupTool.Spec.Env, backupTool.Spec.Resources) + if err != nil { + return objs, err + } + if err = controllerutil.SetControllerReference(p.Cluster, logicJob, p.Scheme); err != nil { + return nil, err + } + logicJob.SetLabels(commonLabels) + // DO NOT use "volume.kubernetes.io/selected-node" annotation key in PVC, because it is unreliable. + logicJob.Spec.Template.Spec.NodeName = dataPVC.Annotations["node-name"] + objs = append(objs, logicJob) + } + + return objs, nil +} + +func (p *RestoreManager) checkJobDone(key client.ObjectKey) (bool, error) { + result := &batchv1.Job{} + if err := p.Client.Get(p.Ctx, key, result); err != nil { + if apierrors.IsNotFound(err) { + return false, nil + } + // if err is NOT "not found", that means unknown error. + return false, err + } + if result.Status.Conditions != nil && len(result.Status.Conditions) > 0 { + jobStatusCondition := result.Status.Conditions[0] + if jobStatusCondition.Type == batchv1.JobComplete { + return true, nil + } else if jobStatusCondition.Type == batchv1.JobFailed { + return true, errors.New(jobStatusCondition.Reason) + } + } + // if found, return true + return false, nil +} + +func (p *RestoreManager) createJobsAndWaiting(objs []client.Object) error { + // creates and checks into different loops to support concurrent resource creation. + for _, job := range objs { + fetchedJob := &batchv1.Job{} + if err := p.Client.Get(p.Ctx, client.ObjectKeyFromObject(job), fetchedJob); err != nil { + if !apierrors.IsNotFound(err) { + return err + } + if err = p.Client.Create(p.Ctx, job); err != nil && !apierrors.IsAlreadyExists(err) { + return err + } + } + } + for _, job := range objs { + if done, err := p.checkJobDone(client.ObjectKeyFromObject(job)); err != nil { + return err + } else if !done { + return intctrlutil.NewErrorf(intctrlutil.ErrorTypeNeedWaiting, "waiting restore job %s", job.GetName()) + } + } + return nil +} + +func (p *RestoreManager) cleanupJobs(objs []client.Object) error { + if p.Cluster.Status.Phase == appsv1alpha1.RunningClusterPhase { + for _, obj := range objs { + if err := intctrlutil.BackgroundDeleteObject(p.Client, p.Ctx, obj); err != nil { + return err + } + } + } + return nil +} + +func (p *RestoreManager) cleanupClusterAnnotations() error { + if p.Cluster.Status.Phase == appsv1alpha1.RunningClusterPhase && p.Cluster.Annotations != nil { + cluster := p.Cluster + patch := client.MergeFrom(cluster.DeepCopy()) + delete(cluster.Annotations, constant.RestoreFromSrcClusterAnnotationKey) + delete(cluster.Annotations, constant.RestoreFromTimeAnnotationKey) + delete(cluster.Annotations, constant.RestoreFromBackUpAnnotationKey) + return p.Client.Patch(p.Ctx, cluster, patch) + } + return nil +} diff --git a/internal/controller/plan/restore_test.go b/internal/controller/plan/restore_test.go new file mode 100644 index 000000000..48f7ea585 --- /dev/null +++ b/internal/controller/plan/restore_test.go @@ -0,0 +1,320 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package plan + +import ( + "fmt" + "time" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + batchv1 "k8s.io/api/batch/v1" + "k8s.io/client-go/kubernetes/scheme" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + dpv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/controller/component" + intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + "github.com/apecloud/kubeblocks/internal/generics" + testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" +) + +var _ = Describe("PITR Functions", func() { + const defaultTTL = "7d" + const backupName = "test-backup-job" + const sourceCluster = "source-cluster" + + var ( + randomStr = testCtx.GetRandomStr() + clusterName = "cluster-for-pitr-" + randomStr + backupToolName string + + now = metav1.Now() + startTime = metav1.Time{Time: now.Add(-time.Hour * 2)} + stopTime = metav1.Time{Time: now.Add(time.Hour * 2)} + ) + + cleanEnv := func() { + // must wait till resources deleted and no longer existed before the testcases start, + // otherwise if later it needs to create some new resource objects with the same name, + // in race conditions, it will find the existence of old objects, resulting failure to + // create the new objects. + By("clean resources") + + // delete cluster(and all dependent sub-resources), clusterversion and clusterdef + testapps.ClearClusterResources(&testCtx) + inNS := client.InNamespace(testCtx.DefaultNamespace) + ml := client.HasLabels{testCtx.TestObjLabelKey} + + deletionPropagation := metav1.DeletePropagationBackground + deletionGracePeriodSeconds := int64(0) + opts := client.DeleteAllOfOptions{ + DeleteOptions: client.DeleteOptions{ + GracePeriodSeconds: &deletionGracePeriodSeconds, + PropagationPolicy: &deletionPropagation, + }, + } + testapps.ClearResources(&testCtx, generics.PodSignature, inNS, ml, &opts) + testapps.ClearResources(&testCtx, generics.BackupSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.BackupPolicySignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.JobSignature, inNS, ml) + testapps.ClearResources(&testCtx, generics.CronJobSignature, inNS, ml) + testapps.ClearResourcesWithRemoveFinalizerOption(&testCtx, generics.PersistentVolumeClaimSignature, true, inNS, ml) + // + // non-namespaced + testapps.ClearResources(&testCtx, generics.BackupPolicyTemplateSignature, ml) + } + + BeforeEach(cleanEnv) + + AfterEach(cleanEnv) + + Context("Test PITR", func() { + const ( + clusterDefName = "test-clusterdef" + clusterVersionName = "test-clusterversion" + mysqlCompType = "replicasets" + mysqlCompName = "mysql" + nginxCompType = "proxy" + ) + + var ( + clusterDef *appsv1alpha1.ClusterDefinition + clusterVersion *appsv1alpha1.ClusterVersion + cluster *appsv1alpha1.Cluster + synthesizedComponent *component.SynthesizedComponent + pvc *corev1.PersistentVolumeClaim + backup *dpv1alpha1.Backup + ) + + BeforeEach(func() { + clusterDef = testapps.NewClusterDefFactory(clusterDefName). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatelessNginxComponent, nginxCompType). + Create(&testCtx).GetObject() + clusterVersion = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefName). + AddComponentVersion(mysqlCompType). + AddContainerShort("mysql", testapps.ApeCloudMySQLImage). + AddComponentVersion(nginxCompType). + AddInitContainerShort("nginx-init", testapps.NginxImage). + AddContainerShort("nginx", testapps.NginxImage). + Create(&testCtx).GetObject() + pvcSpec := testapps.NewPVCSpec("1Gi") + cluster = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, + clusterDef.Name, clusterVersion.Name). + AddComponent(mysqlCompName, mysqlCompType). + AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). + AddRestorePointInTime(metav1.Time{Time: stopTime.Time}, sourceCluster). + Create(&testCtx).GetObject() + + By("By mocking a pvc") + pvc = testapps.NewPersistentVolumeClaimFactory( + testCtx.DefaultNamespace, "data-"+clusterName+"-"+mysqlCompName+"-0", clusterName, mysqlCompName, "data"). + SetStorage("1Gi"). + Create(&testCtx).GetObject() + + By("By mocking a pod") + volume := corev1.Volume{Name: pvc.Name, VolumeSource: corev1.VolumeSource{ + PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ClaimName: pvc.Name}}} + _ = testapps.NewPodFactory(testCtx.DefaultNamespace, clusterName+"-"+mysqlCompName+"-0"). + AddAppInstanceLabel(clusterName). + AddAppComponentLabel(mysqlCompName). + AddAppManangedByLabel(). + AddVolume(volume). + AddContainer(corev1.Container{Name: testapps.DefaultMySQLContainerName, Image: testapps.ApeCloudMySQLImage}). + AddNodeName("fake-node-name"). + Create(&testCtx).GetObject() + + By("By creating backup tool: ") + backupSelfDefineObj := &dpv1alpha1.BackupTool{} + backupSelfDefineObj.SetLabels(map[string]string{ + constant.BackupToolTypeLabelKey: "pitr", + constant.ClusterDefLabelKey: clusterDefName, + }) + backupTool := testapps.CreateCustomizedObj(&testCtx, "backup/pitr_backuptool.yaml", + backupSelfDefineObj, testapps.RandomizedObjName()) + backupToolName = backupTool.Name + + backupObj := dpv1alpha1.BackupToolList{} + Expect(testCtx.Cli.List(testCtx.Ctx, &backupObj)).Should(Succeed()) + + By("By creating backup policyTemplate: ") + backupTplLabels := map[string]string{ + constant.ClusterDefLabelKey: clusterDefName, + } + _ = testapps.NewBackupPolicyTemplateFactory("backup-policy-template"). + WithRandomName().SetLabels(backupTplLabels). + AddBackupPolicy(mysqlCompName). + SetClusterDefRef(clusterDefName). + SetBackupToolName(backupToolName). + SetSchedule("0 * * * *", true). + AddDatafilePolicy(). + SetTTL(defaultTTL). + Create(&testCtx).GetObject() + + clusterCompDefObj := clusterDef.Spec.ComponentDefs[0] + synthesizedComponent = &component.SynthesizedComponent{ + PodSpec: clusterCompDefObj.PodSpec, + Probes: clusterCompDefObj.Probes, + LogConfigs: clusterCompDefObj.LogConfigs, + HorizontalScalePolicy: clusterCompDefObj.HorizontalScalePolicy, + VolumeClaimTemplates: cluster.Spec.ComponentSpecs[0].ToVolumeClaimTemplates(), + Name: mysqlCompName, + VolumeTypes: []appsv1alpha1.VolumeTypeSpec{{Name: testapps.DataVolumeName, Type: appsv1alpha1.VolumeTypeData}}, + Replicas: 1, + } + By("By creating remote pvc: ") + remotePVC := testapps.NewPersistentVolumeClaimFactory( + testCtx.DefaultNamespace, "remote-pvc", clusterName, mysqlCompName, "log"). + SetStorage("1Gi"). + Create(&testCtx).GetObject() + + By("By creating base backup: ") + backupLabels := map[string]string{ + constant.AppInstanceLabelKey: sourceCluster, + constant.KBAppComponentLabelKey: mysqlCompName, + constant.BackupTypeLabelKeyKey: string(dpv1alpha1.BackupTypeDataFile), + } + backup = testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). + WithRandomName().SetLabels(backupLabels). + SetBackupPolicyName("test-fake"). + SetBackupType(dpv1alpha1.BackupTypeDataFile). + Create(&testCtx).GetObject() + baseStartTime := &startTime + baseStopTime := &now + backupStatus := dpv1alpha1.BackupStatus{ + Phase: dpv1alpha1.BackupCompleted, + StartTimestamp: baseStartTime, + CompletionTimestamp: baseStopTime, + BackupToolName: backupToolName, + PersistentVolumeClaimName: remotePVC.Name, + Manifests: &dpv1alpha1.ManifestsStatus{ + BackupLog: &dpv1alpha1.BackupLogStatus{ + StartTime: baseStartTime, + StopTime: baseStopTime, + }, + }, + } + patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backup)) + + By("By creating incremental backup: ") + incrBackupLabels := map[string]string{ + constant.AppInstanceLabelKey: sourceCluster, + constant.KBAppComponentLabelKey: mysqlCompName, + constant.BackupTypeLabelKeyKey: string(dpv1alpha1.BackupTypeLogFile), + } + incrStartTime := &startTime + incrStopTime := &stopTime + backupIncr := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). + WithRandomName().SetLabels(incrBackupLabels). + SetBackupPolicyName("test-fake"). + SetBackupType(dpv1alpha1.BackupTypeLogFile). + Create(&testCtx).GetObject() + backupStatus = dpv1alpha1.BackupStatus{ + Phase: dpv1alpha1.BackupCompleted, + StartTimestamp: incrStartTime, + CompletionTimestamp: incrStopTime, + PersistentVolumeClaimName: remotePVC.Name, + BackupToolName: backupToolName, + Manifests: &dpv1alpha1.ManifestsStatus{ + BackupLog: &dpv1alpha1.BackupLogStatus{ + StartTime: incrStartTime, + StopTime: incrStopTime, + }, + }, + } + patchBackupStatus(backupStatus, client.ObjectKeyFromObject(backupIncr)) + }) + + It("Test restore", func() { + By("restore from snapshot backup") + backupSnapshot := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). + WithRandomName(). + SetBackupPolicyName("test-fake"). + SetBackupType(dpv1alpha1.BackupTypeSnapshot). + Create(&testCtx).GetObject() + restoreFromBackup := fmt.Sprintf(`{"%s":"%s"}`, mysqlCompName, backupSnapshot.Name) + cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] = restoreFromBackup + Expect(DoRestore(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme)).Should(Succeed()) + + By("restore from datafile backup") + restoreFromBackup = fmt.Sprintf(`{"%s":"%s"}`, mysqlCompName, backup.Name) + cluster.Annotations[constant.RestoreFromBackUpAnnotationKey] = restoreFromBackup + err := DoRestore(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme) + Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeNeedWaiting)).Should(BeTrue()) + }) + It("Test PITR job run and cleanup", func() { + By("when creating pitr jobs") + cluster.Status.ObservedGeneration = 1 + err := DoPITR(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme) + Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeNeedWaiting)).Should(BeTrue()) + + By("when base backup restore job completed") + baseBackupJobName := fmt.Sprintf("base-%s-%s-0", clusterName, mysqlCompName) + baseBackupJobKey := types.NamespacedName{Namespace: cluster.Namespace, Name: baseBackupJobName} + Eventually(testapps.GetAndChangeObjStatus(&testCtx, baseBackupJobKey, func(fetched *batchv1.Job) { + fetched.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete}} + })).Should(Succeed()) + err = DoPITR(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme) + Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeNeedWaiting)).Should(BeTrue()) + + By("when physical PITR jobs are completed") + jobName := fmt.Sprintf("pitr-phy-data-%s-%s-0", clusterName, mysqlCompName) + jobKey := types.NamespacedName{Namespace: cluster.Namespace, Name: jobName} + Eventually(testapps.GetAndChangeObjStatus(&testCtx, jobKey, func(fetched *batchv1.Job) { + fetched.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete}} + })).Should(Succeed()) + Expect(DoPITR(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme)).Should(Succeed()) + + By("when logic PITR jobs are creating after cluster RUNNING") + Eventually(testapps.GetAndChangeObjStatus(&testCtx, client.ObjectKeyFromObject(cluster), func(fetched *appsv1alpha1.Cluster) { + fetched.Status.Phase = appsv1alpha1.RunningClusterPhase + })).Should(Succeed()) + cluster.Status.Phase = appsv1alpha1.RunningClusterPhase + err = DoPITR(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme) + Expect(intctrlutil.IsTargetError(err, intctrlutil.ErrorTypeNeedWaiting)).Should(BeTrue()) + + By("when logic PITR jobs are completed") + logicJobName := fmt.Sprintf("restore-logic-data-%s-%s-0", clusterName, mysqlCompName) + logicJobKey := types.NamespacedName{Namespace: cluster.Namespace, Name: logicJobName} + Eventually(testapps.GetAndChangeObjStatus(&testCtx, logicJobKey, func(fetched *batchv1.Job) { + fetched.Status.Conditions = []batchv1.JobCondition{{Type: batchv1.JobComplete}} + })).Should(Succeed()) + Expect(DoPITR(ctx, testCtx.Cli, cluster, synthesizedComponent, scheme.Scheme)).Should(Succeed()) + + By("expect all jobs are cleaned") + Eventually(testapps.CheckObjExists(&testCtx, logicJobKey, &batchv1.Job{}, false)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, jobKey, &batchv1.Job{}, false)).Should(Succeed()) + Eventually(testapps.CheckObjExists(&testCtx, baseBackupJobKey, &batchv1.Job{}, false)).Should(Succeed()) + }) + }) +}) + +func patchBackupStatus(status dpv1alpha1.BackupStatus, key types.NamespacedName) { + Eventually(testapps.GetAndChangeObjStatus(&testCtx, key, func(fetched *dpv1alpha1.Backup) { + fetched.Status = status + })).Should(Succeed()) +} diff --git a/internal/controller/plan/suite_test.go b/internal/controller/plan/suite_test.go index ce9cb549b..a13ce2d67 100644 --- a/internal/controller/plan/suite_test.go +++ b/internal/controller/plan/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan diff --git a/internal/controller/plan/template_wrapper.go b/internal/controller/plan/template_wrapper.go index 174908b93..a3be35051 100644 --- a/internal/controller/plan/template_wrapper.go +++ b/internal/controller/plan/template_wrapper.go @@ -1,28 +1,34 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan import ( "context" + "encoding/json" + "reflect" "strings" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" @@ -30,7 +36,7 @@ import ( cfgcore "github.com/apecloud/kubeblocks/internal/configuration" "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/controller/builder" - intctrltypes "github.com/apecloud/kubeblocks/internal/controller/types" + "github.com/apecloud/kubeblocks/internal/controller/component" "github.com/apecloud/kubeblocks/internal/generics" ) @@ -46,15 +52,13 @@ type renderWrapper struct { ctx context.Context cli client.Client cluster *appsv1alpha1.Cluster - params builder.BuilderParams } -func newTemplateRenderWrapper(templateBuilder *configTemplateBuilder, cluster *appsv1alpha1.Cluster, params builder.BuilderParams, ctx context.Context, cli client.Client) renderWrapper { +func newTemplateRenderWrapper(templateBuilder *configTemplateBuilder, cluster *appsv1alpha1.Cluster, ctx context.Context, cli client.Client) renderWrapper { return renderWrapper{ ctx: ctx, cli: cli, cluster: cluster, - params: params, templateBuilder: templateBuilder, templateAnnotations: make(map[string]string), @@ -62,77 +66,81 @@ func newTemplateRenderWrapper(templateBuilder *configTemplateBuilder, cluster *a } } -func (wrapper *renderWrapper) enableRerenderTemplateSpec(cfgCMName string, task *intctrltypes.ReconcileTask) (bool, error) { +func (wrapper *renderWrapper) checkRerenderTemplateSpec(cfgCMName string, localObjs []client.Object) (bool, *corev1.ConfigMap, error) { cmKey := client.ObjectKey{ Name: cfgCMName, Namespace: wrapper.cluster.Namespace, } cmObj := &corev1.ConfigMap{} - localObject := task.GetLocalResourceWithObjectKey(cmKey, generics.ToGVK(cmObj)) + localObject := findMatchedLocalObject(localObjs, cmKey, generics.ToGVK(cmObj)) if localObject != nil { - return false, nil + if cm, ok := localObject.(*corev1.ConfigMap); ok { + return false, cm, nil + } } cmErr := wrapper.cli.Get(wrapper.ctx, cmKey, cmObj) if cmErr != nil && !apierrors.IsNotFound(cmErr) { // An unexpected error occurs - return false, cmErr + return false, nil, cmErr } if cmErr != nil { // Config is not exists - return true, nil + return true, nil, nil } // Config is exists - return cfgcore.IsNotUserReconfigureOperation(cmObj), nil + return cfgcore.IsNotUserReconfigureOperation(cmObj), cmObj, nil } -func (wrapper *renderWrapper) renderConfigTemplate(task *intctrltypes.ReconcileTask) error { - var err error - var enableRerender bool - +func (wrapper *renderWrapper) renderConfigTemplate(cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, localObjs []client.Object) error { scheme, _ := appsv1alpha1.SchemeBuilder.Build() - for _, configSpec := range task.Component.ConfigTemplates { - cmName := cfgcore.GetComponentCfgName(task.Cluster.Name, task.Component.Name, configSpec.Name) - if enableRerender, err = wrapper.enableRerenderTemplateSpec(cmName, task); err != nil { + for _, configSpec := range component.ConfigTemplates { + cmName := cfgcore.GetComponentCfgName(cluster.Name, component.Name, configSpec.Name) + enableRerender, origCMObj, err := wrapper.checkRerenderTemplateSpec(cmName, localObjs) + if err != nil { return err } if !enableRerender { - wrapper.addVolumeMountMeta(configSpec.ComponentTemplateSpec, cmName) + wrapper.addVolumeMountMeta(configSpec.ComponentTemplateSpec, origCMObj, false) continue } - // Generate ConfigMap objects for config files - cm, err := generateConfigMapFromTpl(wrapper.templateBuilder, cmName, configSpec.ConfigConstraintRef, configSpec.ComponentTemplateSpec, - wrapper.params, wrapper.ctx, wrapper.cli, func(m map[string]string) error { + newCMObj, err := generateConfigMapFromTpl(cluster, component, wrapper.templateBuilder, cmName, configSpec.ConfigConstraintRef, + configSpec.ComponentTemplateSpec, wrapper.ctx, wrapper.cli, func(m map[string]string) error { return validateRenderedData(m, configSpec, wrapper.ctx, wrapper.cli) }) if err != nil { return err } - updateCMConfigSpecLabels(cm, configSpec) - if err := wrapper.addRenderedObject(configSpec.ComponentTemplateSpec, cm, scheme); err != nil { + if err := wrapper.checkAndPatchConfigResource(origCMObj, newCMObj.Data); err != nil { + return err + } + updateCMConfigSpecLabels(newCMObj, configSpec) + if err := wrapper.addRenderedObject(configSpec.ComponentTemplateSpec, newCMObj, scheme); err != nil { return err } } return nil } -func (wrapper *renderWrapper) renderScriptTemplate(task *intctrltypes.ReconcileTask) error { +func (wrapper *renderWrapper) renderScriptTemplate(cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent, + localObjs []client.Object) error { scheme, _ := appsv1alpha1.SchemeBuilder.Build() - for _, templateSpec := range task.Component.ScriptTemplates { - cmName := cfgcore.GetComponentCfgName(task.Cluster.Name, task.Component.Name, templateSpec.Name) - if task.GetLocalResourceWithObjectKey(client.ObjectKey{ + for _, templateSpec := range component.ScriptTemplates { + cmName := cfgcore.GetComponentCfgName(cluster.Name, component.Name, templateSpec.Name) + object := findMatchedLocalObject(localObjs, client.ObjectKey{ Name: cmName, - Namespace: wrapper.cluster.Namespace, - }, generics.ToGVK(&corev1.ConfigMap{})) != nil { - wrapper.addVolumeMountMeta(templateSpec, cmName) + Namespace: wrapper.cluster.Namespace}, generics.ToGVK(&corev1.ConfigMap{})) + if object != nil { + wrapper.addVolumeMountMeta(templateSpec, object, false) continue } // Generate ConfigMap objects for config files - cm, err := generateConfigMapFromTpl(wrapper.templateBuilder, cmName, "", templateSpec, wrapper.params, wrapper.ctx, wrapper.cli, nil) + cm, err := generateConfigMapFromTpl(cluster, component, wrapper.templateBuilder, cmName, "", templateSpec, wrapper.ctx, wrapper.cli, nil) if err != nil { return err } @@ -144,21 +152,57 @@ func (wrapper *renderWrapper) renderScriptTemplate(task *intctrltypes.ReconcileT } func (wrapper *renderWrapper) addRenderedObject(templateSpec appsv1alpha1.ComponentTemplateSpec, cm *corev1.ConfigMap, scheme *runtime.Scheme) error { - // The owner of the configmap object is a cluster of users, + // The owner of the configmap object is a cluster, // in order to manage the life cycle of configmap if err := controllerutil.SetOwnerReference(wrapper.cluster, cm, scheme); err != nil { return err } cfgcore.SetParametersUpdateSource(cm, constant.ReconfigureManagerSource) - wrapper.renderedObjs = append(wrapper.renderedObjs, cm) - wrapper.addVolumeMountMeta(templateSpec, cm.Name) + wrapper.addVolumeMountMeta(templateSpec, cm, true) return nil } -func (wrapper *renderWrapper) addVolumeMountMeta(templateSpec appsv1alpha1.ComponentTemplateSpec, cmName string) { - wrapper.volumes[cmName] = templateSpec - wrapper.templateAnnotations[cfgcore.GenerateTPLUniqLabelKeyWithConfig(templateSpec.Name)] = cmName +func (wrapper *renderWrapper) addVolumeMountMeta(templateSpec appsv1alpha1.ComponentTemplateSpec, object client.Object, rendered bool) { + wrapper.volumes[object.GetName()] = templateSpec + if rendered { + wrapper.renderedObjs = append(wrapper.renderedObjs, object) + } + wrapper.templateAnnotations[cfgcore.GenerateTPLUniqLabelKeyWithConfig(templateSpec.Name)] = object.GetName() +} + +func (wrapper *renderWrapper) checkAndPatchConfigResource(origCMObj *corev1.ConfigMap, newData map[string]string) error { + if origCMObj == nil { + return nil + } + if reflect.DeepEqual(origCMObj.Data, newData) { + return nil + } + + patch := client.MergeFrom(origCMObj.DeepCopy()) + origCMObj.Data = newData + if origCMObj.Annotations == nil { + origCMObj.Annotations = make(map[string]string) + } + cfgcore.SetParametersUpdateSource(origCMObj, constant.ReconfigureManagerSource) + rawData, err := json.Marshal(origCMObj.Data) + if err != nil { + return err + } + + origCMObj.Annotations[corev1.LastAppliedConfigAnnotation] = string(rawData) + return wrapper.cli.Patch(wrapper.ctx, origCMObj, patch) +} + +func findMatchedLocalObject(localObjs []client.Object, objKey client.ObjectKey, gvk schema.GroupVersionKind) client.Object { + for _, obj := range localObjs { + if obj.GetName() == objKey.Name && obj.GetNamespace() == objKey.Namespace { + if generics.ToGVK(obj) == gvk { + return obj + } + } + } + return nil } func updateCMConfigSpecLabels(cm *corev1.ConfigMap, configSpec appsv1alpha1.ComponentConfigSpec) { @@ -177,12 +221,13 @@ func updateCMConfigSpecLabels(cm *corev1.ConfigMap, configSpec appsv1alpha1.Comp } } -// generateConfigMapFromTpl render config file by config template provided by provider. -func generateConfigMapFromTpl(tplBuilder *configTemplateBuilder, +// generateConfigMapFromTpl renders config file by config template provided by provider. +func generateConfigMapFromTpl(cluster *appsv1alpha1.Cluster, + component *component.SynthesizedComponent, + tplBuilder *configTemplateBuilder, cmName string, configConstraintName string, templateSpec appsv1alpha1.ComponentTemplateSpec, - params builder.BuilderParams, ctx context.Context, cli client.Client, dataValidator templateRenderValidator) (*corev1.ConfigMap, error) { // Render config template by TplEngine @@ -199,10 +244,10 @@ func generateConfigMapFromTpl(tplBuilder *configTemplateBuilder, } // Using ConfigMap cue template render to configmap of config - return builder.BuildConfigMapWithTemplate(configs, params, cmName, configConstraintName, templateSpec) + return builder.BuildConfigMapWithTemplateLow(cluster, component, configs, cmName, configConstraintName, templateSpec) } -// renderConfigMapTemplate render config file using template engine +// renderConfigMapTemplate renders config file using template engine func renderConfigMapTemplate( templateBuilder *configTemplateBuilder, templateSpec appsv1alpha1.ComponentTemplateSpec, @@ -229,7 +274,7 @@ func renderConfigMapTemplate( return renderedData, nil } -// validateRenderedData validate config file against constraint +// validateRenderedData validates config file against constraint func validateRenderedData( renderedData map[string]string, configSpec appsv1alpha1.ComponentConfigSpec, @@ -246,7 +291,6 @@ func validateRenderedData( return cfgcore.WrapError(err, "failed to get ConfigConstraint, key[%v]", configSpec) } - // NOTE: not require checker configuration template status configChecker := cfgcore.NewConfigValidator(&configConstraint.Spec, cfgcore.WithKeySelector(configSpec.Keys)) // NOTE: It is necessary to verify the correctness of the data diff --git a/internal/controller/plan/tls_utils.go b/internal/controller/plan/tls_utils.go index f76bfd068..9c1f725b6 100644 --- a/internal/controller/plan/tls_utils.go +++ b/internal/controller/plan/tls_utils.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package plan import ( "bytes" + "context" "strings" "text/template" @@ -25,69 +29,16 @@ import ( "github.com/pkg/errors" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" - "k8s.io/apimachinery/pkg/util/sets" - "sigs.k8s.io/controller-runtime/pkg/client" dbaasv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - componentutil "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/controller/builder" client2 "github.com/apecloud/kubeblocks/internal/controller/client" - "github.com/apecloud/kubeblocks/internal/controller/component" - "github.com/apecloud/kubeblocks/internal/controllerutil" ) -func CreateOrCheckTLSCerts(reqCtx controllerutil.RequestCtx, - cli client.Client, - cluster *dbaasv1alpha1.Cluster, -) (*v1.Secret, error) { - if cluster == nil { - return nil, componentutil.ErrReqClusterObj - } - - for _, comp := range cluster.Spec.ComponentSpecs { - if !comp.TLS { - continue - } - // REVIEW/TODO: should do spec validation during validation stage - if comp.Issuer == nil { - return nil, errors.New("issuer shouldn't be nil when tls enabled") - } - switch comp.Issuer.Name { - case dbaasv1alpha1.IssuerUserProvided: - if err := CheckTLSSecretRef(reqCtx, cli, cluster.Namespace, comp.Issuer.SecretRef); err != nil { - return nil, err - } - case dbaasv1alpha1.IssuerKubeBlocks: - return createTLSSecret(reqCtx, cli, cluster, comp.Name) - } - } - return nil, nil -} - -// func deleteTLSSecrets(reqCtx controllerutil.RequestCtx, cli client.Client, secretList []v1.Secret) { -// for _, secret := range secretList { -// err := cli.Delete(reqCtx.Ctx, &secret) -// if err != nil { -// reqCtx.Log.Info("delete tls secret error", "err", err) -// } -// } -// } - -func createTLSSecret(reqCtx controllerutil.RequestCtx, - cli client.Client, - cluster *dbaasv1alpha1.Cluster, - componentName string) (*v1.Secret, error) { - secret, err := ComposeTLSSecret(cluster.Namespace, cluster.Name, componentName) - if err != nil { - return nil, err - } - return secret, nil -} - -// ComposeTLSSecret compose a TSL secret object. +// ComposeTLSSecret composes a TSL secret object. // REVIEW/TODO: // 1. missing public function doc -// 2. should avoid using Go template to call a function, this is too hack & costly, +// 2. should avoid using Go template to call a function, this is too hacky & costly, // should just call underlying registered Go template function. func ComposeTLSSecret(namespace, clusterName, componentName string) (*v1.Secret, error) { secret, err := builder.BuildTLSSecret(namespace, clusterName, componentName) @@ -131,14 +82,14 @@ func buildFromTemplate(tpl string, vars interface{}) (string, error) { return b.String(), nil } -func CheckTLSSecretRef(reqCtx controllerutil.RequestCtx, cli client2.ReadonlyClient, namespace string, +func CheckTLSSecretRef(ctx context.Context, cli client2.ReadonlyClient, namespace string, secretRef *dbaasv1alpha1.TLSSecretRef) error { if secretRef == nil { return errors.New("issuer.secretRef shouldn't be nil when issuer is UserProvided") } secret := &v1.Secret{} - if err := cli.Get(reqCtx.Ctx, types.NamespacedName{Namespace: namespace, Name: secretRef.Name}, secret); err != nil { + if err := cli.Get(ctx, types.NamespacedName{Namespace: namespace, Name: secretRef.Name}, secret); err != nil { return err } if secret.Data == nil { @@ -153,113 +104,6 @@ func CheckTLSSecretRef(reqCtx controllerutil.RequestCtx, cli client2.ReadonlyCli return nil } -func updateTLSVolumeAndVolumeMount(podSpec *v1.PodSpec, clusterName string, component component.SynthesizedComponent) error { - if !component.TLS { - return nil - } - - // update volume - volumes := podSpec.Volumes - volume, err := composeTLSVolume(clusterName, component) - if err != nil { - return err - } - volumes = append(volumes, *volume) - podSpec.Volumes = volumes - - // update volumeMount - for index, container := range podSpec.Containers { - volumeMounts := container.VolumeMounts - volumeMount := composeTLSVolumeMount() - volumeMounts = append(volumeMounts, volumeMount) - podSpec.Containers[index].VolumeMounts = volumeMounts - } - - return nil -} - -func composeTLSVolume(clusterName string, component component.SynthesizedComponent) (*v1.Volume, error) { - if !component.TLS { - return nil, errors.New("can't compose TLS volume when TLS not enabled") - } - if component.Issuer == nil { - return nil, errors.New("issuer shouldn't be nil when TLS enabled") - } - if component.Issuer.Name == dbaasv1alpha1.IssuerUserProvided && - component.Issuer.SecretRef == nil { - return nil, errors.New("secret ref shouldn't be nil when issuer is UserProvided") - } - var secretName, ca, cert, key string - switch component.Issuer.Name { - case dbaasv1alpha1.IssuerKubeBlocks: - secretName = GenerateTLSSecretName(clusterName, component.Name) - ca = builder.CAName - cert = builder.CertName - key = builder.KeyName - case dbaasv1alpha1.IssuerUserProvided: - secretName = component.Issuer.SecretRef.Name - ca = component.Issuer.SecretRef.CA - cert = component.Issuer.SecretRef.Cert - key = component.Issuer.SecretRef.Key - } - volume := v1.Volume{ - Name: builder.VolumeName, - VolumeSource: v1.VolumeSource{ - Secret: &v1.SecretVolumeSource{ - SecretName: secretName, - Items: []v1.KeyToPath{ - {Key: ca, Path: builder.CAName}, - {Key: cert, Path: builder.CertName}, - {Key: key, Path: builder.KeyName}, - }, - Optional: func() *bool { o := false; return &o }(), - }, - }, - } - return &volume, nil -} - -func composeTLSVolumeMount() v1.VolumeMount { - return v1.VolumeMount{ - Name: builder.VolumeName, - MountPath: builder.MountPath, - ReadOnly: true, - } -} - -func IsTLSSettingsUpdated(cType string, oldCm v1.ConfigMap, newCm v1.ConfigMap) bool { - // build intersection sets - oldKeys := make([]string, 0) - for key := range oldCm.Data { - oldKeys = append(oldKeys, key) - } - oldSet := sets.New(oldKeys...) - newKeys := make([]string, 0) - for key := range newCm.Data { - newKeys = append(newKeys, key) - } - newSet := sets.New(newKeys...) - interSet := oldSet.Intersection(newSet) - - // get tls key-word based on cType - tlsKeyWord := GetTLSKeyWord(cType) - - // search key-word in both old and new set - for _, configFileName := range interSet.UnsortedList() { - oldConfigFile := oldCm.Data[configFileName] - newConfigFile := newCm.Data[configFileName] - oldIndex := strings.Index(oldConfigFile, tlsKeyWord) - newIndex := strings.Index(newConfigFile, tlsKeyWord) - // tls key-word appears in one file and disappears in another, means tls settings updated - if oldIndex >= 0 && newIndex < 0 || - oldIndex < 0 && newIndex >= 0 { - return true - } - } - - return false -} - func GetTLSKeyWord(cType string) string { switch cType { case "mysql": diff --git a/internal/controller/types/lifecycle_vertex.go b/internal/controller/types/lifecycle_vertex.go new file mode 100644 index 000000000..6e5520f4e --- /dev/null +++ b/internal/controller/types/lifecycle_vertex.go @@ -0,0 +1,163 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package types + +import ( + "fmt" + + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/internal/controller/graph" +) + +type LifecycleAction string + +const ( + CREATE = LifecycleAction("CREATE") + DELETE = LifecycleAction("DELETE") + UPDATE = LifecycleAction("UPDATE") + PATCH = LifecycleAction("PATCH") + STATUS = LifecycleAction("STATUS") + NOOP = LifecycleAction("NOOP") +) + +// LifecycleVertex describes expected object spec and how to reach it +// obj always represents the expected part: new object in Create/Update action and old object in Delete action +// oriObj is set in Update action +// all transformers doing their object manipulation works on obj.spec +// the root vertex(i.e. the cluster vertex) will be treated specially: +// as all its meta, spec and status can be updated in one reconciliation loop +// Update is ignored when immutable=true +// orphan object will be force deleted when action is DELETE +type LifecycleVertex struct { + Obj client.Object + ObjCopy client.Object + Immutable bool + Orphan bool + Action *LifecycleAction +} + +func (v LifecycleVertex) String() string { + if v.Action == nil { + return fmt.Sprintf("{obj:%T, name: %s, immutable: %v, orphan: %v, action: nil}", + v.Obj, v.Obj.GetName(), v.Immutable, v.Orphan) + } + return fmt.Sprintf("{obj:%T, name: %s, immutable: %v, orphan: %v, action: %v}", + v.Obj, v.Obj.GetName(), v.Immutable, v.Orphan, *v.Action) +} + +func ActionPtr(action LifecycleAction) *LifecycleAction { + return &action +} + +func ActionCreatePtr() *LifecycleAction { + return ActionPtr(CREATE) +} + +func ActionDeletePtr() *LifecycleAction { + return ActionPtr(DELETE) +} + +func ActionUpdatePtr() *LifecycleAction { + return ActionPtr(UPDATE) +} + +func ActionPatchPtr() *LifecycleAction { + return ActionPtr(PATCH) +} + +func ActionStatusPtr() *LifecycleAction { + return ActionPtr(STATUS) +} + +func ActionNoopPtr() *LifecycleAction { + return ActionPtr(NOOP) +} + +func LifecycleObjectCreate(dag *graph.DAG, obj client.Object, parent *LifecycleVertex) *LifecycleVertex { + return addObject(dag, obj, ActionCreatePtr(), parent) +} + +func LifecycleObjectDelete(dag *graph.DAG, obj client.Object, parent *LifecycleVertex) *LifecycleVertex { + vertex := addObject(dag, obj, ActionDeletePtr(), parent) + vertex.Orphan = true + return vertex +} + +func LifecycleObjectUpdate(dag *graph.DAG, obj client.Object, parent *LifecycleVertex) *LifecycleVertex { + return addObject(dag, obj, ActionUpdatePtr(), parent) +} + +func LifecycleObjectPatch(dag *graph.DAG, obj client.Object, objCopy client.Object, parent *LifecycleVertex) *LifecycleVertex { + vertex := addObject(dag, obj, ActionPatchPtr(), parent) + vertex.ObjCopy = objCopy + return vertex +} + +func LifecycleObjectNoop(dag *graph.DAG, obj client.Object, parent *LifecycleVertex) *LifecycleVertex { + return addObject(dag, obj, ActionNoopPtr(), parent) +} + +func addObject(dag *graph.DAG, obj client.Object, action *LifecycleAction, parent *LifecycleVertex) *LifecycleVertex { + if obj == nil { + panic("try to add nil object") + } + vertex := &LifecycleVertex{ + Obj: obj, + Action: action, + } + dag.AddVertex(vertex) + + if parent != nil { + dag.Connect(parent, vertex) + } + return vertex +} + +func FindAll[T interface{}](dag *graph.DAG) []graph.Vertex { + vertices := make([]graph.Vertex, 0) + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*LifecycleVertex) + if _, ok := v.Obj.(T); ok { + vertices = append(vertices, vertex) + } + } + return vertices +} + +func FindAllNot[T interface{}](dag *graph.DAG) []graph.Vertex { + vertices := make([]graph.Vertex, 0) + for _, vertex := range dag.Vertices() { + v, _ := vertex.(*LifecycleVertex) + if _, ok := v.Obj.(T); !ok { + vertices = append(vertices, vertex) + } + } + return vertices +} + +func FindRootVertex(dag *graph.DAG) (*LifecycleVertex, error) { + root := dag.Root() + if root == nil { + return nil, fmt.Errorf("root vertex not found: %v", dag) + } + rootVertex, _ := root.(*LifecycleVertex) + return rootVertex, nil +} diff --git a/internal/controller/types/task.go b/internal/controller/types/task.go deleted file mode 100644 index dfa80c648..000000000 --- a/internal/controller/types/task.go +++ /dev/null @@ -1,77 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 types - -import ( - "k8s.io/apimachinery/pkg/runtime/schema" - "sigs.k8s.io/controller-runtime/pkg/client" - - appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/internal/controller/builder" - "github.com/apecloud/kubeblocks/internal/controller/component" - "github.com/apecloud/kubeblocks/internal/generics" -) - -type ReconcileTask struct { - ClusterDefinition *appsv1alpha1.ClusterDefinition - ClusterVersion *appsv1alpha1.ClusterVersion - Cluster *appsv1alpha1.Cluster - Component *component.SynthesizedComponent - Resources *[]client.Object -} - -func InitReconcileTask(clusterDef *appsv1alpha1.ClusterDefinition, clusterVer *appsv1alpha1.ClusterVersion, - cluster *appsv1alpha1.Cluster, component *component.SynthesizedComponent) *ReconcileTask { - resources := make([]client.Object, 0) - return &ReconcileTask{ - ClusterDefinition: clusterDef, - ClusterVersion: clusterVer, - Cluster: cluster, - Component: component, - Resources: &resources, - } -} - -func (r *ReconcileTask) GetBuilderParams() builder.BuilderParams { - return builder.BuilderParams{ - ClusterDefinition: r.ClusterDefinition, - ClusterVersion: r.ClusterVersion, - Cluster: r.Cluster, - Component: r.Component, - } -} - -func (r *ReconcileTask) AppendResource(objs ...client.Object) { - if r == nil { - return - } - *r.Resources = append(*r.Resources, objs...) -} - -func (r *ReconcileTask) GetLocalResourceWithObjectKey(objKey client.ObjectKey, gvk schema.GroupVersionKind) client.Object { - if r.Resources == nil { - return nil - } - for _, obj := range *r.Resources { - if obj.GetName() == objKey.Name && obj.GetNamespace() == objKey.Namespace { - if generics.ToGVK(obj) == gvk { - return obj - } - } - } - return nil -} diff --git a/internal/controllerutil/container_util.go b/internal/controllerutil/container_util.go index 2571f6a12..0db587d2d 100644 --- a/internal/controllerutil/container_util.go +++ b/internal/controllerutil/container_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil diff --git a/internal/controllerutil/controller_common.go b/internal/controllerutil/controller_common.go index 5bbca501c..06cb2d981 100644 --- a/internal/controllerutil/controller_common.go +++ b/internal/controllerutil/controller_common.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -50,9 +53,8 @@ func Reconciled() (reconcile.Result, error) { return reconcile.Result{}, nil } -// CheckedRequeueWithError is a convenience wrapper around logging an error message -// separate from the stacktrace and then passing the error through to the controller -// manager, this will ignore not-found errors. +// CheckedRequeueWithError passes the error through to the controller +// manager, it ignores unknown errors. func CheckedRequeueWithError(err error, logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) { if apierrors.IsNotFound(err) { return Reconciled() @@ -60,7 +62,7 @@ func CheckedRequeueWithError(err error, logger logr.Logger, msg string, keysAndV return RequeueWithError(err, logger, msg, keysAndValues...) } -// RequeueWithErrorAndRecordEvent requeue when an error occurs. if it is a not found error, send an event +// RequeueWithErrorAndRecordEvent requeues when an error occurs. if it is an unknown error, triggers an event func RequeueWithErrorAndRecordEvent(obj client.Object, recorder record.EventRecorder, err error, logger logr.Logger) (reconcile.Result, error) { if apierrors.IsNotFound(err) { recorder.Eventf(obj, corev1.EventTypeWarning, constant.ReasonNotFoundCR, err.Error()) @@ -68,7 +70,7 @@ func RequeueWithErrorAndRecordEvent(obj client.Object, recorder record.EventReco return RequeueWithError(err, logger, "") } -// RequeueWithError requeue when an error occurs +// RequeueWithError requeues when an error occurs func RequeueWithError(err error, logger logr.Logger, msg string, keysAndValues ...interface{}) (reconcile.Result, error) { if msg == "" { logger.Info(err.Error()) @@ -102,8 +104,8 @@ func Requeue(logger logr.Logger, msg string, keysAndValues ...interface{}) (reco return reconcile.Result{Requeue: true}, nil } -// HandleCRDeletion Handled CR deletion flow, will add finalizer if discovered a non-deleting object and remove finalizer during -// deletion process. Pass optional 'deletionHandler' func for external dependency deletion. Return Result pointer +// HandleCRDeletion handles CR deletion, adds finalizer if found a non-deleting object and removes finalizer during +// deletion process. Passes optional 'deletionHandler' func for external dependency deletion. Returns Result pointer // if required to return out of outer 'Reconcile' reconciliation loop. func HandleCRDeletion(reqCtx RequestCtx, r client.Writer, @@ -113,7 +115,7 @@ func HandleCRDeletion(reqCtx RequestCtx, // examine DeletionTimestamp to determine if object is under deletion if cr.GetDeletionTimestamp().IsZero() { // The object is not being deleted, so if it does not have our finalizer, - // then lets add the finalizer and update the object. This is equivalent + // then add the finalizer and update the object. This is equivalent to // registering our finalizer. if !controllerutil.ContainsFinalizer(cr, finalizer) { controllerutil.AddFinalizer(cr, finalizer) @@ -125,25 +127,25 @@ func HandleCRDeletion(reqCtx RequestCtx, // The object is being deleted if controllerutil.ContainsFinalizer(cr, finalizer) { // We need to record the deletion event first. - // Because if the resource has dependencies, it will not be automatically deleted. - // so it can prevent users from manually deleting it without event records + // If the resource has dependencies, it will not be automatically deleted. + // It can also prevent users from manually deleting it without event records if reqCtx.Recorder != nil { cluster, ok := cr.(*v1alpha1.Cluster) // throw warning event if terminationPolicy set to DoNotTerminate if ok && cluster.Spec.TerminationPolicy == v1alpha1.DoNotTerminate { - reqCtx.Recorder.Eventf(cr, corev1.EventTypeWarning, constant.ReasonDeleteFailed, + reqCtx.Eventf(cr, corev1.EventTypeWarning, constant.ReasonDeleteFailed, "Deleting %s: %s failed due to terminationPolicy set to DoNotTerminate", strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName()) } else { - reqCtx.Recorder.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletingCR, "Deleting %s: %s", + reqCtx.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletingCR, "Deleting %s: %s", strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName()) } } - // our finalizer is present, so lets handle any external dependency + // our finalizer is present, so handle any external dependency if deletionHandler != nil { if res, err := deletionHandler(); err != nil { - // if fail to delete the external dependency here, return with error + // if failed to delete the external dependencies here, return with error // so that it can be retried if res == nil { return ResultToP(CheckedRequeueWithError(err, reqCtx.Log, "")) @@ -159,10 +161,8 @@ func HandleCRDeletion(reqCtx RequestCtx, return ResultToP(CheckedRequeueWithError(err, reqCtx.Log, "")) } // record resources deleted event - if reqCtx.Recorder != nil { - reqCtx.Recorder.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletedCR, "Deleted %s: %s", - strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName()) - } + reqCtx.Eventf(cr, corev1.EventTypeNormal, constant.ReasonDeletedCR, "Deleted %s: %s", + strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName()) } } @@ -173,7 +173,7 @@ func HandleCRDeletion(reqCtx RequestCtx, return nil, nil } -// ValidateReferenceCR validate is exist referencing CRs. if exists, requeue reconcile after 30 seconds +// ValidateReferenceCR validates existing referencing CRs, if exists, requeue reconcile after 30 seconds func ValidateReferenceCR(reqCtx RequestCtx, cli client.Client, obj client.Object, labelKey string, recordEvent func(), objLists ...client.ObjectList) (*ctrl.Result, error) { for _, objList := range objLists { @@ -200,23 +200,25 @@ func ValidateReferenceCR(reqCtx RequestCtx, cli client.Client, obj client.Object return nil, nil } -// RecordCreatedEvent record an event when CR created successfully +// RecordCreatedEvent records an event when a CR created successfully func RecordCreatedEvent(r record.EventRecorder, cr client.Object) { if r != nil && cr.GetGeneration() == 1 { r.Eventf(cr, corev1.EventTypeNormal, constant.ReasonCreatedCR, "Created %s: %s", strings.ToLower(cr.GetObjectKind().GroupVersionKind().Kind), cr.GetName()) } } -// WorkloadFilterPredicate provide filter predicate for workload objects, i.e., deployment/statefulset/pod/pvc. +// WorkloadFilterPredicate provides filter predicate for workload objects, i.e., deployment/statefulset/pod/pvc. func WorkloadFilterPredicate(object client.Object) bool { - objLabels := object.GetLabels() - if objLabels == nil { - return false - } - return objLabels[constant.AppManagedByLabelKey] == constant.AppName + _, containCompNameLabelKey := object.GetLabels()[constant.KBAppComponentLabelKey] + return ManagedByKubeBlocksFilterPredicate(object) && containCompNameLabelKey +} + +// ManagedByKubeBlocksFilterPredicate provides filter predicate for objects managed by kubeBlocks. +func ManagedByKubeBlocksFilterPredicate(object client.Object) bool { + return object.GetLabels()[constant.AppManagedByLabelKey] == constant.AppName } -// IgnoreIsAlreadyExists return errors that is not AlreadyExists +// IgnoreIsAlreadyExists returns errors if 'err' is not type of AlreadyExists func IgnoreIsAlreadyExists(err error) error { if !apierrors.IsAlreadyExists(err) { return err @@ -224,7 +226,7 @@ func IgnoreIsAlreadyExists(err error) error { return nil } -// BackgroundDeleteObject delete the object in the background, usually used in the Reconcile method +// BackgroundDeleteObject deletes the object in the background, usually used in the Reconcile method func BackgroundDeleteObject(cli client.Client, ctx context.Context, obj client.Object) error { deletePropagation := metav1.DeletePropagationBackground deleteOptions := &client.DeleteOptions{ @@ -237,10 +239,17 @@ func BackgroundDeleteObject(cli client.Client, ctx context.Context, obj client.O return nil } -// SetOwnership set owner reference and add finalizer if not exists -func SetOwnership(owner, obj client.Object, scheme *runtime.Scheme, finalizer string) error { - if err := controllerutil.SetControllerReference(owner, obj, scheme); err != nil { - return err +// SetOwnership provides helper function controllerutil.SetControllerReference/controllerutil.SetOwnerReference +// and controllerutil.AddFinalizer if not exists. +func SetOwnership(owner, obj client.Object, scheme *runtime.Scheme, finalizer string, useOwnerReference ...bool) error { + if len(useOwnerReference) > 0 && useOwnerReference[0] { + if err := controllerutil.SetOwnerReference(owner, obj, scheme); err != nil { + return err + } + } else { + if err := controllerutil.SetControllerReference(owner, obj, scheme); err != nil { + return err + } } if !controllerutil.ContainsFinalizer(obj, finalizer) { // pvc objects do not need to add finalizer diff --git a/internal/controllerutil/controller_common_test.go b/internal/controllerutil/controller_common_test.go index 55e66bf8d..195e2e1e1 100644 --- a/internal/controllerutil/controller_common_test.go +++ b/internal/controllerutil/controller_common_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil diff --git a/internal/controllerutil/cue_value.go b/internal/controllerutil/cue_value.go index f85ebce40..f9743f921 100644 --- a/internal/controllerutil/cue_value.go +++ b/internal/controllerutil/cue_value.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -23,6 +26,7 @@ import ( "cuelang.org/go/cue" "cuelang.org/go/cue/cuecontext" cuejson "cuelang.org/go/encoding/json" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) func NewCUETplFromPath(filePathString string) (*CUETpl, error) { @@ -90,6 +94,22 @@ func (v *CUEBuilder) Lookup(path string) ([]byte, error) { return cueValue.MarshalJSON() } +func (v *CUEBuilder) ConvertContentToUnstructured(path string) (*unstructured.Unstructured, error) { + var ( + contentByte []byte + err error + unstructuredObj = &unstructured.Unstructured{} + ) + if contentByte, err = v.Lookup(path); err != nil { + return nil, err + } + if err = json.Unmarshal(contentByte, &unstructuredObj); err != nil { + return nil, err + } + + return unstructuredObj, nil +} + // func (v *CueValue) Render() (string, error) { // b, err := v.Value.MarshalJSON() // str := string(b) diff --git a/internal/controllerutil/cue_value_test.go b/internal/controllerutil/cue_value_test.go index 86f3311a9..0586c4750 100644 --- a/internal/controllerutil/cue_value_test.go +++ b/internal/controllerutil/cue_value_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -51,7 +54,7 @@ type testCUEInputBoolOmitEmpty struct { Flag bool `json:"flag,omitempty"` } -// This test shows that the omitempty tag should be used with care if the field +// This test shows that the omitempty tag should be used with much care if the field // is used in cue template. func TestCUE(t *testing.T) { cueTplIntJSON := ` diff --git a/internal/controllerutil/errors.go b/internal/controllerutil/errors.go index ed4318430..996ca1927 100644 --- a/internal/controllerutil/errors.go +++ b/internal/controllerutil/errors.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -37,10 +40,29 @@ func (v *Error) Error() string { type ErrorType string const ( - // ErrorTypeBackupNotCompleted is used to report backup not completed. - ErrorTypeBackupNotCompleted ErrorType = "BackupNotCompleted" // ErrorWaitCacheRefresh waits for synchronization of the corresponding object cache in client-go from ApiServer. - ErrorWaitCacheRefresh = "WaitCacheRefresh" + ErrorWaitCacheRefresh ErrorType = "WaitCacheRefresh" + // ErrorTypeNotFound not found any resource. + ErrorTypeNotFound ErrorType = "NotFound" + + ErrorTypeRequeue ErrorType = "Requeue" // requeue for reconcile. + + // ErrorType for backup + ErrorTypeBackupNotSupported ErrorType = "BackupNotSupported" // this backup type not supported + ErrorTypeBackupPVTemplateNotFound ErrorType = "BackupPVTemplateNotFound" // this pv template not found + ErrorTypeBackupNotCompleted ErrorType = "BackupNotCompleted" // report backup not completed. + ErrorTypeBackupPVCNameIsEmpty ErrorType = "BackupPVCNameIsEmpty" // pvc name for backup is empty + ErrorTypeBackupJobFailed ErrorType = "BackupJobFailed" // backup job failed + ErrorTypeStorageNotMatch ErrorType = "ErrorTypeStorageNotMatch" + ErrorTypeReconfigureFailed ErrorType = "ErrorTypeReconfigureFailed" + + // ErrorType for cluster controller + ErrorTypeBackupFailed ErrorType = "BackupFailed" + ErrorTypeNeedWaiting ErrorType = "NeedWaiting" // waiting for next reconcile + + // ErrorType for preflight + ErrorTypePreflightCommon = "PreflightCommon" + ErrorTypeSkipPreflight = "SkipPreflight" ) var ErrFailedToAddFinalizer = errors.New("failed to add finalizer") @@ -66,3 +88,44 @@ func IsTargetError(err error, errorType ErrorType) bool { } return false } + +// ToControllerError converts the error to the Controller error. +func ToControllerError(err error) *Error { + if tmpErr, ok := err.(*Error); ok || errors.As(err, &tmpErr) { + return tmpErr + } + return nil +} + +// NewNotFound returns a new Error with ErrorTypeNotFound. +func NewNotFound(format string, a ...any) *Error { + return &Error{ + Type: ErrorTypeNotFound, + Message: fmt.Sprintf(format, a...), + } +} + +// IsNotFound returns true if the specified error is the error type of ErrorTypeNotFound. +func IsNotFound(err error) bool { + return IsTargetError(err, ErrorTypeNotFound) +} + +// NewBackupNotSupported returns a new Error with ErrorTypeBackupNotSupported. +func NewBackupNotSupported(backupType, backupPolicyName string) *Error { + return NewErrorf(ErrorTypeBackupNotSupported, `backup type "%s" not supported by backup policy "%s"`, backupType, backupPolicyName) +} + +// NewBackupPVTemplateNotFound returns a new Error with ErrorTypeBackupPVTemplateNotFound. +func NewBackupPVTemplateNotFound(cmName, cmNamespace string) *Error { + return NewErrorf(ErrorTypeBackupPVTemplateNotFound, `"the persistentVolume template is empty in the configMap %s/%s", pvConfig.Namespace, pvConfig.Name`, cmNamespace, cmName) +} + +// NewBackupPVCNameIsEmpty returns a new Error with ErrorTypeBackupPVCNameIsEmpty. +func NewBackupPVCNameIsEmpty(backupPolicyName string) *Error { + return NewErrorf(ErrorTypeBackupPVCNameIsEmpty, `the persistentVolumeClaim name of this policy "%s" is empty`, backupPolicyName) +} + +// NewBackupJobFailed returns a new Error with ErrorTypeBackupJobFailed. +func NewBackupJobFailed(jobName string) *Error { + return NewErrorf(ErrorTypeBackupJobFailed, `backup job "%s" failed`, jobName) +} diff --git a/internal/controllerutil/errors_test.go b/internal/controllerutil/errors_test.go index 34fd04558..1a3dd0da3 100644 --- a/internal/controllerutil/errors_test.go +++ b/internal/controllerutil/errors_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil diff --git a/internal/controllerutil/pod_utils.go b/internal/controllerutil/pod_utils.go index a79c588e6..b9b1092c4 100644 --- a/internal/controllerutil/pod_utils.go +++ b/internal/controllerutil/pod_utils.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -53,10 +56,7 @@ func GetParentNameAndOrdinal(pod *corev1.Pod) (string, int) { return parent, ordinal } -// GetContainerByConfigSpec function description: -// Search the container using the configmap of config from the pod -// -// Return: The first container pointer of using configs +// GetContainerByConfigSpec searches for container using the configmap of config from the pod // // e.g.: // ClusterDefinition.configTemplateRef: @@ -83,12 +83,7 @@ func GetContainerByConfigSpec(podSpec *corev1.PodSpec, configs []appsv1alpha1.Co return nil } -// GetPodContainerWithVolumeMount function description: -// Search which containers mounting the volume -// -// Case: When the configmap update, we restart all containers who using configmap -// -// Return: all containers mount volumeName +// GetPodContainerWithVolumeMount searches for containers mounting the volume func GetPodContainerWithVolumeMount(podSpec *corev1.PodSpec, volumeName string) []*corev1.Container { containers := podSpec.Containers if len(containers) == 0 { @@ -97,12 +92,7 @@ func GetPodContainerWithVolumeMount(podSpec *corev1.PodSpec, volumeName string) return getContainerWithVolumeMount(containers, volumeName) } -// GetVolumeMountName function description: -// Find the volume of pod using name of cm -// -// Case: When the configmap object of configuration is modified by user, we need to query whose volumeName -// -// Return: The volume pointer of pod +// GetVolumeMountName finds the volume with mount name func GetVolumeMountName(volumes []corev1.Volume, resourceName string) *corev1.Volume { for i := range volumes { if volumes[i].ConfigMap != nil && volumes[i].ConfigMap.Name == resourceName { @@ -197,8 +187,7 @@ func GetVolumeMountByVolume(container *corev1.Container, volumeName string) *cor return nil } -// GetCoreNum function description: -// if not Resource field return 0 else Resources.Limits.cpu +// GetCoreNum gets content of Resources.Limits.cpu func GetCoreNum(container corev1.Container) int64 { limits := container.Resources.Limits if val, ok := (limits)[corev1.ResourceCPU]; ok { @@ -207,8 +196,7 @@ func GetCoreNum(container corev1.Container) int64 { return 0 } -// GetMemorySize function description: -// if not Resource field, return 0 else Resources.Limits.memory +// GetMemorySize gets content of Resources.Limits.memory func GetMemorySize(container corev1.Container) int64 { limits := container.Resources.Limits if val, ok := (limits)[corev1.ResourceMemory]; ok { @@ -217,7 +205,16 @@ func GetMemorySize(container corev1.Container) int64 { return 0 } -// PodIsReady check the pod is ready +// GetRequestMemorySize gets content of Resources.Limits.memory +func GetRequestMemorySize(container corev1.Container) int64 { + requests := container.Resources.Requests + if val, ok := (requests)[corev1.ResourceMemory]; ok { + return val.Value() + } + return 0 +} + +// PodIsReady checks if pod is ready func PodIsReady(pod *corev1.Pod) bool { if pod.Status.Conditions == nil { return false @@ -235,7 +232,7 @@ func PodIsReady(pod *corev1.Pod) bool { return false } -// GetContainerID find the containerID from pod by name +// GetContainerID gets the containerID from pod by name func GetContainerID(pod *corev1.Pod, containerName string) string { const containerSep = "//" @@ -313,12 +310,12 @@ func GetIntOrPercentValue(intOrStr *metautil.IntOrString) (int, bool, error) { } v, err := strconv.Atoi(s) if err != nil { - return 0, false, fmt.Errorf("failed to atoi. [%s], error: %v", intOrStr.StrVal, err) + return 0, false, fmt.Errorf("failed to atoi [%s], error: %v", intOrStr.StrVal, err) } return v, true, nil } -// GetPortByPortName find the Port from pod by name +// GetPortByPortName gets the Port from pod by name func GetPortByPortName(pod *corev1.Pod, portName string) (int32, error) { for _, container := range pod.Spec.Containers { for _, port := range container.Ports { @@ -338,7 +335,7 @@ func GetProbeHTTPPort(pod *corev1.Pod) (int32, error) { return GetPortByPortName(pod, constant.ProbeHTTPPortName) } -// GetProbeContainerName find the probe container from pod +// GetProbeContainerName gets the probe container from pod func GetProbeContainerName(pod *corev1.Pod) (string, error) { for _, container := range pod.Spec.Containers { if container.Name == constant.RoleProbeContainerName { @@ -349,8 +346,8 @@ func GetProbeContainerName(pod *corev1.Pod) (string, error) { } -// PodIsReadyWithLabel checks whether pod is ready or not if the component is ConsensusSet or ReplicationSet, -// it will be available when the pod is ready and labeled with its role. +// PodIsReadyWithLabel checks if pod is ready for ConsensusSet/ReplicationSet component, +// it will be available when the pod is ready and labeled with role. func PodIsReadyWithLabel(pod corev1.Pod) bool { if _, ok := pod.Labels[constant.RoleLabelKey]; !ok { return false @@ -364,7 +361,7 @@ func PodIsControlledByLatestRevision(pod *corev1.Pod, sts *appsv1.StatefulSet) b return GetPodRevision(pod) == sts.Status.UpdateRevision && sts.Status.ObservedGeneration == sts.Generation } -// GetPodRevision gets the revision of Pod by inspecting the StatefulSetRevisionLabel. If pod has no revision the empty +// GetPodRevision gets the revision of Pod by inspecting the StatefulSetRevisionLabel. If pod has no revision empty // string is returned. func GetPodRevision(pod *corev1.Pod) string { if pod.Labels == nil { @@ -376,17 +373,17 @@ func GetPodRevision(pod *corev1.Pod) string { // ByPodName sorts a list of jobs by pod name type ByPodName []corev1.Pod -// Len return the length of byPodName, for the sort.Sort +// Len returns the length of byPodName for sort.Sort func (c ByPodName) Len() int { return len(c) } -// Swap the items, for the sort.Sort +// Swap swaps the items for sort.Sort func (c ByPodName) Swap(i, j int) { c[i], c[j] = c[j], c[i] } -// Less define how to compare items, for the sort.Sort +// Less defines compare method for sort.Sort func (c ByPodName) Less(i, j int) bool { return c[i].Name < c[j].Name } diff --git a/internal/controllerutil/pod_utils_test.go b/internal/controllerutil/pod_utils_test.go index 568b9fbc1..cd18818c7 100644 --- a/internal/controllerutil/pod_utils_test.go +++ b/internal/controllerutil/pod_utils_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -273,17 +276,17 @@ var _ = Describe("pod utils", func() { // for test GetContainerByConfigSpec Context("GetContainerByConfigSpec test", func() { // found name: mysql3 - It("Should success with no error", func() { + It("Should succeed with no error", func() { podSpec := &statefulSet.Spec.Template.Spec Expect(GetContainerByConfigSpec(podSpec, configTemplates)).To(Equal(&podSpec.Containers[2])) }) // found name: init_mysql - It("Should success with no error", func() { + It("Should succeed with no error", func() { podSpec := &statefulSet.Spec.Template.Spec Expect(GetContainerByConfigSpec(podSpec, foundInitContainerConfigTemplates)).To(Equal(&podSpec.InitContainers[0])) }) // not found container - It("Should failed", func() { + It("Should fail", func() { podSpec := &statefulSet.Spec.Template.Spec Expect(GetContainerByConfigSpec(podSpec, notFoundConfigTemplates)).To(BeNil(), "get container is nil!") }) @@ -291,7 +294,7 @@ var _ = Describe("pod utils", func() { // for test GetVolumeMountName Context("GetPodContainerWithVolumeMount test", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { mountedContainers := GetPodContainerWithVolumeMount(&pod.Spec, "config1") Expect(len(mountedContainers)).To(Equal(2)) Expect(mountedContainers[0].Name).To(Equal("mysql")) @@ -303,7 +306,7 @@ var _ = Describe("pod utils", func() { Expect(mountedContainers[0].Name).To(Equal("mysql")) Expect(mountedContainers[1].Name).To(Equal("mysql3")) }) - It("Should failed", func() { + It("Should fail", func() { Expect(len(GetPodContainerWithVolumeMount(&pod.Spec, "not_exist_cm"))).To(Equal(0)) emptyPod := corev1.Pod{} @@ -316,14 +319,14 @@ var _ = Describe("pod utils", func() { // for test GetContainerWithVolumeMount Context("GetVolumeMountName test", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { volume := GetVolumeMountName(pod.Spec.Volumes, "stateful_test-config1") Expect(volume).NotTo(BeNil()) Expect(volume.Name).To(Equal("config1")) Expect(GetVolumeMountName(pod.Spec.Volumes, "stateful_test-config1")).To(Equal(&pod.Spec.Volumes[0])) }) - It("Should failed", func() { + It("Should fail", func() { Expect(GetVolumeMountName(pod.Spec.Volumes, "not_exist_resource")).To(BeNil()) }) }) @@ -389,7 +392,7 @@ var _ = Describe("pod utils", func() { }) Context("testGetContainerID", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { pods := []*corev1.Pod{{ Status: corev1.PodStatus{ ContainerStatuses: []corev1.ContainerStatus{ @@ -447,7 +450,7 @@ var _ = Describe("pod utils", func() { }) Context("common funcs test", func() { - It("GetContainersByConfigmap Should success with no error", func() { + It("GetContainersByConfigmap Should succeed with no error", func() { type args struct { containers []corev1.Container volumeName string @@ -495,7 +498,7 @@ var _ = Describe("pod utils", func() { }) - It("GetIntOrPercentValue Should success with no error", func() { + It("GetIntOrPercentValue Should succeed with no error", func() { fn := func(v metautil.IntOrString) *metautil.IntOrString { return &v } tests := []struct { name string @@ -532,7 +535,7 @@ var _ = Describe("pod utils", func() { }) }) Context("test sort by pod name", func() { - It("Should success with no error", func() { + It("Should succeed with no error", func() { pods := []corev1.Pod{{ ObjectMeta: metav1.ObjectMeta{Name: "pod-2"}, }, { diff --git a/internal/controllerutil/requeue_errors.go b/internal/controllerutil/requeue_errors.go new file mode 100644 index 000000000..a8b235d5a --- /dev/null +++ b/internal/controllerutil/requeue_errors.go @@ -0,0 +1,85 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package controllerutil + +import ( + "fmt" + "time" +) + +type RequeueError interface { + RequeueAfter() time.Duration + Reason() string +} + +type DelayedRequeueError interface { + RequeueError + Delayed() +} + +func NewRequeueError(after time.Duration, reason string) error { + return &requeueError{ + reason: reason, + requeueAfter: after, + } +} + +// NewDelayedRequeueError creates a delayed requeue error which only returns in the last step of the DAG. +func NewDelayedRequeueError(after time.Duration, reason string) error { + return &delayedRequeueError{ + requeueError{ + reason: reason, + requeueAfter: after, + }, + } +} + +func IsDelayedRequeueError(err error) bool { + if _, ok := err.(DelayedRequeueError); ok { + return true + } + return false +} + +type requeueError struct { + reason string + requeueAfter time.Duration +} + +type delayedRequeueError struct { + requeueError +} + +var _ RequeueError = &requeueError{} +var _ DelayedRequeueError = &delayedRequeueError{} + +func (r *requeueError) Error() string { + return fmt.Sprintf("requeue after: %v as: %s", r.requeueAfter, r.reason) +} + +func (r *requeueError) RequeueAfter() time.Duration { + return r.requeueAfter +} + +func (r *requeueError) Reason() string { + return r.reason +} + +func (r *delayedRequeueError) Delayed() {} diff --git a/internal/controllerutil/suite_test.go b/internal/controllerutil/suite_test.go index e6cd2410a..0f8dd32f5 100644 --- a/internal/controllerutil/suite_test.go +++ b/internal/controllerutil/suite_test.go @@ -1,26 +1,31 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil import ( "context" + "go/build" "path/filepath" "testing" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -71,7 +76,12 @@ var _ = BeforeSuite(func() { By("bootstrapping test environment") testEnv = &envtest.Environment{ - CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, + CRDDirectoryPaths: []string{ + filepath.Join("..", "..", "config", "crd", "bases"), + // use VolumeSnapshot v1beta1 API CRDs. + filepath.Join(build.Default.GOPATH, "pkg", "mod", "github.com", "kubernetes-csi/external-snapshotter/", + "client/v3@v3.0.0", "config", "crd"), + }, ErrorIfCRDPathMissing: true, } @@ -82,8 +92,12 @@ var _ = BeforeSuite(func() { Expect(cfg).NotTo(BeNil()) // +kubebuilder:scaffold:scheme + scheme := scheme.Scheme + + err = snapshotv1beta1.AddToScheme(scheme) + Expect(err).NotTo(HaveOccurred()) - k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) + k8sClient, err = client.New(cfg, client.Options{Scheme: scheme}) Expect(err).NotTo(HaveOccurred()) Expect(k8sClient).NotTo(BeNil()) }) diff --git a/internal/controllerutil/task.go b/internal/controllerutil/task.go index fdcc5373e..a9bdb47a0 100644 --- a/internal/controllerutil/task.go +++ b/internal/controllerutil/task.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -42,7 +45,7 @@ func NewTask() Task { return t } -// TaskFunction REVIEW (cx): using interface{} is rather error-prone +// TaskFunction REVIEW (cx): using interface{} is error-prone type TaskFunction func(RequestCtx, client.Client, interface{}) error func (t *Task) Exec(ctx RequestCtx, cli client.Client) error { diff --git a/internal/controllerutil/type.go b/internal/controllerutil/type.go index 02a999774..624f19992 100644 --- a/internal/controllerutil/type.go +++ b/internal/controllerutil/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -20,6 +23,7 @@ import ( "context" "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/client-go/tools/record" ctrl "sigs.k8s.io/controller-runtime" ) @@ -32,9 +36,31 @@ type RequestCtx struct { Recorder record.EventRecorder } -// UpdateCtxValue update Context value, return parent Context. +// Event is wrapper for Recorder.Event, if Recorder is nil, then it's no-op. +func (r *RequestCtx) Event(object runtime.Object, eventtype, reason, message string) { + if r == nil || r.Recorder == nil { + return + } + r.Recorder.Event(object, eventtype, reason, message) +} + +// Eventf is wrapper for Recorder.Eventf, if Recorder is nil, then it's no-op. +func (r *RequestCtx) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { + if r == nil || r.Recorder == nil { + return + } + r.Recorder.Eventf(object, eventtype, reason, messageFmt, args...) +} + +// UpdateCtxValue updates Context value, returns parent Context. func (r *RequestCtx) UpdateCtxValue(key, val any) context.Context { p := r.Ctx r.Ctx = context.WithValue(r.Ctx, key, val) return p } + +// WithValue returns a copy of parent in which the value associated with key is +// val. +func (r *RequestCtx) WithValue(key, val any) context.Context { + return context.WithValue(r.Ctx, key, val) +} diff --git a/internal/controllerutil/types_util.go b/internal/controllerutil/types_util.go index 50956dbb3..bd6d56d5c 100644 --- a/internal/controllerutil/types_util.go +++ b/internal/controllerutil/types_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil diff --git a/internal/controllerutil/volume_util.go b/internal/controllerutil/volume_util.go index 10605b43d..468f74edb 100644 --- a/internal/controllerutil/volume_util.go +++ b/internal/controllerutil/volume_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil @@ -78,7 +81,7 @@ func CreateOrUpdatePodVolumes(podSpec *corev1.PodSpec, volumes map[string]appsv1 }, func(volume *corev1.Volume) error { configMap := volume.ConfigMap if configMap == nil { - return fmt.Errorf("mount volume[%s] type require ConfigMap: [%+v]", volume.Name, volume) + return fmt.Errorf("mount volume[%s] requires a ConfigMap: [%+v]", volume.Name, volume) } configMap.Name = cmName return nil diff --git a/internal/controllerutil/volume_util_test.go b/internal/controllerutil/volume_util_test.go index 3c8605683..c653be22e 100644 --- a/internal/controllerutil/volume_util_test.go +++ b/internal/controllerutil/volume_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package controllerutil diff --git a/internal/controllerutil/volumesnapshot.go b/internal/controllerutil/volumesnapshot.go new file mode 100644 index 000000000..ef785af57 --- /dev/null +++ b/internal/controllerutil/volumesnapshot.go @@ -0,0 +1,170 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package controllerutil + +import ( + "context" + "encoding/json" + + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/client/v3/apis/volumesnapshot/v1beta1" + snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + "github.com/spf13/viper" + "sigs.k8s.io/controller-runtime/pkg/client" + + roclient "github.com/apecloud/kubeblocks/internal/controller/client" +) + +func InVolumeSnapshotV1Beta1() bool { + return viper.GetBool("VOLUMESNAPSHOT_API_BETA") +} + +// VolumeSnapshotCompatClient client is compatible with VolumeSnapshot v1 and v1beta1 +type VolumeSnapshotCompatClient struct { + client.Client + roclient.ReadonlyClient + Ctx context.Context +} + +func (c *VolumeSnapshotCompatClient) Create(snapshot *snapshotv1.VolumeSnapshot, opts ...client.CreateOption) error { + if InVolumeSnapshotV1Beta1() { + snapshotV1Beta1, err := convertV1ToV1beta1(snapshot) + if err != nil { + return err + } + return c.Client.Create(c.Ctx, snapshotV1Beta1, opts...) + } + return c.Client.Create(c.Ctx, snapshot, opts...) +} + +func (c *VolumeSnapshotCompatClient) Get(key client.ObjectKey, snapshot *snapshotv1.VolumeSnapshot, opts ...client.GetOption) error { + if c.ReadonlyClient == nil { + c.ReadonlyClient = c.Client + } + if InVolumeSnapshotV1Beta1() { + snapshotV1Beta1 := &snapshotv1beta1.VolumeSnapshot{} + err := c.ReadonlyClient.Get(c.Ctx, key, snapshotV1Beta1, opts...) + if err != nil { + return err + } + snap, err := convertV1Beta1ToV1(snapshotV1Beta1) + if err != nil { + return err + } + *snapshot = *snap + return nil + } + return c.ReadonlyClient.Get(c.Ctx, key, snapshot, opts...) +} + +func (c *VolumeSnapshotCompatClient) Delete(snapshot *snapshotv1.VolumeSnapshot, opts ...client.DeleteOption) error { + if InVolumeSnapshotV1Beta1() { + snapshotV1Beta1, err := convertV1ToV1beta1(snapshot) + if err != nil { + return err + } + return BackgroundDeleteObject(c.Client, c.Ctx, snapshotV1Beta1) + } + return BackgroundDeleteObject(c.Client, c.Ctx, snapshot) +} + +func (c *VolumeSnapshotCompatClient) Patch(snapshot *snapshotv1.VolumeSnapshot, deepCopy *snapshotv1.VolumeSnapshot, opts ...client.PatchOption) error { + if InVolumeSnapshotV1Beta1() { + snapshotV1Beta1, err := convertV1ToV1beta1(snapshot) + if err != nil { + return err + } + snapshotV1Beta1Patch, err := convertV1ToV1beta1(deepCopy) + if err != nil { + return err + } + patch := client.MergeFrom(snapshotV1Beta1Patch) + return c.Client.Patch(c.Ctx, snapshotV1Beta1, patch, opts...) + } + snapPatch := client.MergeFrom(deepCopy) + return c.Client.Patch(c.Ctx, snapshot, snapPatch, opts...) +} + +func (c *VolumeSnapshotCompatClient) List(snapshotList *snapshotv1.VolumeSnapshotList, opts ...client.ListOption) error { + if c.ReadonlyClient == nil { + c.ReadonlyClient = c.Client + } + if InVolumeSnapshotV1Beta1() { + snapshotV1Beta1List := &snapshotv1beta1.VolumeSnapshotList{} + err := c.ReadonlyClient.List(c.Ctx, snapshotV1Beta1List, opts...) + if err != nil { + return err + } + snaps, err := convertListV1Beta1ToV1(snapshotV1Beta1List) + if err != nil { + return err + } + *snapshotList = *snaps + return nil + } + return c.ReadonlyClient.List(c.Ctx, snapshotList, opts...) +} + +// CheckResourceExists checks whether resource exist or not. +func (c *VolumeSnapshotCompatClient) CheckResourceExists(key client.ObjectKey, obj *snapshotv1.VolumeSnapshot) (bool, error) { + if err := c.Get(key, obj); err != nil { + return false, client.IgnoreNotFound(err) + } + // if found, return true + return true, nil +} + +func convertV1ToV1beta1(snapshot *snapshotv1.VolumeSnapshot) (*snapshotv1beta1.VolumeSnapshot, error) { + v1beta1Snapshot := &snapshotv1beta1.VolumeSnapshot{} + snapshotBytes, err := json.Marshal(snapshot) + if err != nil { + return nil, err + } + if err := json.Unmarshal(snapshotBytes, v1beta1Snapshot); err != nil { + return nil, err + } + + return v1beta1Snapshot, nil +} + +func convertV1Beta1ToV1(snapshot *snapshotv1beta1.VolumeSnapshot) (*snapshotv1.VolumeSnapshot, error) { + v1Snapshot := &snapshotv1.VolumeSnapshot{} + snapshotBytes, err := json.Marshal(snapshot) + if err != nil { + return nil, err + } + if err := json.Unmarshal(snapshotBytes, v1Snapshot); err != nil { + return nil, err + } + + return v1Snapshot, nil +} + +func convertListV1Beta1ToV1(snapshots *snapshotv1beta1.VolumeSnapshotList) (*snapshotv1.VolumeSnapshotList, error) { + v1Snapshots := &snapshotv1.VolumeSnapshotList{} + snapshotBytes, err := json.Marshal(snapshots) + if err != nil { + return nil, err + } + if err := json.Unmarshal(snapshotBytes, v1Snapshots); err != nil { + return nil, err + } + + return v1Snapshots, nil +} diff --git a/internal/controllerutil/volumesnapshot_test.go b/internal/controllerutil/volumesnapshot_test.go new file mode 100644 index 000000000..d81c68555 --- /dev/null +++ b/internal/controllerutil/volumesnapshot_test.go @@ -0,0 +1,94 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package controllerutil + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + snapshotv1 "github.com/kubernetes-csi/external-snapshotter/client/v6/apis/volumesnapshot/v1" + "github.com/spf13/viper" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + rtclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +var _ = Describe("VolumeSnapshot compat client", func() { + const snapName = "test-volumesnapshot-name" + + var ( + pvcName = "test-pvc-name" + snapClassName = "test-vsc-name" + ) + + viper.SetDefault("VOLUMESNAPSHOT", "true") + viper.SetDefault("VOLUMESNAPSHOT_API_BETA", "true") + + It("test create/get/list/patch/delete", func() { + compatClient := VolumeSnapshotCompatClient{Client: k8sClient, Ctx: ctx} + snap := &snapshotv1.VolumeSnapshot{ + ObjectMeta: metav1.ObjectMeta{ + Name: snapName, + Namespace: "default", + }, + Spec: snapshotv1.VolumeSnapshotSpec{ + Source: snapshotv1.VolumeSnapshotSource{ + PersistentVolumeClaimName: &pvcName, + }, + }, + } + snapKey := rtclient.ObjectKeyFromObject(snap) + snapGet := &snapshotv1.VolumeSnapshot{} + + By("create volumesnapshot") + // check object not found + exists, err := compatClient.CheckResourceExists(snapKey, snapGet) + Expect(err).Should(BeNil()) + Expect(exists).Should(BeFalse()) + // create + Expect(compatClient.Create(snap)).Should(Succeed()) + // check object exists + exists, err = compatClient.CheckResourceExists(snapKey, snapGet) + Expect(err).Should(BeNil()) + Expect(exists).Should(BeTrue()) + + By("get volumesnapshot") + Expect(compatClient.Get(snapKey, snapGet)).Should(Succeed()) + Expect(snapKey.Name).Should(Equal(snapName)) + + By("list volumesnapshots") + snapList := &snapshotv1.VolumeSnapshotList{} + Expect(compatClient.List(snapList)).Should(Succeed()) + Expect(snapList.Items).ShouldNot(BeEmpty()) + + By("patch volumesnapshot") + snapPatch := snap.DeepCopy() + snap.Spec.VolumeSnapshotClassName = &snapClassName + Expect(compatClient.Patch(snap, snapPatch)).Should(Succeed()) + Expect(compatClient.Get(snapKey, snapGet)).Should(Succeed()) + Expect(*snapGet.Spec.VolumeSnapshotClassName).Should(Equal(snapClassName)) + + By("delete volumesnapshot") + Expect(compatClient.Delete(snap)).Should(Succeed()) + Eventually(func() error { + return compatClient.Get(snapKey, snapGet) + }).Should(Satisfy(apierrors.IsNotFound)) + }) +}) diff --git a/internal/generics/type.go b/internal/generics/type.go index 1095efa6f..9b90c176a 100644 --- a/internal/generics/type.go +++ b/internal/generics/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package generics @@ -64,6 +67,7 @@ var EndpointsSignature = func(_ corev1.Endpoints, _ corev1.EndpointsList) {} var StatefulSetSignature = func(_ appsv1.StatefulSet, _ appsv1.StatefulSetList) {} var DeploymentSignature = func(_ appsv1.Deployment, _ appsv1.DeploymentList) {} +var ReplicaSetSignature = func(_ appsv1.ReplicaSet, _ appsv1.ReplicaSetList) {} var JobSignature = func(_ batchv1.Job, _ batchv1.JobList) {} var CronJobSignature = func(_ batchv1.CronJob, _ batchv1.CronJobList) {} @@ -83,7 +87,7 @@ var OpsRequestSignature = func(_ appsv1alpha1.OpsRequest, _ appsv1alpha1.OpsRequ var ConfigConstraintSignature = func(_ appsv1alpha1.ConfigConstraint, _ appsv1alpha1.ConfigConstraintList) { } -var BackupPolicyTemplateSignature = func(_ dataprotectionv1alpha1.BackupPolicyTemplate, _ dataprotectionv1alpha1.BackupPolicyTemplateList) { +var BackupPolicyTemplateSignature = func(_ appsv1alpha1.BackupPolicyTemplate, _ appsv1alpha1.BackupPolicyTemplateList) { } var BackupPolicySignature = func(_ dataprotectionv1alpha1.BackupPolicy, _ dataprotectionv1alpha1.BackupPolicyList) { } @@ -95,6 +99,8 @@ var RestoreJobSignature = func(_ dataprotectionv1alpha1.RestoreJob, _ dataprotec } var AddonSignature = func(_ extensionsv1alpha1.Addon, _ extensionsv1alpha1.AddonList) { } +var ComponentResourceConstraintSignature = func(_ appsv1alpha1.ComponentResourceConstraint, _ appsv1alpha1.ComponentResourceConstraintList) {} +var ComponentClassDefinitionSignature = func(_ appsv1alpha1.ComponentClassDefinition, _ appsv1alpha1.ComponentClassDefinitionList) {} func ToGVK(object client.Object) schema.GroupVersionKind { t := reflect.TypeOf(object) diff --git a/internal/gotemplate/functional.go b/internal/gotemplate/functional.go index d0bc082f6..a3f1fa436 100644 --- a/internal/gotemplate/functional.go +++ b/internal/gotemplate/functional.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package gotemplate diff --git a/internal/gotemplate/suite_test.go b/internal/gotemplate/suite_test.go index 3542ce4bb..d5c7f3d8e 100644 --- a/internal/gotemplate/suite_test.go +++ b/internal/gotemplate/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package gotemplate diff --git a/internal/gotemplate/tpl_engine.go b/internal/gotemplate/tpl_engine.go index 23a068853..daa588a06 100644 --- a/internal/gotemplate/tpl_engine.go +++ b/internal/gotemplate/tpl_engine.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package gotemplate @@ -70,6 +73,10 @@ type TplEngine struct { ctx context.Context } +func (t *TplEngine) GetTplEngine() *template.Template { + return t.tpl +} + func (t *TplEngine) Render(context string) (string, error) { var buf strings.Builder tpl, err := t.tpl.Parse(context) @@ -83,12 +90,10 @@ func (t *TplEngine) Render(context string) (string, error) { } func (t *TplEngine) initSystemFunMap(funcs template.FuncMap) { - // Add the 'failed' function here. // When an error occurs, go template engine can detect it and exit early. funcs[buildInSystemFailedName] = failed - // Add the 'import' function here. - // Using it, you can make any function in a configmap object visible in this scope. + // With 'import', you can make a function in configmap visible in this scope. funcs[buildInSystemImportName] = func(namespacedName string) (string, error) { if t.importModules.InArray(namespacedName) { return "", nil @@ -111,7 +116,7 @@ func (t *TplEngine) initSystemFunMap(funcs template.FuncMap) { } for key, value := range cm.Data { if fn, ok := t.importFuncs[key]; ok { - return "", cfgcore.MakeError("failed to import function: %s, from %v, function is ready import: %v", + return "", cfgcore.MakeError("failed to import function: %s, from %v, function is already imported: %v", key, client.ObjectKey{ Namespace: fields[0], Name: fields[1], @@ -126,14 +131,13 @@ func (t *TplEngine) initSystemFunMap(funcs template.FuncMap) { return "", nil } - // Add the 'call' function here. - // This function simulates a function call in a programming language. + // Function 'call' simulates a function call in a programming language. // The parameter list of function is similar to function in bash language. // The access parameter uses .arg or $.arg. // e.g: $.arg0, $.arg1, ... // - // Node: How to do handle the return type of go template functions? - // It is recommended that you serialize it to string and cast it to a specific type in the calling function. + // Note: How to handle the return type of go template functions? + // It is recommended that serialize it to string and cast it to a specific type in the calling function. // e.g : // The return type of the function is map, the return value is $sts, // {{- $sts | toJson }} @@ -142,7 +146,7 @@ func (t *TplEngine) initSystemFunMap(funcs template.FuncMap) { funcs[buildInSystemCallName] = func(funcName string, args ...interface{}) (string, error) { fn, ok := t.importFuncs[funcName] if !ok { - return "", cfgcore.MakeError("not exist func: %s", funcName) + return "", cfgcore.MakeError("not existed func: %s", funcName) } values := ConstructFunctionArgList(args...) @@ -174,11 +178,7 @@ func (t *TplEngine) importSelfModuleFuncs(funcs map[string]functional, fn func(t } } -// NewTplEngine create go template helper -// To support this caller has a concept of import dependency which is recursive. -// -// As it recurses, it also sets the values to be appropriate for the parameters of the called function, -// it looks like it's calling a local function. +// NewTplEngine creates go template helper func NewTplEngine(values *TplValues, funcs *BuiltInObjectsFunc, tplName string, cli types2.ReadonlyClient, ctx context.Context) *TplEngine { coreBuiltinFuncs := sprig.TxtFuncMap() diff --git a/internal/gotemplate/tpl_engine_test.go b/internal/gotemplate/tpl_engine_test.go index a924ca300..522f5681e 100644 --- a/internal/gotemplate/tpl_engine_test.go +++ b/internal/gotemplate/tpl_engine_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package gotemplate @@ -56,16 +59,12 @@ var _ = Describe("tpl engine template", func() { f2 := Friend{Name: "test2"} pp := TplValues{ "UserName": "user@@test", - "Emails": []string{"test1@gmail.com", "test2@gmail.com"}, "Friends": []*Friend{&f1, &f2}, "MemorySize": 100, } tplString := `hello {{.UserName}}! cal test: {{ add ( div ( mul .MemorySize 88 ) 100 ) 6 7 }} -{{ range .Emails }} -an email {{ . }} -{{- end }} {{ with .Friends }} {{- range . }} my friend name is {{.Name}} @@ -75,9 +74,6 @@ my friend name is {{.Name}} expectString := `hello user@@test! cal test: 101 -an email test1@gmail.com -an email test2@gmail.com - my friend name is test1 my friend name is test2 ` @@ -210,7 +206,7 @@ mathAvg = [8-9][0-9]\.?\d*` engine.importFuncs["duplicate_fun2"] = functional{} _, err = engine.Render(`{{ import "xxx.yyy" }}`) Expect(err).ShouldNot(Succeed()) - Expect(err.Error()).Should(ContainSubstring("failed to import function: duplicate_fun2, from xxx/yyy, function is ready import")) + Expect(err.Error()).Should(ContainSubstring("failed to import function: duplicate_fun2, from xxx/yyy, function is already imported")) By("Error for not exist cm") mockCM = nil diff --git a/internal/preflight/analyze.go b/internal/preflight/analyze.go index f75ed39fc..d6bab2cd8 100644 --- a/internal/preflight/analyze.go +++ b/internal/preflight/analyze.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -25,6 +28,7 @@ import ( analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" "github.com/replicatedhq/troubleshoot/pkg/preflight" + "helm.sh/helm/v3/pkg/cli/values" preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" kbanalyzer "github.com/apecloud/kubeblocks/internal/preflight/analyzer" @@ -32,6 +36,7 @@ import ( type KBClusterCollectResult struct { preflight.ClusterCollectResult + HelmOptions *values.Options AnalyzerSpecs []*troubleshoot.Analyze KbAnalyzerSpecs []*preflightv1beta2.ExtendAnalyze } @@ -43,19 +48,19 @@ type KBHostCollectResult struct { } func (c KBClusterCollectResult) Analyze() []*analyze.AnalyzeResult { - return doAnalyze(c.Context, c.AllCollectedData, c.AnalyzerSpecs, c.KbAnalyzerSpecs, nil, nil) + return doAnalyze(c.Context, c.AllCollectedData, c.AnalyzerSpecs, c.KbAnalyzerSpecs, nil, nil, c.HelmOptions) } func (c KBHostCollectResult) Analyze() []*analyze.AnalyzeResult { - return doAnalyze(c.Context, c.AllCollectedData, nil, nil, c.AnalyzerSpecs, c.KbAnalyzerSpecs) + return doAnalyze(c.Context, c.AllCollectedData, nil, nil, c.AnalyzerSpecs, c.KbAnalyzerSpecs, nil) } -func doAnalyze(ctx context.Context, - allCollectedData map[string][]byte, +func doAnalyze(ctx context.Context, allCollectedData map[string][]byte, analyzers []*troubleshoot.Analyze, kbAnalyzers []*preflightv1beta2.ExtendAnalyze, hostAnalyzers []*troubleshoot.HostAnalyze, kbhHostAnalyzers []*preflightv1beta2.ExtendHostAnalyze, + options *values.Options, ) []*analyze.AnalyzeResult { getCollectedFileContents := func(fileName string) ([]byte, error) { contents, ok := allCollectedData[fileName] @@ -99,7 +104,7 @@ func doAnalyze(ctx context.Context, } } for _, kbAnalyzer := range kbAnalyzers { - analyzeResult := kbanalyzer.KBAnalyze(ctx, kbAnalyzer, getCollectedFileContents, getChildCollectedFileContents) + analyzeResult := kbanalyzer.KBAnalyze(ctx, kbAnalyzer, getCollectedFileContents, getChildCollectedFileContents, options) analyzeResults = append(analyzeResults, analyzeResult...) } for _, hostAnalyzer := range hostAnalyzers { diff --git a/internal/preflight/analyze_test.go b/internal/preflight/analyze_test.go index 4dda0200f..7ff35e53c 100644 --- a/internal/preflight/analyze_test.go +++ b/internal/preflight/analyze_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -49,7 +52,7 @@ var _ = Describe("analyze_test", func() { It("doAnalyze test, and expect success", func() { Eventually(func(g Gomega) { - analyzeList := doAnalyze(ctx, allCollectedData, analyzers, kbAnalyzers, hostAnalyzers, kbhHostAnalyzers) + analyzeList := doAnalyze(ctx, allCollectedData, analyzers, kbAnalyzers, hostAnalyzers, kbhHostAnalyzers, nil) g.Expect(len(analyzeList)).Should(Equal(4)) g.Expect(analyzeList[0].IsPass).Should(Equal(true)) g.Expect(analyzeList[1].IsFail).Should(Equal(true)) diff --git a/internal/preflight/analyzer/access.go b/internal/preflight/analyzer/access.go index c86704139..342d8be18 100644 --- a/internal/preflight/analyzer/access.go +++ b/internal/preflight/analyzer/access.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/access_test.go b/internal/preflight/analyzer/access_test.go index 4bec2d55b..ddbbbc8a2 100644 --- a/internal/preflight/analyzer/access_test.go +++ b/internal/preflight/analyzer/access_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/analyze_result.go b/internal/preflight/analyzer/analyze_result.go new file mode 100644 index 000000000..1d418c2e4 --- /dev/null +++ b/internal/preflight/analyzer/analyze_result.go @@ -0,0 +1,89 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +const ( + MissingOutcomeMessage = "there is a missing outcome message" + IncorrectOutcomeType = "there is an incorrect outcome type" + PassType = "Pass" + WarnType = "Warn" + FailType = "Fail" +) + +func newAnalyzeResult(title string, resultType string, outcomes []*troubleshoot.Outcome) *analyze.AnalyzeResult { + for _, outcome := range outcomes { + if outcome == nil { + continue + } + switch resultType { + case PassType: + if outcome.Pass != nil { + return newPassAnalyzeResult(title, outcome) + } + case WarnType: + if outcome.Warn != nil { + return newWarnAnalyzeResult(title, outcome) + } + case FailType: + if outcome.Fail != nil { + return newFailAnalyzeResult(title, outcome) + } + default: + return newFailedResultWithMessage(title, IncorrectOutcomeType) + } + } + return newFailedResultWithMessage(title, MissingOutcomeMessage) +} + +func newFailAnalyzeResult(title string, outcome *troubleshoot.Outcome) *analyze.AnalyzeResult { + return &analyze.AnalyzeResult{ + Title: title, + IsFail: true, + Message: outcome.Fail.Message, + URI: outcome.Fail.URI, + } +} + +func newWarnAnalyzeResult(title string, outcome *troubleshoot.Outcome) *analyze.AnalyzeResult { + return &analyze.AnalyzeResult{ + Title: title, + IsWarn: true, + Message: outcome.Warn.Message, + URI: outcome.Warn.URI, + } +} + +func newPassAnalyzeResult(title string, outcome *troubleshoot.Outcome) *analyze.AnalyzeResult { + return &analyze.AnalyzeResult{ + Title: title, + IsPass: true, + Message: outcome.Pass.Message, + URI: outcome.Pass.URI, + } +} + +func newFailedResultWithMessage(title, message string) *analyze.AnalyzeResult { + return newFailAnalyzeResult(title, &troubleshoot.Outcome{Fail: &troubleshoot.SingleOutcome{Message: message}}) +} diff --git a/internal/preflight/analyzer/analyze_result_test.go b/internal/preflight/analyzer/analyze_result_test.go new file mode 100644 index 000000000..4987cd56f --- /dev/null +++ b/internal/preflight/analyzer/analyze_result_test.go @@ -0,0 +1,81 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" +) + +var _ = Describe("kb_storage_class_test", func() { + var ( + outcomes []*troubleshoot.Outcome + ) + Context("analyze storage class test", func() { + BeforeEach(func() { + outcomes = []*troubleshoot.Outcome{ + { + Pass: &troubleshoot.SingleOutcome{ + + Message: "analyze storage class success", + }, + Fail: &troubleshoot.SingleOutcome{ + Message: "analyze storage class fail", + }, + Warn: &troubleshoot.SingleOutcome{ + Message: "warn message", + }, + }, + } + }) + It("AnalyzeResult test, and expected that fail is true", func() { + Eventually(func(g Gomega) { + res := newAnalyzeResult("test", FailType, outcomes) + g.Expect(res.IsFail).Should(BeTrue()) + g.Expect(res.Message).Should(Equal(outcomes[0].Fail.Message)) + }).Should(Succeed()) + }) + + It("AnalyzeResult test, and expected that warn is true", func() { + Eventually(func(g Gomega) { + res := newAnalyzeResult("test", WarnType, outcomes) + g.Expect(res.IsWarn).Should(BeTrue()) + g.Expect(res.Message).Should(Equal(outcomes[0].Warn.Message)) + }).Should(Succeed()) + }) + It("AnalyzeResult test, and expected that pass is true", func() { + Eventually(func(g Gomega) { + res := newAnalyzeResult("test", PassType, outcomes) + g.Expect(res.IsPass).Should(BeTrue()) + g.Expect(res.Message).Should(Equal(outcomes[0].Pass.Message)) + }).Should(Succeed()) + }) + It("AnalyzeResult with message test, and expected that fail is true", func() { + Eventually(func(g Gomega) { + message := "test" + res := newFailedResultWithMessage("test", message) + g.Expect(res.IsFail).Should(BeTrue()) + g.Expect(res.Message).Should(Equal(message)) + }).Should(Succeed()) + }) + }) +}) diff --git a/internal/preflight/analyzer/analyzer.go b/internal/preflight/analyzer/analyzer.go index df7a18e13..79b95e63d 100644 --- a/internal/preflight/analyzer/analyzer.go +++ b/internal/preflight/analyzer/analyzer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer @@ -22,6 +25,7 @@ import ( "github.com/pkg/errors" analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + "helm.sh/helm/v3/pkg/cli/values" preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" ) @@ -35,17 +39,21 @@ type KBAnalyzer interface { type GetCollectedFileContents func(string) ([]byte, error) type GetChildCollectedFileContents func(string, []string) (map[string][]byte, error) -func GetAnalyzer(analyzer *preflightv1beta2.ExtendAnalyze) (KBAnalyzer, bool) { +func GetAnalyzer(analyzer *preflightv1beta2.ExtendAnalyze, options *values.Options) (KBAnalyzer, bool) { switch { case analyzer.ClusterAccess != nil: return &AnalyzeClusterAccess{analyzer: analyzer.ClusterAccess}, true + case analyzer.StorageClass != nil: + return &AnalyzeStorageClassByKb{analyzer: analyzer.StorageClass}, true + case analyzer.Taint != nil: + return &AnalyzeTaintClassByKb{analyzer: analyzer.Taint, HelmOpts: options}, true default: return nil, false } } -func KBAnalyze(ctx context.Context, kbAnalyzer *preflightv1beta2.ExtendAnalyze, getFile func(string) ([]byte, error), findFiles func(string, []string) (map[string][]byte, error)) []*analyze.AnalyzeResult { - analyzer, ok := GetAnalyzer(kbAnalyzer) +func KBAnalyze(ctx context.Context, kbAnalyzer *preflightv1beta2.ExtendAnalyze, getFile func(string) ([]byte, error), findFiles func(string, []string) (map[string][]byte, error), options *values.Options) []*analyze.AnalyzeResult { + analyzer, ok := GetAnalyzer(kbAnalyzer, options) if !ok { return NewAnalyzeResultError(analyzer, errors.New("invalid analyzer")) } diff --git a/internal/preflight/analyzer/anzlyzer_test.go b/internal/preflight/analyzer/anzlyzer_test.go index 94a525037..ba747d3ea 100644 --- a/internal/preflight/analyzer/anzlyzer_test.go +++ b/internal/preflight/analyzer/anzlyzer_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer @@ -36,7 +39,7 @@ var _ = Describe("analyzer_test", func() { Context("KBAnalyze test", func() { It("KBAnalyze test, and ExtendAnalyze is nil", func() { Eventually(func(g Gomega) { - res := KBAnalyze(context.TODO(), &preflightv1beta2.ExtendAnalyze{}, nil, nil) + res := KBAnalyze(context.TODO(), &preflightv1beta2.ExtendAnalyze{}, nil, nil, nil) g.Expect(res[0].IsFail).Should(BeTrue()) }).Should(Succeed()) }) @@ -74,7 +77,7 @@ var _ = Describe("analyzer_test", func() { getCollectedFileContents := func(string) ([]byte, error) { return b, nil } - res := KBAnalyze(context.TODO(), kbAnalyzer, getCollectedFileContents, nil) + res := KBAnalyze(context.TODO(), kbAnalyzer, getCollectedFileContents, nil, nil) Expect(len(res)).Should(Equal(1)) g.Expect(res[0].IsPass).Should(BeTrue()) }).Should(Succeed()) @@ -122,10 +125,10 @@ var _ = Describe("analyzer_test", func() { It("GetAnalyzer test, and expect success", func() { Eventually(func(g Gomega) { - collector, ok := GetAnalyzer(&preflightv1beta2.ExtendAnalyze{ClusterAccess: &preflightv1beta2.ClusterAccessAnalyze{}}) + collector, ok := GetAnalyzer(&preflightv1beta2.ExtendAnalyze{ClusterAccess: &preflightv1beta2.ClusterAccessAnalyze{}}, nil) g.Expect(collector).ShouldNot(BeNil()) g.Expect(ok).Should(BeTrue()) - collector, ok = GetAnalyzer(&preflightv1beta2.ExtendAnalyze{}) + collector, ok = GetAnalyzer(&preflightv1beta2.ExtendAnalyze{}, nil) g.Expect(collector).Should(BeNil()) g.Expect(ok).Should(BeFalse()) }).Should(Succeed()) diff --git a/internal/preflight/analyzer/host_analyzer.go b/internal/preflight/analyzer/host_analyzer.go index ac23bfe04..a5fadf2c5 100644 --- a/internal/preflight/analyzer/host_analyzer.go +++ b/internal/preflight/analyzer/host_analyzer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/host_analyzer_test.go b/internal/preflight/analyzer/host_analyzer_test.go index f8efa9ace..a1504ef15 100644 --- a/internal/preflight/analyzer/host_analyzer_test.go +++ b/internal/preflight/analyzer/host_analyzer_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/host_region.go b/internal/preflight/analyzer/host_region.go index ef2c2a484..b2900e672 100644 --- a/internal/preflight/analyzer/host_region.go +++ b/internal/preflight/analyzer/host_region.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/host_region_test.go b/internal/preflight/analyzer/host_region_test.go index d9df42ff6..9feb9d97f 100644 --- a/internal/preflight/analyzer/host_region_test.go +++ b/internal/preflight/analyzer/host_region_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/host_utility.go b/internal/preflight/analyzer/host_utility.go index 9994249b8..a2894cb8c 100644 --- a/internal/preflight/analyzer/host_utility.go +++ b/internal/preflight/analyzer/host_utility.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/host_utility_test.go b/internal/preflight/analyzer/host_utility_test.go index 0c9f8bc26..d1f198ecb 100644 --- a/internal/preflight/analyzer/host_utility_test.go +++ b/internal/preflight/analyzer/host_utility_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/analyzer/kb_storage_class.go b/internal/preflight/analyzer/kb_storage_class.go new file mode 100644 index 000000000..19b9b7593 --- /dev/null +++ b/internal/preflight/analyzer/kb_storage_class.go @@ -0,0 +1,93 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + "encoding/json" + "fmt" + + analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + storagev1beta1 "k8s.io/api/storage/v1beta1" + "k8s.io/kubectl/pkg/util/storage" + + preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" + "github.com/apecloud/kubeblocks/internal/preflight/util" +) + +const ( + StorageClassPath = "cluster-resources/storage-classes.json" +) + +type AnalyzeStorageClassByKb struct { + analyzer *preflightv1beta2.KBStorageClassAnalyze +} + +func (a *AnalyzeStorageClassByKb) Title() string { + return util.TitleOrDefault(a.analyzer.AnalyzeMeta, "KubeBlocks Storage Class") +} + +func (a *AnalyzeStorageClassByKb) GetAnalyzer() *preflightv1beta2.KBStorageClassAnalyze { + return a.analyzer +} + +func (a *AnalyzeStorageClassByKb) IsExcluded() (bool, error) { + return util.IsExcluded(a.analyzer.Exclude) +} + +func (a *AnalyzeStorageClassByKb) Analyze(getFile GetCollectedFileContents, findFiles GetChildCollectedFileContents) ([]*analyze.AnalyzeResult, error) { + result, err := a.analyzeStorageClass(a.analyzer, getFile, findFiles) + if err != nil { + return []*analyze.AnalyzeResult{result}, err + } + result.Strict = a.analyzer.Strict.BoolOrDefaultFalse() + return []*analyze.AnalyzeResult{result}, nil +} + +func (a *AnalyzeStorageClassByKb) analyzeStorageClass(analyzer *preflightv1beta2.KBStorageClassAnalyze, getFile GetCollectedFileContents, findFiles GetChildCollectedFileContents) (*analyze.AnalyzeResult, error) { + storageClassesData, err := getFile(StorageClassPath) + if err != nil { + return newFailedResultWithMessage(a.Title(), fmt.Sprintf("get jsonfile failed, err:%v", err)), err + } + var storageClasses storagev1beta1.StorageClassList + if err = json.Unmarshal(storageClassesData, &storageClasses); err != nil { + return newFailedResultWithMessage(a.Title(), fmt.Sprintf("get jsonfile failed, err:%v", err)), err + } + + for _, storageClass := range storageClasses.Items { + // if storageClassType not set, check if default storageClass exists + if analyzer.StorageClassType == "" { + val := storageClass.Annotations[storage.IsDefaultStorageClassAnnotation] + if val == "true" { + return newAnalyzeResult(a.Title(), PassType, a.analyzer.Outcomes), nil + } + continue + } + + if storageClass.Parameters["type"] != analyzer.StorageClassType { + continue + } + if storageClass.Provisioner == "" || (storageClass.Provisioner == analyzer.Provisioner) { + return newAnalyzeResult(a.Title(), PassType, a.analyzer.Outcomes), nil + } + } + return newAnalyzeResult(a.Title(), WarnType, a.analyzer.Outcomes), nil +} + +var _ KBAnalyzer = &AnalyzeStorageClassByKb{} diff --git a/internal/preflight/analyzer/kb_storage_class_test.go b/internal/preflight/analyzer/kb_storage_class_test.go new file mode 100644 index 000000000..c8e449097 --- /dev/null +++ b/internal/preflight/analyzer/kb_storage_class_test.go @@ -0,0 +1,148 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + "encoding/json" + "errors" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kubectl/pkg/util/storage" + + troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + storagev1beta1 "k8s.io/api/storage/v1beta1" + + preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" +) + +var ( + clusterResources = storagev1beta1.StorageClassList{ + Items: []storagev1beta1.StorageClass{ + { + Provisioner: "ebs.csi.aws.com", + Parameters: map[string]string{"type": "gp3"}, + }, + }, + } + clusterResources2 = storagev1beta1.StorageClassList{ + Items: []storagev1beta1.StorageClass{ + { + Provisioner: "ebs.csi.aws.com", + Parameters: map[string]string{"type": "gp3"}, + ObjectMeta: metav1.ObjectMeta{Annotations: map[string]string{storage.IsDefaultStorageClassAnnotation: "true"}}, + }, + }, + } +) + +var _ = Describe("kb_storage_class_test", func() { + var ( + analyzer AnalyzeStorageClassByKb + ) + Context("analyze storage class test", func() { + BeforeEach(func() { + analyzer = AnalyzeStorageClassByKb{ + analyzer: &preflightv1beta2.KBStorageClassAnalyze{ + Provisioner: "ebs.csi.aws.com", + StorageClassType: "gp3", + Outcomes: []*troubleshoot.Outcome{ + { + Pass: &troubleshoot.SingleOutcome{ + + Message: "analyze storage class success", + }, + Fail: &troubleshoot.SingleOutcome{ + Message: "analyze storage class fail", + }, + }, + }}} + }) + It("Analyze test, and get file failed", func() { + Eventually(func(g Gomega) { + getCollectedFileContents := func(filename string) ([]byte, error) { + return nil, errors.New("get file failed") + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(res[0].IsFail).Should(BeTrue()) + g.Expect(res[0].IsPass).Should(BeFalse()) + }).Should(Succeed()) + }) + + It("Analyze test, and return of get file is not clusterResource", func() { + Eventually(func(g Gomega) { + getCollectedFileContents := func(filename string) ([]byte, error) { + return []byte("test"), nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(res[0].IsFail).Should(BeTrue()) + g.Expect(res[0].IsPass).Should(BeFalse()) + }).Should(Succeed()) + }) + + It("Analyze test, and analyzer result is expected that pass is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(clusterResources) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeTrue()) + g.Expect(res[0].IsFail).Should(BeFalse()) + }).Should(Succeed()) + }) + It("Analyze test, and analyzer result is expected that fail is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + clusterResources.Items[0].Provisioner = "apecloud" + b, err := json.Marshal(clusterResources) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeFalse()) + g.Expect(res[0].IsFail).Should(BeTrue()) + }).Should(Succeed()) + }) + It("Analyze test, and analyzer result is expected that fail is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(clusterResources2) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + analyzer.analyzer.StorageClassType = "" + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeTrue()) + g.Expect(res[0].IsWarn).Should(BeFalse()) + }).Should(Succeed()) + }) + }) +}) diff --git a/internal/preflight/analyzer/kb_taint.go b/internal/preflight/analyzer/kb_taint.go new file mode 100644 index 000000000..b61b61367 --- /dev/null +++ b/internal/preflight/analyzer/kb_taint.go @@ -0,0 +1,191 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + "encoding/json" + "fmt" + "strings" + + "helm.sh/helm/v3/pkg/cli" + "helm.sh/helm/v3/pkg/getter" + + analyze "github.com/replicatedhq/troubleshoot/pkg/analyze" + "helm.sh/helm/v3/pkg/cli/values" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/runtime" + + preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" + "github.com/apecloud/kubeblocks/internal/preflight/util" +) + +const ( + NodesPath = "cluster-resources/nodes.json" + Tolerations = "tolerations" + KubeBlocks = "kubeblocks" +) + +type AnalyzeTaintClassByKb struct { + analyzer *preflightv1beta2.KBTaintAnalyze + HelmOpts *values.Options +} + +func (a *AnalyzeTaintClassByKb) Title() string { + return util.TitleOrDefault(a.analyzer.AnalyzeMeta, "KubeBlocks Taints") +} + +func (a *AnalyzeTaintClassByKb) GetAnalyzer() *preflightv1beta2.KBTaintAnalyze { + return a.analyzer +} + +func (a *AnalyzeTaintClassByKb) IsExcluded() (bool, error) { + return util.IsExcluded(a.analyzer.Exclude) +} + +func (a *AnalyzeTaintClassByKb) Analyze(getFile GetCollectedFileContents, findFiles GetChildCollectedFileContents) ([]*analyze.AnalyzeResult, error) { + result, err := a.analyzeTaint(getFile, findFiles) + if err != nil { + return []*analyze.AnalyzeResult{result}, err + } + result.Strict = a.analyzer.Strict.BoolOrDefaultFalse() + return []*analyze.AnalyzeResult{result}, nil +} + +func (a *AnalyzeTaintClassByKb) analyzeTaint(getFile GetCollectedFileContents, findFiles GetChildCollectedFileContents) (*analyze.AnalyzeResult, error) { + nodesData, err := getFile(NodesPath) + if err != nil { + return newFailedResultWithMessage(a.Title(), fmt.Sprintf("get jsonfile failed, err:%v", err)), err + } + var nodes v1.NodeList + if err = json.Unmarshal(nodesData, &nodes); err != nil { + return newFailedResultWithMessage(a.Title(), fmt.Sprintf("get jsonfile failed, err:%v", err)), err + } + err = a.generateTolerations() + if err != nil { + return newFailedResultWithMessage(a.Title(), fmt.Sprintf("get tolerations failed, err:%v", err)), err + } + return a.doAnalyzeTaint(nodes) +} + +func (a *AnalyzeTaintClassByKb) doAnalyzeTaint(nodes v1.NodeList) (*analyze.AnalyzeResult, error) { + taintFailResult := []string{} + for _, node := range nodes.Items { + if node.Spec.Taints == nil || len(node.Spec.Taints) == 0 { + return newAnalyzeResult(a.Title(), PassType, a.analyzer.Outcomes), nil + } + } + + if a.analyzer.TolerationsMap == nil || len(a.analyzer.TolerationsMap) == 0 { + return newAnalyzeResult(a.Title(), FailType, a.analyzer.Outcomes), nil + } + + for k, tolerations := range a.analyzer.TolerationsMap { + count := 0 + for _, node := range nodes.Items { + if isTolerableTaints(node.Spec.Taints, tolerations) { + count++ + } + } + if count <= 0 { + taintFailResult = append(taintFailResult, k) + } + } + if len(taintFailResult) > 0 { + result := newAnalyzeResult(a.Title(), FailType, a.analyzer.Outcomes) + result.Message += fmt.Sprintf(" Taint check failed components: %s", strings.Join(taintFailResult, ", ")) + return result, nil + } + return newAnalyzeResult(a.Title(), PassType, a.analyzer.Outcomes), nil +} + +func (a *AnalyzeTaintClassByKb) generateTolerations() error { + tolerations := map[string][]v1.Toleration{} + if a.HelmOpts != nil { + optsMap, err := a.getHelmValues() + if err != nil { + return err + } + getTolerationsMap(optsMap, "", tolerations) + } + a.analyzer.TolerationsMap = tolerations + return nil +} + +func (a *AnalyzeTaintClassByKb) getHelmValues() (map[string]interface{}, error) { + settings := cli.New() + p := getter.All(settings) + vals, err := a.HelmOpts.MergeValues(p) + if err != nil { + return nil, err + } + return vals, nil +} + +func getTolerationsMap(tolerationData map[string]interface{}, addonName string, tolerationsMap map[string][]v1.Toleration) { + var tmpTolerationList []v1.Toleration + var tmpToleration v1.Toleration + + for k, v := range tolerationData { + if k == Tolerations { + tolerationList := v.([]interface{}) + tmpTolerationList = []v1.Toleration{} + for _, t := range tolerationList { + toleration := t.(map[string]interface{}) + if err := runtime.DefaultUnstructuredConverter.FromUnstructured(toleration, &tmpToleration); err != nil { + continue + } + tmpTolerationList = append(tmpTolerationList, tmpToleration) + } + if addonName == "" { + addonName = KubeBlocks + } + tolerationsMap[addonName] = tmpTolerationList + continue + } + + switch v := v.(type) { + case map[string]interface{}: + if addonName != "" { + addonName += "." + } + addonName += k + getTolerationsMap(v, addonName, tolerationsMap) + default: + continue + } + } +} + +func isTolerableTaints(taints []v1.Taint, tolerations []v1.Toleration) bool { + tolerableCount := 0 + for _, taint := range taints { + // check only on taints that have effect NoSchedule + if taint.Effect != v1.TaintEffectNoSchedule { + continue + } + for _, toleration := range tolerations { + if toleration.ToleratesTaint(&taint) { + tolerableCount++ + break + } + } + } + return tolerableCount >= len(taints) +} diff --git a/internal/preflight/analyzer/kb_taint_test.go b/internal/preflight/analyzer/kb_taint_test.go new file mode 100644 index 000000000..31f21624b --- /dev/null +++ b/internal/preflight/analyzer/kb_taint_test.go @@ -0,0 +1,168 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package analyzer + +import ( + "encoding/json" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/pkg/errors" + troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" + "helm.sh/helm/v3/pkg/cli/values" + v1 "k8s.io/api/core/v1" + + preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" +) + +var ( + nodeList1 = v1.NodeList{Items: []v1.Node{ + {Spec: v1.NodeSpec{Taints: []v1.Taint{ + {Key: "dev", Value: "true", Effect: v1.TaintEffectNoSchedule}, + {Key: "large", Value: "true", Effect: v1.TaintEffectNoSchedule}, + }}}, + {Spec: v1.NodeSpec{Taints: []v1.Taint{ + {Key: "dev", Value: "false", Effect: v1.TaintEffectNoSchedule}, + }}}, + }} + nodeList2 = v1.NodeList{Items: []v1.Node{ + {Spec: v1.NodeSpec{Taints: []v1.Taint{ + {Key: "dev", Value: "false", Effect: v1.TaintEffectNoSchedule}, + {Key: "large", Value: "true", Effect: v1.TaintEffectNoSchedule}, + }}}, + }} + nodeList3 = v1.NodeList{Items: []v1.Node{ + {Spec: v1.NodeSpec{}}, + }} +) + +var _ = Describe("taint_class_test", func() { + var ( + analyzer AnalyzeTaintClassByKb + ) + Context("analyze taint test", func() { + BeforeEach(func() { + JSONStr := "tolerations=[ { \"key\": \"dev\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" }, " + + "{ \"key\": \"large\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" } ]," + + "prometheus.server.tolerations=[ { \"key\": \"dev\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" }, " + + "{ \"key\": \"large\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" } ]," + + "grafana.tolerations=[ { \"key\": \"dev\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" }, " + + "{ \"key\": \"large\", \"operator\": \"Equal\", \"effect\": \"NoSchedule\", \"value\": \"true\" } ]," + analyzer = AnalyzeTaintClassByKb{ + analyzer: &preflightv1beta2.KBTaintAnalyze{ + Outcomes: []*troubleshoot.Outcome{ + { + Pass: &troubleshoot.SingleOutcome{ + Message: "analyze taint success", + }, + Fail: &troubleshoot.SingleOutcome{ + Message: "analyze taint fail", + }, + }, + }, + }, + HelmOpts: &values.Options{JSONValues: []string{JSONStr}}, + } + + }) + It("Analyze test, and get file failed", func() { + Eventually(func(g Gomega) { + getCollectedFileContents := func(filename string) ([]byte, error) { + return nil, errors.New("get file failed") + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(res[0].IsFail).Should(BeTrue()) + g.Expect(res[0].IsPass).Should(BeFalse()) + }).Should(Succeed()) + }) + + It("Analyze test, and return of get file is not clusterResource", func() { + Eventually(func(g Gomega) { + getCollectedFileContents := func(filename string) ([]byte, error) { + return []byte("test"), nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).To(HaveOccurred()) + g.Expect(res[0].IsFail).Should(BeTrue()) + g.Expect(res[0].IsPass).Should(BeFalse()) + }).Should(Succeed()) + }) + + It("Analyze test, and analyzer result is expected that pass is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(nodeList1) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeTrue()) + g.Expect(res[0].IsFail).Should(BeFalse()) + }).Should(Succeed()) + }) + It("Analyze test, and analyzer result is expected that fail is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(nodeList2) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeFalse()) + g.Expect(res[0].IsFail).Should(BeTrue()) + }).Should(Succeed()) + }) + It("Analyze test, the taints are nil, and analyzer result is expected that pass is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(nodeList3) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeTrue()) + g.Expect(res[0].IsFail).Should(BeFalse()) + }).Should(Succeed()) + }) + It("Analyze test, the tolerations are nil, and analyzer result is expected that fail is true", func() { + Eventually(func(g Gomega) { + g.Expect(analyzer.IsExcluded()).Should(BeFalse()) + b, err := json.Marshal(nodeList2) + g.Expect(err).NotTo(HaveOccurred()) + getCollectedFileContents := func(filename string) ([]byte, error) { + return b, nil + } + analyzer.HelmOpts = nil + res, err := analyzer.Analyze(getCollectedFileContents, nil) + g.Expect(err).NotTo(HaveOccurred()) + g.Expect(res[0].IsPass).Should(BeFalse()) + g.Expect(res[0].IsFail).Should(BeTrue()) + }).Should(Succeed()) + }) + }) +}) diff --git a/internal/preflight/analyzer/suite_test.go b/internal/preflight/analyzer/suite_test.go index 28d34aa38..f509de773 100644 --- a/internal/preflight/analyzer/suite_test.go +++ b/internal/preflight/analyzer/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package analyzer diff --git a/internal/preflight/collect.go b/internal/preflight/collect.go index 734aac1a3..ace90c338 100644 --- a/internal/preflight/collect.go +++ b/internal/preflight/collect.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -25,27 +28,26 @@ import ( "github.com/pkg/errors" troubleshoot "github.com/replicatedhq/troubleshoot/pkg/apis/troubleshoot/v1beta2" pkgcollector "github.com/replicatedhq/troubleshoot/pkg/collect" - "github.com/replicatedhq/troubleshoot/pkg/constants" - "github.com/replicatedhq/troubleshoot/pkg/k8sutil" "github.com/replicatedhq/troubleshoot/pkg/logger" "github.com/replicatedhq/troubleshoot/pkg/preflight" "github.com/spf13/viper" + "helm.sh/helm/v3/pkg/cli/values" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" - "k8s.io/client-go/kubernetes" + cmdutil "k8s.io/kubectl/pkg/cmd/util" preflightv1beta2 "github.com/apecloud/kubeblocks/externalapis/preflight/v1beta2" kbcollector "github.com/apecloud/kubeblocks/internal/preflight/collector" ) -func CollectPreflight(ctx context.Context, kbPreflight *preflightv1beta2.Preflight, kbHostPreflight *preflightv1beta2.HostPreflight, progressCh chan interface{}) ([]preflight.CollectResult, error) { +func CollectPreflight(f cmdutil.Factory, helmOpts *values.Options, ctx context.Context, kbPreflight *preflightv1beta2.Preflight, kbHostPreflight *preflightv1beta2.HostPreflight, progressCh chan interface{}) ([]preflight.CollectResult, error) { var ( collectResults []preflight.CollectResult err error ) // deal with preflight if kbPreflight != nil && (len(kbPreflight.Spec.ExtendCollectors) > 0 || len(kbPreflight.Spec.Collectors) > 0) { - res, err := CollectClusterData(ctx, kbPreflight, progressCh) + res, err := CollectClusterData(ctx, kbPreflight, f, helmOpts, progressCh) if err != nil { return collectResults, errors.Wrap(err, "failed to collect data in cluster") } @@ -61,7 +63,7 @@ func CollectPreflight(ctx context.Context, kbPreflight *preflightv1beta2.Preflig collectResults = append(collectResults, *res) } if len(kbHostPreflight.Spec.RemoteCollectors) > 0 { - res, err := CollectRemoteData(ctx, kbHostPreflight, progressCh) + res, err := CollectRemoteData(ctx, kbHostPreflight, f, progressCh) if err != nil { return collectResults, errors.Wrap(err, "failed to collect data remotely") } @@ -126,19 +128,23 @@ func CollectHost(ctx context.Context, opts preflight.CollectOpts, collectors []p } // CollectClusterData transforms the specs of Preflight to Collector, and sets the collectOpts, such as restConfig, Namespace, and ProgressChan -func CollectClusterData(ctx context.Context, kbPreflight *preflightv1beta2.Preflight, progressCh chan interface{}) (*preflight.CollectResult, error) { +func CollectClusterData(ctx context.Context, kbPreflight *preflightv1beta2.Preflight, f cmdutil.Factory, helmOpts *values.Options, progressCh chan interface{}) (*preflight.CollectResult, error) { + var err error v := viper.GetViper() - restConfig, err := k8sutil.GetRESTConfig() - if err != nil { - return nil, errors.Wrap(err, "failed to convert kube flags to rest config") - } - collectOpts := preflight.CollectOpts{ Namespace: v.GetString("namespace"), IgnorePermissionErrors: v.GetBool("collect-without-permissions"), ProgressChan: progressCh, - KubernetesRestConfig: restConfig, + } + + if collectOpts.KubernetesRestConfig, err = f.ToRESTConfig(); err != nil { + return nil, errors.Wrap(err, "failed to instantiate Kubernetes restconfig") + } + + k8sClient, err := f.KubernetesClientSet() + if err != nil { + return nil, errors.Wrap(err, "failed to instantiate Kubernetes client") } if v.GetString("since") != "" || v.GetString("since-time") != "" { @@ -159,14 +165,6 @@ func CollectClusterData(ctx context.Context, kbPreflight *preflightv1beta2.Prefl collectSpecs = pkgcollector.DedupCollectors(collectSpecs) collectSpecs = pkgcollector.EnsureClusterResourcesFirst(collectSpecs) - collectOpts.KubernetesRestConfig.QPS = constants.DEFAULT_CLIENT_QPS - collectOpts.KubernetesRestConfig.Burst = constants.DEFAULT_CLIENT_BURST - // collectOpts.KubernetesRestConfig.UserAgent = fmt.Sprintf("%s/%s", constants.DEFAULT_CLIENT_USER_AGENT, version.Version()) - - k8sClient, err := kubernetes.NewForConfig(collectOpts.KubernetesRestConfig) - if err != nil { - return nil, errors.Wrap(err, "failed to instantiate Kubernetes client") - } var collectors []pkgcollector.Collector allCollectorsMap := make(map[reflect.Type][]pkgcollector.Collector) for _, collectSpec := range collectSpecs { @@ -185,12 +183,18 @@ func CollectClusterData(ctx context.Context, kbPreflight *preflightv1beta2.Prefl // // todo user defined cluster collector // } - collectResults, err := CollectCluster(ctx, collectOpts, collectors, allCollectorsMap, kbPreflight) + collectResults, err := CollectCluster(ctx, collectOpts, collectors, allCollectorsMap, kbPreflight, helmOpts) return &collectResults, err } -// CollectCluster collects ciuster data against by Collector,and returns the collected data which is encapsulated in CollectResult struct -func CollectCluster(ctx context.Context, opts preflight.CollectOpts, allCollectors []pkgcollector.Collector, allCollectorsMap map[reflect.Type][]pkgcollector.Collector, kbPreflight *preflightv1beta2.Preflight) (preflight.CollectResult, error) { +// CollectCluster collects cluster data against by Collector,and returns the collected data which is encapsulated in CollectResult struct +func CollectCluster(ctx context.Context, + opts preflight.CollectOpts, + allCollectors []pkgcollector.Collector, + allCollectorsMap map[reflect.Type][]pkgcollector.Collector, + kbPreflight *preflightv1beta2.Preflight, + helmOpts *values.Options, +) (preflight.CollectResult, error) { var foundForbidden bool allCollectedData := make(map[string][]byte) collectorList := map[string]preflight.CollectorStatus{} @@ -226,6 +230,7 @@ func CollectCluster(ctx context.Context, opts preflight.CollectOpts, allCollecto }, AnalyzerSpecs: kbPreflight.Spec.Analyzers, KbAnalyzerSpecs: kbPreflight.Spec.ExtendAnalyzers, + HelmOptions: helmOpts, } if foundForbidden && !opts.IgnorePermissionErrors { @@ -299,14 +304,13 @@ func CollectCluster(ctx context.Context, opts preflight.CollectOpts, allCollecto } collectResult.AllCollectedData = allCollectedData - return collectResult, nil } -func CollectRemoteData(ctx context.Context, preflightSpec *preflightv1beta2.HostPreflight, progressCh chan interface{}) (*preflight.CollectResult, error) { +func CollectRemoteData(ctx context.Context, preflightSpec *preflightv1beta2.HostPreflight, f cmdutil.Factory, progressCh chan interface{}) (*preflight.CollectResult, error) { v := viper.GetViper() - restConfig, err := k8sutil.GetRESTConfig() + restConfig, err := f.ToRESTConfig() if err != nil { return nil, errors.Wrap(err, "failed to convert kube flags to rest config") } diff --git a/internal/preflight/collect_test.go b/internal/preflight/collect_test.go index cd1ac9717..d277d47cd 100644 --- a/internal/preflight/collect_test.go +++ b/internal/preflight/collect_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -97,7 +100,7 @@ var _ = Describe("collect_test", func() { g.Expect(<-progressCh).NotTo(BeNil()) } }() - results, err := CollectPreflight(context.TODO(), preflight, hostPreflight, progressCh) + results, err := CollectPreflight(tf, nil, context.TODO(), preflight, hostPreflight, progressCh) g.Expect(err).NotTo(HaveOccurred()) g.Expect(len(results)).Should(BeNumerically(">=", 3)) }).WithTimeout(timeOut).Should(Succeed()) @@ -126,7 +129,7 @@ var _ = Describe("collect_test", func() { g.Expect(<-progressCh).NotTo(BeNil()) } }() - collectResult, err := CollectRemoteData(context.TODO(), &preflightv1beta2.HostPreflight{}, progressCh) + collectResult, err := CollectRemoteData(context.TODO(), &preflightv1beta2.HostPreflight{}, tf, progressCh) g.Expect(err).NotTo(HaveOccurred()) g.Expect(collectResult).NotTo(BeNil()) }).WithTimeout(timeOut).Should(Succeed()) diff --git a/internal/preflight/collector/host_collector.go b/internal/preflight/collector/host_collector.go index bb317f55c..912a4e219 100644 --- a/internal/preflight/collector/host_collector.go +++ b/internal/preflight/collector/host_collector.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/host_collector_test.go b/internal/preflight/collector/host_collector_test.go index 6e9303bb0..bab32bbeb 100644 --- a/internal/preflight/collector/host_collector_test.go +++ b/internal/preflight/collector/host_collector_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/host_region.go b/internal/preflight/collector/host_region.go index fed0ddb62..37e96bce0 100644 --- a/internal/preflight/collector/host_region.go +++ b/internal/preflight/collector/host_region.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/host_region_test.go b/internal/preflight/collector/host_region_test.go index ffafc2a18..4644c859f 100644 --- a/internal/preflight/collector/host_region_test.go +++ b/internal/preflight/collector/host_region_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/host_utility.go b/internal/preflight/collector/host_utility.go index bd99f029d..006ec8388 100644 --- a/internal/preflight/collector/host_utility.go +++ b/internal/preflight/collector/host_utility.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/host_utility_test.go b/internal/preflight/collector/host_utility_test.go index bdbd44575..187848d92 100644 --- a/internal/preflight/collector/host_utility_test.go +++ b/internal/preflight/collector/host_utility_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/collector/suite_test.go b/internal/preflight/collector/suite_test.go index 0722717ca..04411e27c 100644 --- a/internal/preflight/collector/suite_test.go +++ b/internal/preflight/collector/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package collector diff --git a/internal/preflight/concat_spec.go b/internal/preflight/concat_spec.go index 06696079f..a7198a5f7 100644 --- a/internal/preflight/concat_spec.go +++ b/internal/preflight/concat_spec.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight diff --git a/internal/preflight/concat_spec_test.go b/internal/preflight/concat_spec_test.go index 9e865f49d..8ef81b67b 100644 --- a/internal/preflight/concat_spec_test.go +++ b/internal/preflight/concat_spec_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight diff --git a/internal/preflight/interactive/interactive.go b/internal/preflight/interactive/interactive.go index 31a718e0a..8533962cc 100644 --- a/internal/preflight/interactive/interactive.go +++ b/internal/preflight/interactive/interactive.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package interactive @@ -35,7 +38,7 @@ var ( isShowingSaved = false ) -// ShowInteractiveResults displays the results with interactive mode +// ShowInteractiveResults displays the results in interactive mode func ShowInteractiveResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, outputPath string) error { if err := ui.Init(); err != nil { return errors.Wrap(err, "failed to create terminal ui") diff --git a/internal/preflight/load_spec.go b/internal/preflight/load_spec.go index 82b4637cb..9385022fd 100644 --- a/internal/preflight/load_spec.go +++ b/internal/preflight/load_spec.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight diff --git a/internal/preflight/load_spec_test.go b/internal/preflight/load_spec_test.go index a8c8ebb9f..38dee1072 100644 --- a/internal/preflight/load_spec_test.go +++ b/internal/preflight/load_spec_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight diff --git a/internal/preflight/suite_test.go b/internal/preflight/suite_test.go index 452b1c889..1db0fa069 100644 --- a/internal/preflight/suite_test.go +++ b/internal/preflight/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight diff --git a/internal/preflight/testing/fake.go b/internal/preflight/testing/fake.go index f74af46cc..c64793822 100644 --- a/internal/preflight/testing/fake.go +++ b/internal/preflight/testing/fake.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/preflight/testing/fake_test.go b/internal/preflight/testing/fake_test.go index 488b89542..7c1f19802 100644 --- a/internal/preflight/testing/fake_test.go +++ b/internal/preflight/testing/fake_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/preflight/testing/suite_test.go b/internal/preflight/testing/suite_test.go index ba4c7f25e..760ab9b22 100644 --- a/internal/preflight/testing/suite_test.go +++ b/internal/preflight/testing/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testing diff --git a/internal/preflight/text_results.go b/internal/preflight/text_results.go index b4a4e95f2..410268d45 100644 --- a/internal/preflight/text_results.go +++ b/internal/preflight/text_results.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -19,11 +22,20 @@ package preflight import ( "encoding/json" "fmt" + "io" + "strings" + + "github.com/apecloud/kubeblocks/internal/cli/spinner" - "github.com/fatih/color" "github.com/pkg/errors" analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" "gopkg.in/yaml.v2" + + "github.com/apecloud/kubeblocks/internal/cli/printer" +) + +const ( + FailMessage = "Failed items were found. Please resolve the failed items and try again." ) type TextResultOutput struct { @@ -56,64 +68,115 @@ func NewTextOutput() TextOutput { } // ShowTextResults shadows interactive mode, and exports results by customized format -func ShowTextResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, format string, verbose bool) error { +func ShowTextResults(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, format string, verbose bool, out io.Writer) error { switch format { case "json": - return showTextResultsJSON(preflightName, analyzeResults, verbose) + return showTextResultsJSON(preflightName, analyzeResults, verbose, out) case "yaml": - return showStdoutResultsYAML(preflightName, analyzeResults, verbose) + return showStdoutResultsYAML(preflightName, analyzeResults, verbose, out) + case "kbcli": + return showResultsKBCli(preflightName, analyzeResults, verbose, out) default: return errors.Errorf("unknown output format: %q", format) } } -func showTextResultsJSON(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool) error { - b, err := json.MarshalIndent(showStdoutResultsStructured(preflightName, analyzeResults, verbose), "", " ") +func showResultsKBCli(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool, out io.Writer) error { + var ( + allMsg string + all = make([]string, 0) + spinnerDone = func(s spinner.Interface) { + s.SetFinalMsg(allMsg) + s.Done("") + fmt.Fprintln(out) + } + showResults = func(results []TextResultOutput) { + for _, result := range results { + all = append(all, fmt.Sprintf("- %s", result.Message)) + } + } + ) + msg := fmt.Sprintf("%-50s", "Kubernetes cluster preflight") + s := spinner.New(out, spinner.WithMessage(msg)) + data := showStdoutResultsStructured(preflightName, analyzeResults, verbose) + isFailed := false + + if verbose { + if len(data.Pass) > 0 { + all = append(all, fmt.Sprint(printer.BoldGreen("Pass"))) + showResults(data.Pass) + } + } + if len(data.Warn) > 0 { + all = append(all, fmt.Sprint(printer.BoldYellow("Warn"))) + showResults(data.Warn) + } + if len(data.Fail) > 0 { + all = append(all, fmt.Sprint(printer.BoldRed("Fail"))) + showResults(data.Fail) + isFailed = true + } + allMsg = fmt.Sprintf(" %s", strings.Join(all, "\n ")) + s.SetFinalMsg(suffixMsg(allMsg)) + if isFailed { + s.Fail() + spinnerDone(s) + return errors.New(FailMessage) + } + s.Success() + spinnerDone(s) + return nil +} + +func suffixMsg(msg string) string { + return fmt.Sprintf("%-50s", msg) +} + +func showTextResultsJSON(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool, out io.Writer) error { + output := showStdoutResultsStructured(preflightName, analyzeResults, verbose) + b, err := json.MarshalIndent(output, "", " ") if err != nil { return errors.Wrap(err, "failed to marshal results as json") } - fmt.Printf("%s\n", b) + + fmt.Fprintf(out, "%s\n", b) + if len(output.Fail) > 0 { + return errors.New(FailMessage) + } return nil } -func showStdoutResultsYAML(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool) error { +func showStdoutResultsYAML(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool, out io.Writer) error { data := showStdoutResultsStructured(preflightName, analyzeResults, verbose) - var ( - passInfo = color.New(color.FgGreen) - warnInfo = color.New(color.FgYellow) - failInfo = color.New(color.FgRed) - ) - if len(data.Warn) == 0 && len(data.Fail) == 0 { - passInfo.Println("congratulations, your kubernetes cluster preflight check pass, and begin to enjoy KubeBlocks...") - } if len(data.Pass) > 0 { - passInfo.Println("pass items") + fmt.Fprintln(out, printer.BoldGreen("Pass items")) if b, err := yaml.Marshal(data.Pass); err != nil { return errors.Wrap(err, "failed to marshal results as yaml") } else { - fmt.Printf("%s\n", b) + fmt.Fprintf(out, "%s", b) } } if len(data.Warn) > 0 { - warnInfo.Println("warn items") + fmt.Fprintln(out, printer.BoldYellow("Warn items")) if b, err := yaml.Marshal(data.Warn); err != nil { return errors.Wrap(err, "failed to marshal results as yaml") } else { - fmt.Printf("%s\n", b) + fmt.Fprintf(out, "%s", b) } } if len(data.Fail) > 0 { - failInfo.Println("fail items") + fmt.Fprintln(out, printer.BoldRed("Pass items")) if b, err := yaml.Marshal(data.Fail); err != nil { return errors.Wrap(err, "failed to marshal results as yaml") } else { - fmt.Printf("%s\n", b) + fmt.Fprintf(out, "%s", b) } + return errors.New(FailMessage) } return nil } -// showStdoutResultsStructured is Used by both JSON and YAML outputs +// showStdoutResultsStructured is Used by KBCLI, JSON and YAML outputs func showStdoutResultsStructured(preflightName string, analyzeResults []*analyzerunner.AnalyzeResult, verbose bool) TextOutput { output := NewTextOutput() for _, analyzeResult := range analyzeResults { diff --git a/internal/preflight/text_results_test.go b/internal/preflight/text_results_test.go index c91f9118e..d5921e588 100644 --- a/internal/preflight/text_results_test.go +++ b/internal/preflight/text_results_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package preflight @@ -19,17 +22,20 @@ package preflight import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/cli-runtime/pkg/genericclioptions" analyzerunner "github.com/replicatedhq/troubleshoot/pkg/analyze" ) var _ = Describe("text_results_test", func() { var ( - preflightName = "stdoutPreflightName" - humanFormat = "human" - jsonFormat = "json" - yamlFormat = "yaml" - unknownFormat = "unknown" + preflightName = "stdoutPreflightName" + jsonFormat = "json" + yamlFormat = "yaml" + kbcliFormat = "kbcli" + unknownFormat = "unknown" + streams, _, _, _ = genericclioptions.NewTestIOStreams() + out = streams.Out ) It("ShowStdoutResults Test", func() { analyzeResults := []*analyzerunner.AnalyzeResult{ @@ -39,12 +45,6 @@ var _ = Describe("text_results_test", func() { Message: "message for pass test", URI: "https://kubernetes.io", }, - { - IsFail: true, - Title: "fail item", - Message: "message for fail test", - URI: "https://kubernetes.io", - }, { IsWarn: true, Title: "warn item", @@ -54,14 +54,34 @@ var _ = Describe("text_results_test", func() { }, } Eventually(func(g Gomega) { - err := ShowTextResults(preflightName, analyzeResults, humanFormat, false) + err := ShowTextResults(preflightName, analyzeResults, jsonFormat, true, out) + g.Expect(err).NotTo(HaveOccurred()) + err = ShowTextResults(preflightName, analyzeResults, yamlFormat, false, out) + g.Expect(err).NotTo(HaveOccurred()) + err = ShowTextResults(preflightName, analyzeResults, kbcliFormat, false, out) + g.Expect(err).NotTo(HaveOccurred()) + err = ShowTextResults(preflightName, analyzeResults, unknownFormat, false, out) g.Expect(err).To(HaveOccurred()) - err = ShowTextResults(preflightName, analyzeResults, jsonFormat, true) + }).ShouldNot(HaveOccurred()) + }) + It("ShowStdoutResults Test", func() { + analyzeResults := []*analyzerunner.AnalyzeResult{ + { + IsFail: true, + Title: "fail item", + Message: "message for fail test", + URI: "https://kubernetes.io", + }, + } + Eventually(func(g Gomega) { + err := ShowTextResults(preflightName, analyzeResults, jsonFormat, true, out) + g.Expect(err).NotTo(HaveOccurred()) + err = ShowTextResults(preflightName, analyzeResults, yamlFormat, false, out) g.Expect(err).NotTo(HaveOccurred()) - err = ShowTextResults(preflightName, analyzeResults, yamlFormat, false) + err = ShowTextResults(preflightName, analyzeResults, kbcliFormat, false, out) g.Expect(err).NotTo(HaveOccurred()) - err = ShowTextResults(preflightName, analyzeResults, unknownFormat, false) + err = ShowTextResults(preflightName, analyzeResults, unknownFormat, false, out) g.Expect(err).To(HaveOccurred()) - }).Should(Succeed()) + }).Should(HaveOccurred()) }) }) diff --git a/internal/preflight/util/schema.go b/internal/preflight/util/schema.go index dc8f93d76..4e8a97aec 100644 --- a/internal/preflight/util/schema.go +++ b/internal/preflight/util/schema.go @@ -1,14 +1,20 @@ /* -Copyright ApeCloud, Inc. -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. +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/preflight/util/suite_test.go b/internal/preflight/util/suite_test.go index 65af1d282..ab1fa6284 100644 --- a/internal/preflight/util/suite_test.go +++ b/internal/preflight/util/suite_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/preflight/util/util.go b/internal/preflight/util/util.go index 74d2c3996..e25e89de7 100644 --- a/internal/preflight/util/util.go +++ b/internal/preflight/util/util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/preflight/util/util_test.go b/internal/preflight/util/util_test.go index b8a043489..d82e47e6e 100644 --- a/internal/preflight/util/util_test.go +++ b/internal/preflight/util/util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package util diff --git a/internal/sqlchannel/client.go b/internal/sqlchannel/client.go index ec0860353..b75e3476e 100644 --- a/internal/sqlchannel/client.go +++ b/internal/sqlchannel/client.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package sqlchannel @@ -29,6 +32,7 @@ import ( "github.com/apecloud/kubeblocks/internal/cli/exec" intctrlutil "github.com/apecloud/kubeblocks/internal/controllerutil" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" ) type OperationClient struct { @@ -46,12 +50,6 @@ type OperationResult struct { respTime time.Time } -type Order struct { - OrderID int `json:"orderid"` - Customer string `json:"customer"` - Price float64 `json:"price"` -} - func NewClientWithPod(pod *corev1.Pod, characterType string) (*OperationClient, error) { if characterType == "" { return nil, fmt.Errorf("pod %v chacterType must be set", pod.Name) @@ -61,6 +59,7 @@ func NewClientWithPod(pod *corev1.Pod, characterType string) (*OperationClient, if ip == "" { return nil, fmt.Errorf("pod %v has no ip", pod.Name) } + port, err := intctrlutil.GetProbeGRPCPort(pod) if err != nil { return nil, err @@ -93,6 +92,7 @@ func (cli *OperationClient) GetRole() (string, error) { Data: []byte(""), Metadata: map[string]string{}, } + resp, err := cli.InvokeComponentInRoutine(ctxWithReconcileTimeout, req) if err != nil { return "", err @@ -106,6 +106,37 @@ func (cli *OperationClient) GetRole() (string, error) { return result["role"], nil } +// GetSystemAccounts lists all system accounts created +func (cli *OperationClient) GetSystemAccounts() ([]string, error) { + ctxWithReconcileTimeout, cancel := context.WithTimeout(context.Background(), cli.ReconcileTimeout) + defer cancel() + + // Request sql channel via Dapr SDK + req := &dapr.InvokeBindingRequest{ + Name: cli.CharacterType, + Operation: string(ListSystemAccountsOp), + } + + if resp, err := cli.InvokeComponentInRoutine(ctxWithReconcileTimeout, req); err != nil { + return nil, err + } else { + sqlResponse := SQLChannelResponse{} + if err = json.Unmarshal(resp.Data, &sqlResponse); err != nil { + return nil, err + } + if sqlResponse.Event == RespEveFail { + return nil, fmt.Errorf("get system accounts error: %s", sqlResponse.Message) + } else { + result := []string{} + if err = json.Unmarshal(([]byte)(sqlResponse.Message), &result); err != nil { + return nil, err + } else { + return result, err + } + } + } +} + func (cli *OperationClient) InvokeComponentInRoutine(ctxWithReconcileTimeout context.Context, req *dapr.InvokeBindingRequest) (*dapr.BindingEvent, error) { ch := make(chan *OperationResult, 1) go cli.InvokeComponent(ctxWithReconcileTimeout, req, ch) @@ -169,11 +200,11 @@ type OperationHTTPClient struct { httpRequestPrefix string RequestTimeout time.Duration containerName string - exec *exec.ExecOptions + characterType string } -// NewHTTPClientWithPod create a new OperationHTTPClient with pod -func NewHTTPClientWithPod(exec *exec.ExecOptions, pod *corev1.Pod, characterType string) (*OperationHTTPClient, error) { +// NewHTTPClientWithChannelPod create a new OperationHTTPClient with sqlchannel container +func NewHTTPClientWithChannelPod(pod *corev1.Pod, characterType string) (*OperationHTTPClient, error) { var ( err error ) @@ -199,34 +230,59 @@ func NewHTTPClientWithPod(exec *exec.ExecOptions, pod *corev1.Pod, characterType httpRequestPrefix: fmt.Sprintf(HTTPRequestPrefx, port, characterType), RequestTimeout: 10 * time.Second, containerName: container, - exec: exec, + characterType: characterType, } return client, nil } -// SendRequest exec sql operation, this is a blocking operation and it will use pod EXEC subresource to send an http request to the probe pod -func (cli *OperationHTTPClient) SendRequest(request SQLChannelRequest) (SQLChannelResponse, error) { +// SendRequest execs sql operation, this is a blocking operation and use pod EXEC subresource to send a http request to the probed pod +func (cli *OperationHTTPClient) SendRequest(exec *exec.ExecOptions, request SQLChannelRequest) (SQLChannelResponse, error) { var ( - response = SQLChannelResponse{} strBuffer bytes.Buffer errBuffer bytes.Buffer err error + response = SQLChannelResponse{} ) if jsonData, err := json.Marshal(request); err != nil { return response, err } else { - cli.exec.ContainerName = cli.containerName - cli.exec.Command = []string{"sh", "-c", cli.httpRequestPrefix + " -d '" + string(jsonData) + "'"} + exec.ContainerName = cli.containerName + exec.Command = []string{"sh", "-c", cli.httpRequestPrefix + " -d '" + string(jsonData) + "'"} } // redirect output to strBuffer to be parsed later - if err = cli.exec.RunWithRedirect(&strBuffer, &errBuffer); err != nil { + if err = exec.RunWithRedirect(&strBuffer, &errBuffer); err != nil { return response, err } + return parseResponse(strBuffer.Bytes(), request.Operation, cli.characterType) +} - if err = json.Unmarshal(strBuffer.Bytes(), &response); err != nil { +type errorResponse struct { + ErrorCode string `json:"errorCode"` + Message string `json:"message"` +} + +// parseResponse parses response to errorResponse or SQLChannelResponse to capture error message. +func parseResponse(data []byte, operation string, charType string) (SQLChannelResponse, error) { + errorResponse := errorResponse{} + response := SQLChannelResponse{} + if err := json.Unmarshal(data, &errorResponse); err != nil { return response, err + } else if len(errorResponse.ErrorCode) > 0 { + return SQLChannelResponse{ + Event: RespEveFail, + Message: fmt.Sprintf("Operation `%s` on component of type `%s` is not supported yet.", operation, charType), + Metadata: SQLChannelMeta{ + Operation: operation, + StartTime: time.Now(), + EndTime: time.Now(), + Extra: errorResponse.Message, + }, + }, SQLChannelError{Reason: UnsupportedOps} } - return response, nil + + // convert it to SQLChannelResponse + err := json.Unmarshal(data, &response) + return response, err } diff --git a/internal/sqlchannel/client_test.go b/internal/sqlchannel/client_test.go index 38a0045ec..b860fa7bd 100644 --- a/internal/sqlchannel/client_test.go +++ b/internal/sqlchannel/client_test.go @@ -1,39 +1,79 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package sqlchannel import ( "context" + "encoding/json" + "errors" "fmt" "net" + "os" "strings" "testing" "time" + dapr "github.com/dapr/go-sdk/client" pb "github.com/dapr/go-sdk/dapr/proto/runtime/v1" + "github.com/stretchr/testify/assert" "google.golang.org/grpc" corev1 "k8s.io/api/core/v1" - intctrlutil "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/constant" + . "github.com/apecloud/kubeblocks/internal/sqlchannel/util" testapps "github.com/apecloud/kubeblocks/internal/testutil/apps" ) +type testDaprServer struct { + pb.UnimplementedDaprServer + state map[string][]byte + configurationSubscriptionID map[string]chan struct{} + cachedRequest map[string]*pb.InvokeBindingResponse +} + +var _ pb.DaprServer = &testDaprServer{} + +func (s *testDaprServer) InvokeBinding(ctx context.Context, req *pb.InvokeBindingRequest) (*pb.InvokeBindingResponse, error) { + time.Sleep(100 * time.Millisecond) + darpRequest := dapr.InvokeBindingRequest{Name: req.Name, Operation: req.Operation, Data: req.Data, Metadata: req.Metadata} + resp, ok := s.cachedRequest[GetMapKeyFromRequest(&darpRequest)] + if ok { + return resp, nil + } else { + return nil, fmt.Errorf("unexpected request") + } +} + +func (s *testDaprServer) ExepctRequest(req *pb.InvokeBindingRequest, resp *pb.InvokeBindingResponse) { + darpRequest := dapr.InvokeBindingRequest{Name: req.Name, Operation: req.Operation, Data: req.Data, Metadata: req.Metadata} + s.cachedRequest[GetMapKeyFromRequest(&darpRequest)] = resp +} + func TestNewClientWithPod(t *testing.T) { - port, closer := newTCPServer(t, 50001) + daprServer := &testDaprServer{ + state: make(map[string][]byte), + configurationSubscriptionID: map[string]chan struct{}{}, + cachedRequest: make(map[string]*pb.InvokeBindingResponse), + } + + port, closer := newTCPServer(t, daprServer, 50001) defer closer() podName := "pod-for-sqlchannel-test" pod := testapps.NewPodFactory("default", podName). @@ -41,12 +81,12 @@ func TestNewClientWithPod(t *testing.T) { GetObject() pod.Spec.Containers[0].Ports = []corev1.ContainerPort{{ ContainerPort: int32(3501), - Name: intctrlutil.ProbeHTTPPortName, + Name: constant.ProbeHTTPPortName, Protocol: "TCP", }, { ContainerPort: int32(port), - Name: intctrlutil.ProbeGRPCPortName, + Name: constant.ProbeGRPCPortName, Protocol: "TCP", }, } @@ -73,7 +113,7 @@ func TestNewClientWithPod(t *testing.T) { podWithoutGRPCPort.Spec.Containers[0].Ports = podWithoutGRPCPort.Spec.Containers[0].Ports[:1] _, err := NewClientWithPod(podWithoutGRPCPort, "mysql") if err == nil { - t.Errorf("new sql channel client unexpection") + t.Errorf("new sql channel client union") } }) @@ -85,28 +125,39 @@ func TestNewClientWithPod(t *testing.T) { }) } -func TestGetRole(t *testing.T) { - port, closer := newTCPServer(t, 50001) - defer closer() - podName := "pod-for-sqlchannel-test" - pod := testapps.NewPodFactory("default", podName). - AddContainer(corev1.Container{Name: testapps.DefaultNginxContainerName, Image: testapps.NginxImage}).GetObject() - pod.Spec.Containers[0].Ports = []corev1.ContainerPort{{ - ContainerPort: int32(3501), - Name: intctrlutil.ProbeHTTPPortName, - Protocol: "TCP", - }, - { - ContainerPort: int32(port), - Name: intctrlutil.ProbeGRPCPortName, - Protocol: "TCP", - }, +func TestGPRC(t *testing.T) { + url := os.Getenv("PROBE_GRPC_URL") + if url == "" { + t.SkipNow() } - pod.Status.PodIP = "127.0.0.1" - cli, err := NewClientWithPod(pod, "mysql") + req := &dapr.InvokeBindingRequest{ + Name: "mongodb", + Operation: "getRole", + Data: []byte(""), + Metadata: map[string]string{}, + } + cli, _ := dapr.NewClientWithAddress(url) + resp, _ := cli.InvokeBinding(context.Background(), req) + t.Logf("probe response metadata: %v", resp.Metadata) + result := map[string]string{} + _ = json.Unmarshal(resp.Data, &result) + t.Logf("probe response data: %v", result) + +} + +func TestGetRole(t *testing.T) { + daprServer, cli, closer, err := initSQLChannelClient(t) if err != nil { t.Errorf("new sql channel client error: %v", err) } + defer closer() + + daprServer.ExepctRequest(&pb.InvokeBindingRequest{ + Name: "mysql", + Operation: "getRole", + }, &pb.InvokeBindingResponse{ + Data: []byte("{\"role\": \"leader\"}"), + }) t.Run("ResponseInTime", func(t *testing.T) { cli.ReconcileTimeout = 1 * time.Second @@ -146,7 +197,93 @@ func TestGetRole(t *testing.T) { }) } -func newTCPServer(t *testing.T, port int) (int, func()) { +func TestSystemAccounts(t *testing.T) { + daprServer, cli, closer, err := initSQLChannelClient(t) + if err != nil { + t.Errorf("new sql channel client error: %v", err) + } + defer closer() + + roleNames, _ := json.Marshal([]string{"kbadmin", "kbprobe"}) + sqlResponse := SQLChannelResponse{ + Event: RespEveSucc, + Message: string(roleNames), + } + respData, _ := json.Marshal(sqlResponse) + resp := &pb.InvokeBindingResponse{ + Data: respData, + } + + daprServer.ExepctRequest(&pb.InvokeBindingRequest{ + Name: "mysql", + Operation: string(ListSystemAccountsOp), + }, resp) + + t.Run("ResponseByCache", func(t *testing.T) { + cli.ReconcileTimeout = 200 * time.Millisecond + _, err := cli.GetSystemAccounts() + + if err != nil { + t.Errorf("return reps in cache: %v", err) + } + if len(cli.cache) != 0 { + t.Errorf("cache should be cleared: %v", cli.cache) + } + }) +} + +func TestParseSqlChannelResult(t *testing.T) { + t.Run("Binding Not Supported", func(t *testing.T) { + result := ` + {"errorCode":"ERR_INVOKE_OUTPUT_BINDING","message":"error when invoke output binding mongodb: binding mongodb does not support operation listUsers. supported operations:checkRunning checkRole getRole"} + ` + sqlResposne, err := parseResponse(([]byte)(result), "listUsers", "mongodb") + assert.NotNil(t, err) + assert.True(t, IsUnSupportedError(err)) + assert.Equal(t, sqlResposne.Event, RespEveFail) + assert.Contains(t, sqlResposne.Message, "not supported") + }) + + t.Run("Binding Exec Failed", func(t *testing.T) { + result := ` + {"event":"Failed","message":"db not ready"} + ` + sqlResposne, err := parseResponse(([]byte)(result), "listUsers", "mongodb") + assert.Nil(t, err) + assert.Equal(t, sqlResposne.Event, RespEveFail) + assert.Contains(t, sqlResposne.Message, "db not ready") + }) + + t.Run("Binding Exec Success", func(t *testing.T) { + result := ` + {"event":"Success","message":"[]"} + ` + sqlResposne, err := parseResponse(([]byte)(result), "listUsers", "mongodb") + assert.Nil(t, err) + assert.Equal(t, sqlResposne.Event, RespEveSucc) + }) + + t.Run("Invalid Resonse Format", func(t *testing.T) { + // msg cannot be parsed to json + result := ` + {"event":"Success","message":"[] + ` + _, err := parseResponse(([]byte)(result), "listUsers", "mongodb") + assert.NotNil(t, err) + }) +} + +func TestErrMsg(t *testing.T) { + err := SQLChannelError{ + Reason: UnsupportedOps, + } + assert.True(t, strings.Contains(err.Error(), "unsupported")) + assert.False(t, IsUnSupportedError(nil)) + assert.True(t, IsUnSupportedError(err)) + assert.False(t, IsUnSupportedError(errors.New("test"))) +} + +func newTCPServer(t *testing.T, daprServer pb.DaprServer, port int) (int, func()) { var l net.Listener for i := 0; i < 3; i++ { l, _ = net.Listen("tcp", fmt.Sprintf(":%v", port)) @@ -159,10 +296,7 @@ func newTCPServer(t *testing.T, port int) (int, func()) { t.Errorf("couldn't start listening") } s := grpc.NewServer() - pb.RegisterDaprServer(s, &testDaprServer{ - state: make(map[string][]byte), - configurationSubscriptionID: map[string]chan struct{}{}, - }) + pb.RegisterDaprServer(s, daprServer) go func() { if err := s.Serve(l); err != nil && err.Error() != "closed" { @@ -177,22 +311,33 @@ func newTCPServer(t *testing.T, port int) (int, func()) { return port, closer } -type testDaprServer struct { - pb.UnimplementedDaprServer - state map[string][]byte - configurationSubscriptionID map[string]chan struct{} -} +func initSQLChannelClient(t *testing.T) (*testDaprServer, *OperationClient, func(), error) { + daprServer := &testDaprServer{ + state: make(map[string][]byte), + configurationSubscriptionID: map[string]chan struct{}{}, + cachedRequest: make(map[string]*pb.InvokeBindingResponse), + } -func (s *testDaprServer) InvokeBinding(ctx context.Context, req *pb.InvokeBindingRequest) (*pb.InvokeBindingResponse, error) { - time.Sleep(100 * time.Millisecond) - if req.Data == nil { - return &pb.InvokeBindingResponse{ - Data: []byte("{\"role\": \"leader\"}"), - Metadata: map[string]string{"k1": "v1", "k2": "v2"}, - }, nil - } - return &pb.InvokeBindingResponse{ - Data: req.Data, - Metadata: req.Metadata, - }, nil + port, closer := newTCPServer(t, daprServer, 50001) + podName := "pod-for-sqlchannel-test" + pod := testapps.NewPodFactory("default", podName). + AddContainer(corev1.Container{Name: testapps.DefaultNginxContainerName, Image: testapps.NginxImage}).GetObject() + pod.Spec.Containers[0].Ports = []corev1.ContainerPort{ + { + ContainerPort: int32(3501), + Name: constant.ProbeHTTPPortName, + Protocol: "TCP", + }, + { + ContainerPort: int32(port), + Name: constant.ProbeGRPCPortName, + Protocol: "TCP", + }, + } + pod.Status.PodIP = "127.0.0.1" + cli, err := NewClientWithPod(pod, "mysql") + if err != nil { + t.Errorf("new sql channel client error: %v", err) + } + return daprServer, cli, closer, err } diff --git a/internal/cli/engine/client.go b/internal/sqlchannel/engine/client.go similarity index 50% rename from internal/cli/engine/client.go rename to internal/sqlchannel/engine/client.go index 56381a639..a0f2936c4 100644 --- a/internal/cli/engine/client.go +++ b/internal/sqlchannel/engine/client.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package engine diff --git a/internal/cli/engine/engine.go b/internal/sqlchannel/engine/engine.go similarity index 56% rename from internal/cli/engine/engine.go rename to internal/sqlchannel/engine/engine.go index 4559c3090..5501a7dda 100644 --- a/internal/cli/engine/engine.go +++ b/internal/sqlchannel/engine/engine.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package engine @@ -22,29 +25,25 @@ import ( "strings" ) -// ClusterDefinition ComponentDefRef Const Define const ( stateMysql = "mysql" statePostgreSQL = "postgresql" stateRedis = "redis" + stateMongoDB = "mongodb" ) +// AuthInfo is the authentication information for the database +type AuthInfo struct { + UserName string + UserPasswd string +} + type Interface interface { - ConnectCommand() []string + ConnectCommand(info *AuthInfo) []string Container() string ConnectExample(info *ConnectionInfo, client string) string } -type ConnectionInfo struct { - Host string - User string - Password string - Database string - Port string - Command []string - Args []string -} - type EngineInfo struct { Client string Container string @@ -53,8 +52,6 @@ type EngineInfo struct { Database string } -type buildConnectExample func(info *ConnectionInfo) string - func New(typeName string) (Interface, error) { switch typeName { case stateMysql: @@ -63,11 +60,26 @@ func New(typeName string) (Interface, error) { return newPostgreSQL(), nil case stateRedis: return newRedis(), nil + case stateMongoDB: + return newMongoDB(), nil default: return nil, fmt.Errorf("unsupported engine type: %s", typeName) } } +type ConnectionInfo struct { + Host string + User string + Password string + Database string + Port string + ClusterName string + ComponentName string + HeadlessEndpoint string +} + +type buildConnectExample func(info *ConnectionInfo) string + func buildExample(info *ConnectionInfo, client string, examples map[ClientType]buildConnectExample) string { // if client is not specified, output all examples if len(client) == 0 { diff --git a/internal/sqlchannel/engine/engine_test.go b/internal/sqlchannel/engine/engine_test.go new file mode 100644 index 000000000..56194bd81 --- /dev/null +++ b/internal/sqlchannel/engine/engine_test.go @@ -0,0 +1,51 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package engine + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Engine", func() { + It("new mysql engine", func() { + for _, typeName := range []string{stateMysql, statePostgreSQL, stateRedis} { + engine, _ := New(typeName) + Expect(engine).ShouldNot(BeNil()) + + url := engine.ConnectCommand(nil) + Expect(len(url)).Should(Equal(3)) + + url = engine.ConnectCommand(nil) + Expect(len(url)).Should(Equal(3)) + // it is a tricky way to check the container name + // for the moment, we only support mysql, postgresql and redis + // and the container name is the same as the state name + Expect(engine.Container()).Should(Equal(typeName)) + } + }) + + It("new unknown engine", func() { + typeName := "unknown-type" + engine, err := New(typeName) + Expect(engine).Should(BeNil()) + Expect(err).Should(HaveOccurred()) + }) +}) diff --git a/internal/sqlchannel/engine/mongodb.go b/internal/sqlchannel/engine/mongodb.go new file mode 100644 index 000000000..e3ba9d213 --- /dev/null +++ b/internal/sqlchannel/engine/mongodb.go @@ -0,0 +1,76 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package engine + +import ( + "fmt" + "strings" +) + +type mongodb struct { + info EngineInfo + examples map[ClientType]buildConnectExample +} + +func newMongoDB() *mongodb { + return &mongodb{ + info: EngineInfo{ + Client: "mongosh", + Container: "mongodb", + UserEnv: "$MONGODB_ROOT_USER", + PasswordEnv: "$MONGODB_ROOT_PASSWORD", + Database: "admin", + }, + examples: map[ClientType]buildConnectExample{ + CLI: func(info *ConnectionInfo) string { + return fmt.Sprintf(`# mongodb client connection example +mongosh mongodb://%s:%s@%s/%s?replicaset=%s-%s +`, info.User, info.Password, info.HeadlessEndpoint, info.Database, info.ClusterName, info.ComponentName) + }, + }, + } +} + +func (r mongodb) ConnectCommand(connectInfo *AuthInfo) []string { + userName := r.info.UserEnv + userPass := r.info.PasswordEnv + + if connectInfo != nil { + userName = connectInfo.UserName + userPass = connectInfo.UserPasswd + } + + mongodbCmd := []string{fmt.Sprintf("%s mongodb://%s:%s@$KB_POD_FQDN:27017/admin?replicaSet=$KB_CLUSTER_COMP_NAME", r.info.Client, userName, userPass)} + + return []string{"sh", "-c", strings.Join(mongodbCmd, " ")} +} + +func (r mongodb) Container() string { + return r.info.Container +} + +func (r mongodb) ConnectExample(info *ConnectionInfo, client string) string { + if len(info.Database) == 0 { + info.Database = r.info.Database + } + return buildExample(info, client, r.examples) +} + +var _ Interface = &mongodb{} diff --git a/internal/cli/engine/mysql.go b/internal/sqlchannel/engine/mysql.go similarity index 81% rename from internal/cli/engine/mysql.go rename to internal/sqlchannel/engine/mysql.go index facb2b933..fdd7188a4 100644 --- a/internal/cli/engine/mysql.go +++ b/internal/sqlchannel/engine/mysql.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package engine @@ -34,6 +37,7 @@ func newMySQL() *mysql { Client: "mysql", Container: "mysql", PasswordEnv: "$MYSQL_ROOT_PASSWORD", + UserEnv: "$MYSQL_ROOT_USER", Database: "mysql", }, examples: map[ClientType]buildConnectExample{ @@ -246,8 +250,21 @@ DATABASE_URL='mysql://%s:%s@%s:%s/%s' } } -func (m *mysql) ConnectCommand() []string { - mysqlCmd := []string{"MYSQL_PWD=" + m.info.PasswordEnv, m.info.Client} +func (m *mysql) ConnectCommand(connectInfo *AuthInfo) []string { + userName := m.info.UserEnv + userPass := m.info.PasswordEnv + + if connectInfo != nil { + userName = connectInfo.UserName + userPass = connectInfo.UserPasswd + } + + // avoid using env variables + // MYSQL_PWD is deprecated as of MySQL 8.0; expect it to be removed in a future version of MySQL. + // ref to mysql manual for more details. + // https://dev.mysql.com/doc/refman/8.0/en/environment-variables.html + mysqlCmd := []string{fmt.Sprintf("%s -u%s -p%s", m.info.Client, userName, userPass)} + return []string{"sh", "-c", strings.Join(mysqlCmd, " ")} } diff --git a/internal/sqlchannel/engine/mysql_test.go b/internal/sqlchannel/engine/mysql_test.go new file mode 100644 index 000000000..6e2da5e63 --- /dev/null +++ b/internal/sqlchannel/engine/mysql_test.go @@ -0,0 +1,46 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package engine + +import ( + "fmt" + + . "github.com/onsi/ginkgo/v2" +) + +var _ = Describe("Mysql Engine", func() { + It("connection example", func() { + mysql := newMySQL() + + info := &ConnectionInfo{ + User: "user", + Host: "host", + Password: "*****", + Database: "test-db", + Port: "1234", + } + for k := range mysql.examples { + fmt.Printf("%s Connection Example\n", k.String()) + fmt.Println(mysql.ConnectExample(info, k.String())) + } + + fmt.Println(mysql.ConnectExample(info, "")) + }) +}) diff --git a/internal/cli/engine/postgresql.go b/internal/sqlchannel/engine/postgresql.go similarity index 81% rename from internal/cli/engine/postgresql.go rename to internal/sqlchannel/engine/postgresql.go index 24912774c..77c8227d3 100644 --- a/internal/cli/engine/postgresql.go +++ b/internal/sqlchannel/engine/postgresql.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package engine @@ -33,8 +36,8 @@ func newPostgreSQL() *postgresql { info: EngineInfo{ Client: "psql", Container: "postgresql", - PasswordEnv: "$POSTGRES_PASSWORD", - UserEnv: "$POSTGRES_USER", + PasswordEnv: "$PGPASSWORD", + UserEnv: "$PGUSER", Database: "postgres", }, examples: map[ClientType]buildConnectExample{ @@ -228,8 +231,18 @@ DATABASE_URL='postgresql://%s:%s@%s:%s/%s' } } -func (m *postgresql) ConnectCommand() []string { - cmd := []string{"PGPASSWORD=" + m.info.PasswordEnv, "PGUSER=" + m.info.UserEnv, m.info.Client} +func (m *postgresql) ConnectCommand(connectInfo *AuthInfo) []string { + userName := m.info.UserEnv + userPass := m.info.PasswordEnv + + if connectInfo != nil { + userName = connectInfo.UserName + userPass = connectInfo.UserPasswd + } + + // please refer to PostgreSQL documentation for more details + // https://www.postgresql.org/docs/current/libpq-envars.html + cmd := []string{fmt.Sprintf("PGUSER=%s PGPASSWORD=%s PGDATABASE=%s %s", userName, userPass, m.info.Database, m.info.Client)} return []string{"sh", "-c", strings.Join(cmd, " ")} } diff --git a/internal/sqlchannel/engine/redis.go b/internal/sqlchannel/engine/redis.go new file mode 100644 index 000000000..be5a6b524 --- /dev/null +++ b/internal/sqlchannel/engine/redis.go @@ -0,0 +1,60 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package engine + +import "strings" + +type redis struct { + info EngineInfo + examples map[ClientType]buildConnectExample +} + +func newRedis() *redis { + return &redis{ + info: EngineInfo{ + Client: "redis-cli", + Container: "redis", + }, + examples: map[ClientType]buildConnectExample{}, + } +} + +func (r redis) ConnectCommand(connectInfo *AuthInfo) []string { + redisCmd := []string{ + "redis-cli", + } + + if connectInfo != nil { + redisCmd = append(redisCmd, "--user", connectInfo.UserName) + redisCmd = append(redisCmd, "--pass", connectInfo.UserPasswd) + } + return []string{"sh", "-c", strings.Join(redisCmd, " ")} +} + +func (r redis) Container() string { + return r.info.Container +} + +func (r redis) ConnectExample(info *ConnectionInfo, client string) string { + // TODO implement me + panic("implement me") +} + +var _ Interface = &redis{} diff --git a/internal/sqlchannel/engine/suite_test.go b/internal/sqlchannel/engine/suite_test.go new file mode 100644 index 000000000..647f05097 --- /dev/null +++ b/internal/sqlchannel/engine/suite_test.go @@ -0,0 +1,32 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package engine_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestEngine(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Engine Suite") +} diff --git a/internal/sqlchannel/types.go b/internal/sqlchannel/types.go deleted file mode 100644 index ee000c4aa..000000000 --- a/internal/sqlchannel/types.go +++ /dev/null @@ -1,76 +0,0 @@ -/* -Copyright ApeCloud, Inc. - -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 sqlchannel - -import ( - "time" - - "github.com/dapr/components-contrib/bindings" -) - -const ( - RespTypEve = "event" - RespTypMsg = "message" - RespTypMeta = "metadata" - RespEveSucc = "Success" - RespEveFail = "Failed" - - SuperUserRole string = "superuser" - ReadWriteRole string = "readwrite" - ReadOnlyRole string = "readonly" - InvalidRole string = "invalid" - - // actions for cluster accounts management - ListUsersOp bindings.OperationKind = "listUsers" - CreateUserOp bindings.OperationKind = "createUser" - DeleteUserOp bindings.OperationKind = "deleteUser" - DescribeUserOp bindings.OperationKind = "describeUser" - GrantUserRoleOp bindings.OperationKind = "grantUserRole" - RevokeUserRoleOp bindings.OperationKind = "revokeUserRole" - - HTTPRequestPrefx string = "curl -X POST -H 'Content-Type: application/json' http://localhost:%d/v1.0/bindings/%s" -) - -// UserInfo is the user information for account management -type UserInfo struct { - UserName string `json:"userName"` - Password string `json:"password,omitempty"` - Expired string `json:"expired,omitempty"` - ExpireAt time.Duration `json:"expireAt,omitempty"` - RoleName string `json:"roleName,omitempty"` -} - -// SQLChannelRequest is the request for sqlchannel -type SQLChannelRequest struct { - Operation string `json:"operation"` - Metadata map[string]interface{} `json:"metadata,omitempty"` -} - -// SQLChannelResponse is the response for sqlchannel -type SQLChannelResponse struct { - Event string `json:"event,omitempty"` - Message string `json:"message,omitempty"` - Metadata SQLChannelMeta `json:"metadata,omitempty"` -} - -// SQLChannelMeta is the metadata for sqlchannel -type SQLChannelMeta struct { - Operation string `json:"operation,omitempty"` - StartTime time.Time `json:"startTime,omitempty"` - EndTime time.Time `json:"endTime,omitempty"` - Extra string `json:"extra,omitempty"` -} diff --git a/internal/sqlchannel/util/types.go b/internal/sqlchannel/util/types.go new file mode 100644 index 000000000..270530ec3 --- /dev/null +++ b/internal/sqlchannel/util/types.go @@ -0,0 +1,148 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package util + +import ( + "strings" + "time" + + "github.com/dapr/components-contrib/bindings" +) + +const ( + RespTypEve = "event" + RespTypMsg = "message" + RespTypMeta = "metadata" + RespEveSucc = "Success" + RespEveFail = "Failed" + + CheckRunningOperation bindings.OperationKind = "checkRunning" + CheckStatusOperation bindings.OperationKind = "checkStatus" + CheckRoleOperation bindings.OperationKind = "checkRole" + GetRoleOperation bindings.OperationKind = "getRole" + GetLagOperation bindings.OperationKind = "getLag" + ExecOperation bindings.OperationKind = "exec" + QueryOperation bindings.OperationKind = "query" + CloseOperation bindings.OperationKind = "close" + + // actions for cluster accounts management + ListUsersOp bindings.OperationKind = "listUsers" + CreateUserOp bindings.OperationKind = "createUser" + DeleteUserOp bindings.OperationKind = "deleteUser" + DescribeUserOp bindings.OperationKind = "describeUser" + GrantUserRoleOp bindings.OperationKind = "grantUserRole" + RevokeUserRoleOp bindings.OperationKind = "revokeUserRole" + ListSystemAccountsOp bindings.OperationKind = "listSystemAccounts" + + OperationNotImplemented = "NotImplemented" + OperationInvalid = "Invalid" + OperationSuccess = "Success" + OperationFailed = "Failed" + + HTTPRequestPrefx string = "curl -X POST -H 'Content-Type: application/json' http://localhost:%d/v1.0/bindings/%s" +) + +type RoleType string + +func (r RoleType) EqualTo(role string) bool { + return strings.EqualFold(string(r), role) +} + +func (r RoleType) GetWeight() int32 { + switch r { + case SuperUserRole: + return 1 << 3 + case ReadWriteRole: + return 1 << 2 + case ReadOnlyRole: + return 1 << 1 + case CustomizedRole: + return 1 + default: + return 0 + } +} + +const ( + SuperUserRole RoleType = "superuser" + ReadWriteRole RoleType = "readwrite" + ReadOnlyRole RoleType = "readonly" + NoPrivileges RoleType = "" + CustomizedRole RoleType = "customized" + InvalidRole RoleType = "invalid" +) + +// UserInfo is the user information for account management +type UserInfo struct { + UserName string `json:"userName"` + Password string `json:"password,omitempty"` + Expired string `json:"expired,omitempty"` + ExpireAt time.Duration `json:"expireAt,omitempty"` + RoleName string `json:"roleName,omitempty"` +} + +// SQLChannelRequest is the request for sqlchannel +type SQLChannelRequest struct { + Operation string `json:"operation"` + Metadata map[string]interface{} `json:"metadata,omitempty"` +} + +// SQLChannelResponse is the response for sqlchannel +type SQLChannelResponse struct { + Event string `json:"event,omitempty"` + Message string `json:"message,omitempty"` + Metadata SQLChannelMeta `json:"metadata,omitempty"` +} + +// SQLChannelMeta is the metadata for sqlchannel +type SQLChannelMeta struct { + Operation string `json:"operation,omitempty"` + StartTime time.Time `json:"startTime,omitempty"` + EndTime time.Time `json:"endTime,omitempty"` + Extra string `json:"extra,omitempty"` +} + +type errorReason string + +const ( + UnsupportedOps errorReason = "unsupported operation" +) + +// SQLChannelError is the error for sqlchannel, it implements error interface +type SQLChannelError struct { + Reason errorReason +} + +var _ error = SQLChannelError{} + +func (e SQLChannelError) Error() string { + return string(e.Reason) +} + +// IsUnSupportedError checks if the error is unsupported operation error +func IsUnSupportedError(err error) bool { + if err == nil { + return false + } + if e, ok := err.(SQLChannelError); ok { + return e.Reason == UnsupportedOps + } + return false +} diff --git a/internal/testutil/apps/backup_factory.go b/internal/testutil/apps/backup_factory.go index 35e62bfd5..5695d1d00 100644 --- a/internal/testutil/apps/backup_factory.go +++ b/internal/testutil/apps/backup_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -19,7 +22,6 @@ package apps import ( "time" - "github.com/onsi/gomega" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" @@ -48,12 +50,21 @@ func (factory *MockBackupFactory) SetBackupType(backupType dataprotectionv1alpha return factory } -func (factory *MockBackupFactory) SetTTL(duration string) *MockBackupFactory { - du, err := time.ParseDuration(duration) - gomega.Expect(err).Should(gomega.Succeed()) +func (factory *MockBackupFactory) SetLabels(labels map[string]string) *MockBackupFactory { + factory.get().SetLabels(labels) + return factory +} - var d metav1.Duration - d.Duration = du - factory.get().Spec.TTL = &d +func (factory *MockBackupFactory) SetBackLog(startTime, stopTime time.Time) *MockBackupFactory { + manifests := factory.get().Status.Manifests + if manifests == nil { + manifests = &dataprotectionv1alpha1.ManifestsStatus{} + } + if manifests.BackupLog == nil { + manifests.BackupLog = &dataprotectionv1alpha1.BackupLogStatus{} + } + manifests.BackupLog.StartTime = &metav1.Time{Time: startTime} + manifests.BackupLog.StopTime = &metav1.Time{Time: stopTime} + factory.get().Status.Manifests = manifests return factory } diff --git a/internal/testutil/apps/backuppolicy_factory.go b/internal/testutil/apps/backuppolicy_factory.go index 691988d99..175a302bf 100644 --- a/internal/testutil/apps/backuppolicy_factory.go +++ b/internal/testutil/apps/backuppolicy_factory.go @@ -1,124 +1,207 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( - "time" - - "github.com/onsi/gomega" - corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" ) type MockBackupPolicyFactory struct { BaseFactory[dataprotectionv1alpha1.BackupPolicy, *dataprotectionv1alpha1.BackupPolicy, MockBackupPolicyFactory] + backupType dataprotectionv1alpha1.BackupType } func NewBackupPolicyFactory(namespace, name string) *MockBackupPolicyFactory { f := &MockBackupPolicyFactory{} f.init(namespace, name, - &dataprotectionv1alpha1.BackupPolicy{ - Spec: dataprotectionv1alpha1.BackupPolicySpec{ - Target: dataprotectionv1alpha1.TargetCluster{ - LabelsSelector: &metav1.LabelSelector{ - MatchLabels: map[string]string{}, - }, - }, - Hooks: &dataprotectionv1alpha1.BackupPolicyHook{}, - }, - }, f) + &dataprotectionv1alpha1.BackupPolicy{}, f) return f } -func (factory *MockBackupPolicyFactory) SetBackupPolicyTplName(backupPolicyTplName string) *MockBackupPolicyFactory { - factory.get().Spec.BackupPolicyTemplateName = backupPolicyTplName +func (factory *MockBackupPolicyFactory) setBasePolicyField(setField func(basePolicy *dataprotectionv1alpha1.BasePolicy)) { + var basePolicy *dataprotectionv1alpha1.BasePolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeDataFile: + basePolicy = &factory.get().Spec.Datafile.BasePolicy + case dataprotectionv1alpha1.BackupTypeLogFile: + basePolicy = &factory.get().Spec.Logfile.BasePolicy + case dataprotectionv1alpha1.BackupTypeSnapshot: + basePolicy = &factory.get().Spec.Snapshot.BasePolicy + } + if basePolicy == nil { + // ignore + return + } + setField(basePolicy) +} + +func (factory *MockBackupPolicyFactory) setCommonPolicyField(setField func(commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy)) { + var commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeDataFile: + commonPolicy = factory.get().Spec.Datafile + case dataprotectionv1alpha1.BackupTypeLogFile: + commonPolicy = factory.get().Spec.Logfile + } + if commonPolicy == nil { + // ignore + return + } + setField(commonPolicy) +} + +func (factory *MockBackupPolicyFactory) setScheduleField(setField func(schedulePolicy *dataprotectionv1alpha1.SchedulePolicy)) { + var schedulePolicy *dataprotectionv1alpha1.SchedulePolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeDataFile: + factory.get().Spec.Schedule.Datafile = &dataprotectionv1alpha1.SchedulePolicy{} + schedulePolicy = factory.get().Spec.Schedule.Datafile + case dataprotectionv1alpha1.BackupTypeSnapshot: + factory.get().Spec.Schedule.Snapshot = &dataprotectionv1alpha1.SchedulePolicy{} + schedulePolicy = factory.get().Spec.Schedule.Snapshot + // todo: set logfile schedule + case dataprotectionv1alpha1.BackupTypeLogFile: + factory.get().Spec.Schedule.Logfile = &dataprotectionv1alpha1.SchedulePolicy{} + schedulePolicy = factory.get().Spec.Schedule.Snapshot + } + if schedulePolicy == nil { + // ignore + return + } + setField(schedulePolicy) +} + +func (factory *MockBackupPolicyFactory) AddSnapshotPolicy() *MockBackupPolicyFactory { + factory.get().Spec.Snapshot = &dataprotectionv1alpha1.SnapshotPolicy{ + Hooks: &dataprotectionv1alpha1.BackupPolicyHook{}, + } + factory.backupType = dataprotectionv1alpha1.BackupTypeSnapshot + return factory +} + +func (factory *MockBackupPolicyFactory) AddFullPolicy() *MockBackupPolicyFactory { + factory.get().Spec.Datafile = &dataprotectionv1alpha1.CommonBackupPolicy{ + PersistentVolumeClaim: dataprotectionv1alpha1.PersistentVolumeClaim{ + CreatePolicy: dataprotectionv1alpha1.CreatePVCPolicyIfNotPresent, + }, + } + factory.backupType = dataprotectionv1alpha1.BackupTypeDataFile + return factory +} + +func (factory *MockBackupPolicyFactory) AddIncrementalPolicy() *MockBackupPolicyFactory { + factory.get().Spec.Logfile = &dataprotectionv1alpha1.CommonBackupPolicy{ + PersistentVolumeClaim: dataprotectionv1alpha1.PersistentVolumeClaim{ + CreatePolicy: dataprotectionv1alpha1.CreatePVCPolicyIfNotPresent, + }, + } + factory.backupType = dataprotectionv1alpha1.BackupTypeLogFile return factory } func (factory *MockBackupPolicyFactory) SetBackupToolName(backupToolName string) *MockBackupPolicyFactory { - factory.get().Spec.BackupToolName = backupToolName + factory.setCommonPolicyField(func(commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy) { + commonPolicy.BackupToolName = backupToolName + }) return factory } -func (factory *MockBackupPolicyFactory) SetSchedule(schedule string) *MockBackupPolicyFactory { - factory.get().Spec.Schedule = schedule +func (factory *MockBackupPolicyFactory) SetSchedule(schedule string, enable bool) *MockBackupPolicyFactory { + factory.setScheduleField(func(schedulePolicy *dataprotectionv1alpha1.SchedulePolicy) { + schedulePolicy.Enable = enable + schedulePolicy.CronExpression = schedule + }) return factory } func (factory *MockBackupPolicyFactory) SetTTL(duration string) *MockBackupPolicyFactory { - du, err := time.ParseDuration(duration) - gomega.Expect(err).Should(gomega.Succeed()) - - var d metav1.Duration - d.Duration = du - factory.get().Spec.TTL = &d + factory.get().Spec.Retention = &dataprotectionv1alpha1.RetentionSpec{ + TTL: &duration, + } return factory } func (factory *MockBackupPolicyFactory) SetBackupsHistoryLimit(backupsHistoryLimit int32) *MockBackupPolicyFactory { - factory.get().Spec.BackupsHistoryLimit = backupsHistoryLimit + factory.setBasePolicyField(func(basePolicy *dataprotectionv1alpha1.BasePolicy) { + basePolicy.BackupsHistoryLimit = backupsHistoryLimit + }) return factory } func (factory *MockBackupPolicyFactory) AddMatchLabels(keyAndValues ...string) *MockBackupPolicyFactory { + matchLabels := make(map[string]string) for k, v := range WithMap(keyAndValues...) { - factory.get().Spec.Target.LabelsSelector.MatchLabels[k] = v + matchLabels[k] = v } + factory.setBasePolicyField(func(basePolicy *dataprotectionv1alpha1.BasePolicy) { + basePolicy.Target.LabelsSelector = &metav1.LabelSelector{ + MatchLabels: matchLabels, + } + }) return factory } func (factory *MockBackupPolicyFactory) SetTargetSecretName(name string) *MockBackupPolicyFactory { - factory.get().Spec.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{} - factory.get().Spec.Target.Secret.Name = name + factory.setBasePolicyField(func(basePolicy *dataprotectionv1alpha1.BasePolicy) { + basePolicy.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{Name: name} + }) return factory } func (factory *MockBackupPolicyFactory) SetHookContainerName(containerName string) *MockBackupPolicyFactory { - factory.get().Spec.Hooks.ContainerName = containerName + snapshotPolicy := factory.get().Spec.Snapshot + if snapshotPolicy == nil { + return factory + } + snapshotPolicy.Hooks.ContainerName = containerName return factory } func (factory *MockBackupPolicyFactory) AddHookPreCommand(preCommand string) *MockBackupPolicyFactory { - preCommands := &factory.get().Spec.Hooks.PreCommands + snapshotPolicy := factory.get().Spec.Snapshot + if snapshotPolicy == nil { + return factory + } + preCommands := &snapshotPolicy.Hooks.PreCommands *preCommands = append(*preCommands, preCommand) return factory } func (factory *MockBackupPolicyFactory) AddHookPostCommand(postCommand string) *MockBackupPolicyFactory { - postCommands := &factory.get().Spec.Hooks.PostCommands + snapshotPolicy := factory.get().Spec.Snapshot + if snapshotPolicy == nil { + return factory + } + postCommands := &snapshotPolicy.Hooks.PostCommands *postCommands = append(*postCommands, postCommand) return factory } -func (factory *MockBackupPolicyFactory) SetRemoteVolume(volume corev1.Volume) *MockBackupPolicyFactory { - factory.get().Spec.RemoteVolume = volume - return factory -} - -func (factory *MockBackupPolicyFactory) SetRemoteVolumePVC(volumeName, pvcName string) *MockBackupPolicyFactory { - factory.get().Spec.RemoteVolume = corev1.Volume{ - Name: volumeName, - VolumeSource: corev1.VolumeSource{ - PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: pvcName, - }, - }, - } +func (factory *MockBackupPolicyFactory) SetPVC(pvcName string) *MockBackupPolicyFactory { + factory.setCommonPolicyField(func(commonPolicy *dataprotectionv1alpha1.CommonBackupPolicy) { + commonPolicy.PersistentVolumeClaim.Name = pvcName + commonPolicy.PersistentVolumeClaim.InitCapacity = resource.MustParse(constant.DefaultBackupPvcInitCapacity) + }) return factory } diff --git a/internal/testutil/apps/backuppolicytemplate_factory.go b/internal/testutil/apps/backuppolicytemplate_factory.go index 94e9b8b7c..c0aef4858 100644 --- a/internal/testutil/apps/backuppolicytemplate_factory.go +++ b/internal/testutil/apps/backuppolicytemplate_factory.go @@ -1,86 +1,214 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( - "time" - - "github.com/onsi/gomega" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" dataprotectionv1alpha1 "github.com/apecloud/kubeblocks/apis/dataprotection/v1alpha1" ) type MockBackupPolicyTemplateFactory struct { - BaseFactory[dataprotectionv1alpha1.BackupPolicyTemplate, *dataprotectionv1alpha1.BackupPolicyTemplate, MockBackupPolicyTemplateFactory] + BaseFactory[appsv1alpha1.BackupPolicyTemplate, *appsv1alpha1.BackupPolicyTemplate, MockBackupPolicyTemplateFactory] + backupType dataprotectionv1alpha1.BackupType } func NewBackupPolicyTemplateFactory(name string) *MockBackupPolicyTemplateFactory { f := &MockBackupPolicyTemplateFactory{} f.init("", name, - &dataprotectionv1alpha1.BackupPolicyTemplate{ - Spec: dataprotectionv1alpha1.BackupPolicyTemplateSpec{ - Hooks: &dataprotectionv1alpha1.BackupPolicyHook{}, - }, - }, f) + &appsv1alpha1.BackupPolicyTemplate{}, + f) return f } -func (factory *MockBackupPolicyTemplateFactory) SetBackupToolName(backupToolName string) *MockBackupPolicyTemplateFactory { - factory.get().Spec.BackupToolName = backupToolName +func (factory *MockBackupPolicyTemplateFactory) SetClusterDefRef(clusterDefRef string) *MockBackupPolicyTemplateFactory { + factory.get().Spec.ClusterDefRef = clusterDefRef return factory } -func (factory *MockBackupPolicyTemplateFactory) SetSchedule(schedule string) *MockBackupPolicyTemplateFactory { - factory.get().Spec.Schedule = schedule +func (factory *MockBackupPolicyTemplateFactory) getLastBackupPolicy() *appsv1alpha1.BackupPolicy { + l := len(factory.get().Spec.BackupPolicies) + if l == 0 { + return nil + } + backupPolicies := factory.get().Spec.BackupPolicies + return &backupPolicies[l-1] +} + +func (factory *MockBackupPolicyTemplateFactory) AddBackupPolicy(componentDef string) *MockBackupPolicyTemplateFactory { + factory.get().Spec.BackupPolicies = append(factory.get().Spec.BackupPolicies, appsv1alpha1.BackupPolicy{ + ComponentDefRef: componentDef, + }) return factory } func (factory *MockBackupPolicyTemplateFactory) SetTTL(duration string) *MockBackupPolicyTemplateFactory { - du, err := time.ParseDuration(duration) - gomega.Expect(err).Should(gomega.Succeed()) + factory.getLastBackupPolicy().Retention = &appsv1alpha1.RetentionSpec{ + TTL: &duration, + } + return factory +} - var d metav1.Duration - d.Duration = du - factory.get().Spec.TTL = &d +func (factory *MockBackupPolicyTemplateFactory) setBasePolicyField(setField func(basePolicy *appsv1alpha1.BasePolicy)) { + backupPolicy := factory.getLastBackupPolicy() + var basePolicy *appsv1alpha1.BasePolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeDataFile: + basePolicy = &backupPolicy.Datafile.BasePolicy + case dataprotectionv1alpha1.BackupTypeLogFile: + basePolicy = &backupPolicy.Logfile.BasePolicy + case dataprotectionv1alpha1.BackupTypeSnapshot: + basePolicy = &backupPolicy.Snapshot.BasePolicy + } + if basePolicy == nil { + // ignore + return + } + setField(basePolicy) +} + +func (factory *MockBackupPolicyTemplateFactory) setCommonPolicyField(setField func(commonPolicy *appsv1alpha1.CommonBackupPolicy)) { + backupPolicy := factory.getLastBackupPolicy() + var commonPolicy *appsv1alpha1.CommonBackupPolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeDataFile: + commonPolicy = backupPolicy.Datafile + case dataprotectionv1alpha1.BackupTypeLogFile: + commonPolicy = backupPolicy.Logfile + } + if commonPolicy == nil { + // ignore + return + } + setField(commonPolicy) +} + +func (factory *MockBackupPolicyTemplateFactory) setScheduleField(setField func(schedulePolicy *appsv1alpha1.SchedulePolicy)) { + backupPolicy := factory.getLastBackupPolicy() + var schedulePolicy *appsv1alpha1.SchedulePolicy + switch factory.backupType { + case dataprotectionv1alpha1.BackupTypeSnapshot: + backupPolicy.Schedule.Snapshot = &appsv1alpha1.SchedulePolicy{} + schedulePolicy = backupPolicy.Schedule.Snapshot + case dataprotectionv1alpha1.BackupTypeDataFile: + backupPolicy.Schedule.Datafile = &appsv1alpha1.SchedulePolicy{} + schedulePolicy = backupPolicy.Schedule.Datafile + case dataprotectionv1alpha1.BackupTypeLogFile: + backupPolicy.Schedule.Logfile = &appsv1alpha1.SchedulePolicy{} + schedulePolicy = backupPolicy.Schedule.Logfile + } + if schedulePolicy == nil { + // ignore + return + } + setField(schedulePolicy) +} + +func (factory *MockBackupPolicyTemplateFactory) AddSnapshotPolicy() *MockBackupPolicyTemplateFactory { + backupPolicy := factory.getLastBackupPolicy() + backupPolicy.Snapshot = &appsv1alpha1.SnapshotPolicy{ + Hooks: &appsv1alpha1.BackupPolicyHook{}, + } + factory.backupType = dataprotectionv1alpha1.BackupTypeSnapshot + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) AddDatafilePolicy() *MockBackupPolicyTemplateFactory { + backupPolicy := factory.getLastBackupPolicy() + backupPolicy.Datafile = &appsv1alpha1.CommonBackupPolicy{} + factory.backupType = dataprotectionv1alpha1.BackupTypeDataFile + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) AddIncrementalPolicy() *MockBackupPolicyTemplateFactory { + backupPolicy := factory.getLastBackupPolicy() + backupPolicy.Logfile = &appsv1alpha1.CommonBackupPolicy{} + factory.backupType = dataprotectionv1alpha1.BackupTypeLogFile return factory } func (factory *MockBackupPolicyTemplateFactory) SetHookContainerName(containerName string) *MockBackupPolicyTemplateFactory { - factory.get().Spec.Hooks.ContainerName = containerName + backupPolicy := factory.getLastBackupPolicy() + if backupPolicy.Snapshot == nil { + return factory + } + backupPolicy.Snapshot.Hooks.ContainerName = containerName return factory } func (factory *MockBackupPolicyTemplateFactory) AddHookPreCommand(preCommand string) *MockBackupPolicyTemplateFactory { - preCommands := &factory.get().Spec.Hooks.PreCommands + backupPolicy := factory.getLastBackupPolicy() + if backupPolicy.Snapshot == nil { + return factory + } + preCommands := &backupPolicy.Snapshot.Hooks.PreCommands *preCommands = append(*preCommands, preCommand) return factory } func (factory *MockBackupPolicyTemplateFactory) AddHookPostCommand(postCommand string) *MockBackupPolicyTemplateFactory { - postCommands := &factory.get().Spec.Hooks.PostCommands + backupPolicy := factory.getLastBackupPolicy() + if backupPolicy.Snapshot == nil { + return factory + } + postCommands := &backupPolicy.Snapshot.Hooks.PostCommands *postCommands = append(*postCommands, postCommand) return factory } -func (factory *MockBackupPolicyTemplateFactory) SetCredentialKeyword(userKeyword, passwdKeyword string) *MockBackupPolicyTemplateFactory { - factory.get().Spec.CredentialKeyword = &dataprotectionv1alpha1.BackupPolicyCredentialKeyword{ - UserKeyword: userKeyword, - PasswordKeyword: passwdKeyword, - } +func (factory *MockBackupPolicyTemplateFactory) SetSchedule(schedule string, enable bool) *MockBackupPolicyTemplateFactory { + factory.setScheduleField(func(schedulePolicy *appsv1alpha1.SchedulePolicy) { + schedulePolicy.Enable = enable + schedulePolicy.CronExpression = schedule + }) + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) SetBackupsHistoryLimit(backupsHistoryLimit int32) *MockBackupPolicyTemplateFactory { + factory.setBasePolicyField(func(basePolicy *appsv1alpha1.BasePolicy) { + basePolicy.BackupsHistoryLimit = backupsHistoryLimit + }) + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) SetBackupToolName(backupToolName string) *MockBackupPolicyTemplateFactory { + factory.setCommonPolicyField(func(commonPolicy *appsv1alpha1.CommonBackupPolicy) { + commonPolicy.BackupToolName = backupToolName + }) + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) SetTargetRole(role string) *MockBackupPolicyTemplateFactory { + factory.setBasePolicyField(func(basePolicy *appsv1alpha1.BasePolicy) { + basePolicy.Target.Role = role + }) + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) SetTargetAccount(account string) *MockBackupPolicyTemplateFactory { + factory.setBasePolicyField(func(basePolicy *appsv1alpha1.BasePolicy) { + basePolicy.Target.Account = account + }) + return factory +} + +func (factory *MockBackupPolicyTemplateFactory) SetLabels(labels map[string]string) *MockBackupPolicyTemplateFactory { + factory.get().SetLabels(labels) return factory } diff --git a/internal/testutil/apps/base_factory.go b/internal/testutil/apps/base_factory.go index 722c48291..e97c64684 100644 --- a/internal/testutil/apps/base_factory.go +++ b/internal/testutil/apps/base_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/internal/testutil/apps/cluster_consensus_test_util.go b/internal/testutil/apps/cluster_consensus_test_util.go index 1f5c4a3a1..5a4a048a6 100644 --- a/internal/testutil/apps/cluster_consensus_test_util.go +++ b/internal/testutil/apps/cluster_consensus_test_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -31,15 +34,16 @@ import ( ) const ( - errorLogName = "error" - leader = "leader" - follower = "follower" - learner = "learner" + errorLogName = "error" + leader = "leader" + follower = "follower" + learner = "learner" + ConsensusReplicas = 3 ) // InitConsensusMysql initializes a cluster environment which only contains a component of ConsensusSet type for testing, // includes ClusterDefinition/ClusterVersion/Cluster resources. -func InitConsensusMysql(testCtx testutil.TestContext, +func InitConsensusMysql(testCtx *testutil.TestContext, clusterDefName, clusterVersionName, clusterName, @@ -53,44 +57,48 @@ func InitConsensusMysql(testCtx testutil.TestContext, // CreateConsensusMysqlCluster creates a mysql cluster with a component of ConsensusSet type. func CreateConsensusMysqlCluster( - testCtx testutil.TestContext, + testCtx *testutil.TestContext, clusterDefName, clusterVersionName, clusterName, workloadType, - consensusCompName string) *appsv1alpha1.Cluster { - pvcSpec := NewPVCSpec("2Gi") + consensusCompName string, pvcSize ...string) *appsv1alpha1.Cluster { + size := "2Gi" + if len(pvcSize) > 0 { + size = pvcSize[0] + } + pvcSpec := NewPVCSpec(size) return NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(consensusCompName, workloadType).SetReplicas(3).SetEnabledLogs(errorLogName). - AddVolumeClaimTemplate("data", pvcSpec).Create(&testCtx).GetObject() + AddComponent(consensusCompName, workloadType).SetReplicas(ConsensusReplicas).SetEnabledLogs(errorLogName). + AddVolumeClaimTemplate("data", pvcSpec).Create(testCtx).GetObject() } // CreateConsensusMysqlClusterDef creates a mysql clusterDefinition with a component of ConsensusSet type. -func CreateConsensusMysqlClusterDef(testCtx testutil.TestContext, clusterDefName, workloadType string) *appsv1alpha1.ClusterDefinition { +func CreateConsensusMysqlClusterDef(testCtx *testutil.TestContext, clusterDefName, componentDefName string) *appsv1alpha1.ClusterDefinition { filePathPattern := "/data/mysql/log/mysqld.err" - return NewClusterDefFactory(clusterDefName).AddComponent(ConsensusMySQLComponent, workloadType). - AddLogConfig(errorLogName, filePathPattern).Create(&testCtx).GetObject() + return NewClusterDefFactory(clusterDefName).AddComponentDef(ConsensusMySQLComponent, componentDefName). + AddLogConfig(errorLogName, filePathPattern).Create(testCtx).GetObject() } // CreateConsensusMysqlClusterVersion creates a mysql clusterVersion with a component of ConsensusSet type. -func CreateConsensusMysqlClusterVersion(testCtx testutil.TestContext, clusterDefName, clusterVersionName, workloadType string) *appsv1alpha1.ClusterVersion { - return NewClusterVersionFactory(clusterVersionName, clusterDefName).AddComponent(workloadType).AddContainerShort("mysql", ApeCloudMySQLImage). - Create(&testCtx).GetObject() +func CreateConsensusMysqlClusterVersion(testCtx *testutil.TestContext, clusterDefName, clusterVersionName, workloadType string) *appsv1alpha1.ClusterVersion { + return NewClusterVersionFactory(clusterVersionName, clusterDefName).AddComponentVersion(workloadType).AddContainerShort("mysql", ApeCloudMySQLImage). + Create(testCtx).GetObject() } // MockConsensusComponentStatefulSet mocks the component statefulSet, just using in envTest func MockConsensusComponentStatefulSet( - testCtx testutil.TestContext, + testCtx *testutil.TestContext, clusterName, consensusCompName string) *appsv1.StatefulSet { stsName := clusterName + "-" + consensusCompName - return NewStatefulSetFactory(testCtx.DefaultNamespace, stsName, clusterName, consensusCompName).SetReplicas(int32(3)). - AddContainer(corev1.Container{Name: DefaultMySQLContainerName, Image: ApeCloudMySQLImage}).Create(&testCtx).GetObject() + return NewStatefulSetFactory(testCtx.DefaultNamespace, stsName, clusterName, consensusCompName).SetReplicas(ConsensusReplicas). + AddContainer(corev1.Container{Name: DefaultMySQLContainerName, Image: ApeCloudMySQLImage}).Create(testCtx).GetObject() } // MockConsensusComponentStsPod mocks to create the pod of the consensus StatefulSet, just using in envTest func MockConsensusComponentStsPod( - testCtx testutil.TestContext, + testCtx *testutil.TestContext, sts *appsv1.StatefulSet, clusterName, consensusCompName, @@ -109,7 +117,7 @@ func MockConsensusComponentStsPod( AddConsensusSetAccessModeLabel(accessMode). AddControllerRevisionHashLabel(stsUpdateRevision). AddContainer(corev1.Container{Name: DefaultMySQLContainerName, Image: ApeCloudMySQLImage}). - Create(&testCtx).GetObject() + CheckedCreate(testCtx).GetObject() patch := client.MergeFrom(pod.DeepCopy()) pod.Status.Conditions = []corev1.PodCondition{ { @@ -123,12 +131,12 @@ func MockConsensusComponentStsPod( // MockConsensusComponentPods mocks the component pods, just using in envTest func MockConsensusComponentPods( - testCtx testutil.TestContext, + testCtx *testutil.TestContext, sts *appsv1.StatefulSet, clusterName, consensusCompName string) []*corev1.Pod { - podList := make([]*corev1.Pod, 3) - for i := 0; i < 3; i++ { + podList := make([]*corev1.Pod, ConsensusReplicas) + for i := 0; i < ConsensusReplicas; i++ { podName := fmt.Sprintf("%s-%s-%d", clusterName, consensusCompName, i) podRole := "follower" accessMode := "Readonly" diff --git a/internal/testutil/apps/cluster_factory.go b/internal/testutil/apps/cluster_factory.go index d6b517243..bc2e5fa31 100644 --- a/internal/testutil/apps/cluster_factory.go +++ b/internal/testutil/apps/cluster_factory.go @@ -1,25 +1,32 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( + "time" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" ) type MockClusterFactory struct { @@ -55,10 +62,10 @@ func (factory *MockClusterFactory) AddClusterToleration(toleration corev1.Tolera return factory } -func (factory *MockClusterFactory) AddComponent(compName string, compType string) *MockClusterFactory { +func (factory *MockClusterFactory) AddComponent(compName string, compDefName string) *MockClusterFactory { comp := appsv1alpha1.ClusterComponentSpec{ Name: compName, - ComponentDefRef: compType, + ComponentDefRef: compDefName, } factory.get().Spec.ComponentSpecs = append(factory.get().Spec.ComponentSpecs, comp) return factory @@ -73,6 +80,14 @@ func (factory *MockClusterFactory) SetReplicas(replicas int32) *MockClusterFacto return factory } +func (factory *MockClusterFactory) SetServiceAccountName(serviceAccountName string) *MockClusterFactory { + comps := factory.get().Spec.ComponentSpecs + if len(comps) > 0 { + comps[len(comps)-1].ServiceAccountName = serviceAccountName + } + return factory +} + func (factory *MockClusterFactory) SetResources(resources corev1.ResourceRequirements) *MockClusterFactory { comps := factory.get().Spec.ComponentSpecs if len(comps) > 0 { @@ -100,6 +115,15 @@ func (factory *MockClusterFactory) SetEnabledLogs(logName ...string) *MockCluste return factory } +func (factory *MockClusterFactory) SetClassDefRef(classDefRef *appsv1alpha1.ClassDefRef) *MockClusterFactory { + comps := factory.get().Spec.ComponentSpecs + if len(comps) > 0 { + comps[len(comps)-1].ClassDefRef = classDefRef + } + factory.get().Spec.ComponentSpecs = comps + return factory +} + func (factory *MockClusterFactory) AddComponentToleration(toleration corev1.Toleration) *MockClusterFactory { comps := factory.get().Spec.ComponentSpecs if len(comps) > 0 { @@ -191,3 +215,15 @@ func (factory *MockClusterFactory) AddService(serviceName string, serviceType co factory.get().Spec.ComponentSpecs = comps return factory } + +func (factory *MockClusterFactory) AddRestorePointInTime(restoreTime metav1.Time, sourceCluster string) *MockClusterFactory { + annotations := factory.get().Annotations + if annotations == nil { + annotations = map[string]string{} + } + annotations[constant.RestoreFromTimeAnnotationKey] = restoreTime.Format(time.RFC3339) + annotations[constant.RestoreFromSrcClusterAnnotationKey] = sourceCluster + + factory.get().Annotations = annotations + return factory +} diff --git a/internal/testutil/apps/cluster_replication_test_util.go b/internal/testutil/apps/cluster_replication_test_util.go index 924d4b4ee..296952db5 100644 --- a/internal/testutil/apps/cluster_replication_test_util.go +++ b/internal/testutil/apps/cluster_replication_test_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -30,8 +33,9 @@ import ( "github.com/apecloud/kubeblocks/internal/testutil" ) -// MockReplicationComponentStsPod mocks to create pod of the replication StatefulSet, just using in envTest -func MockReplicationComponentStsPod( +// MockReplicationComponentPod mocks to create pod of the replication StatefulSet, just using in envTest +func MockReplicationComponentPod( + g gomega.Gomega, testCtx testutil.TestContext, sts *appsv1.StatefulSet, clusterName, @@ -54,20 +58,35 @@ func MockReplicationComponentStsPod( Status: corev1.ConditionTrue, }, } - gomega.Expect(testCtx.Cli.Status().Patch(context.Background(), pod, patch)).Should(gomega.Succeed()) + if g != nil { + g.Expect(testCtx.Cli.Status().Patch(context.Background(), pod, patch)).Should(gomega.Succeed()) + } else { + gomega.Expect(testCtx.Cli.Status().Patch(context.Background(), pod, patch)).Should(gomega.Succeed()) + } return pod } -// MockReplicationComponentPods mocks to create pods of the component, just using in envTest +// MockReplicationComponentPods mocks to create pods of the component, just using in envTest. If roleByIdx is empty, +// will have implicit pod-0 being "primary" role and others to "secondary" role. func MockReplicationComponentPods( + g gomega.Gomega, testCtx testutil.TestContext, sts *appsv1.StatefulSet, clusterName, compName string, - podRole string) []*corev1.Pod { + roleByIdx map[int32]string) []*corev1.Pod { + var pods []*corev1.Pod - podName := fmt.Sprintf("%s-0", sts.Name) - pods = append(pods, MockReplicationComponentStsPod(testCtx, sts, clusterName, compName, podName, podRole)) + for i := int32(0); i < *sts.Spec.Replicas; i++ { + podName := fmt.Sprintf("%s-%d", sts.Name, i) + role := "secondary" + if podRole, ok := roleByIdx[i]; ok && podRole != "" { + role = podRole + } else if i == 0 { + role = "primary" + } + pods = append(pods, MockReplicationComponentPod(g, testCtx, sts, clusterName, compName, podName, role)) + } return pods } diff --git a/internal/testutil/apps/cluster_stateless_test_util.go b/internal/testutil/apps/cluster_stateless_test_util.go index 4dcb35e13..45f8c0fce 100644 --- a/internal/testutil/apps/cluster_stateless_test_util.go +++ b/internal/testutil/apps/cluster_stateless_test_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -26,14 +29,14 @@ import ( ) // MockStatelessComponentDeploy mocks a deployment workload of the stateless component. -func MockStatelessComponentDeploy(testCtx testutil.TestContext, clusterName, componentName string) *appsv1.Deployment { +func MockStatelessComponentDeploy(testCtx *testutil.TestContext, clusterName, componentName string) *appsv1.Deployment { deployName := clusterName + "-" + componentName return NewDeploymentFactory(testCtx.DefaultNamespace, deployName, clusterName, componentName).SetMinReadySeconds(int32(10)).SetReplicas(int32(2)). - AddContainer(corev1.Container{Name: DefaultNginxContainerName, Image: NginxImage}).Create(&testCtx).GetObject() + AddContainer(corev1.Container{Name: DefaultNginxContainerName, Image: NginxImage}).Create(testCtx).GetObject() } // MockStatelessPod mocks the pods of the deployment workload. -func MockStatelessPod(testCtx testutil.TestContext, deploy *appsv1.Deployment, clusterName, componentName, podName string) *corev1.Pod { +func MockStatelessPod(testCtx *testutil.TestContext, deploy *appsv1.Deployment, clusterName, componentName, podName string) *corev1.Pod { var newRs *appsv1.ReplicaSet if deploy != nil { newRs = &appsv1.ReplicaSet{ @@ -49,5 +52,5 @@ func MockStatelessPod(testCtx testutil.TestContext, deploy *appsv1.Deployment, c AddAppComponentLabel(componentName). AddAppManangedByLabel(). AddContainer(corev1.Container{Name: DefaultNginxContainerName, Image: NginxImage}). - Create(&testCtx).GetObject() + Create(testCtx).GetObject() } diff --git a/internal/testutil/apps/cluster_util.go b/internal/testutil/apps/cluster_util.go index 94dc37d16..a5fbcde66 100644 --- a/internal/testutil/apps/cluster_util.go +++ b/internal/testutil/apps/cluster_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -31,53 +34,55 @@ import ( // InitClusterWithHybridComps initializes a cluster environment for testing, includes ClusterDefinition/ClusterVersion/Cluster resources. func InitClusterWithHybridComps( - testCtx testutil.TestContext, + testCtx *testutil.TestContext, clusterDefName, clusterVersionName, clusterName, - statelessComp, - statefulComp, - consensusComp string) (*appsv1alpha1.ClusterDefinition, *appsv1alpha1.ClusterVersion, *appsv1alpha1.Cluster) { + statelessCompDefName, + statefulCompDefName, + consensusCompDefName string) (*appsv1alpha1.ClusterDefinition, *appsv1alpha1.ClusterVersion, *appsv1alpha1.Cluster) { clusterDef := NewClusterDefFactory(clusterDefName). - AddComponent(StatelessNginxComponent, statelessComp). - AddComponent(ConsensusMySQLComponent, consensusComp). - AddComponent(StatefulMySQLComponent, statefulComp). - Create(&testCtx).GetObject() + AddComponentDef(StatelessNginxComponent, statelessCompDefName). + AddComponentDef(ConsensusMySQLComponent, consensusCompDefName). + AddComponentDef(StatefulMySQLComponent, statefulCompDefName). + Create(testCtx).GetObject() clusterVersion := NewClusterVersionFactory(clusterVersionName, clusterDefName). - AddComponent(statelessComp).AddContainerShort(DefaultNginxContainerName, NginxImage). - AddComponent(consensusComp).AddContainerShort(DefaultMySQLContainerName, NginxImage). - AddComponent(statefulComp).AddContainerShort(DefaultMySQLContainerName, NginxImage). - Create(&testCtx).GetObject() + AddComponentVersion(statelessCompDefName).AddContainerShort(DefaultNginxContainerName, NginxImage). + AddComponentVersion(consensusCompDefName).AddContainerShort(DefaultMySQLContainerName, NginxImage). + AddComponentVersion(statefulCompDefName).AddContainerShort(DefaultMySQLContainerName, NginxImage). + Create(testCtx).GetObject() + pvcSpec := NewPVCSpec("1Gi") cluster := NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefName, clusterVersionName). - AddComponent(statelessComp, statelessComp).SetReplicas(1). - AddComponent(consensusComp, consensusComp).SetReplicas(3). - AddComponent(statefulComp, statefulComp).SetReplicas(3). - Create(&testCtx).GetObject() + AddComponent(statelessCompDefName, statelessCompDefName).SetReplicas(1). + AddComponent(consensusCompDefName, consensusCompDefName).SetReplicas(3). + AddComponent(statefulCompDefName, statefulCompDefName).SetReplicas(3). + AddVolumeClaimTemplate(DataVolumeName, pvcSpec). + Create(testCtx).GetObject() return clusterDef, clusterVersion, cluster } -func CreateK8sResource(testCtx testutil.TestContext, obj client.Object) client.Object { +func CreateK8sResource(testCtx *testutil.TestContext, obj client.Object) client.Object { gomega.Expect(testCtx.CreateObj(testCtx.Ctx, obj)).Should(gomega.Succeed()) // wait until cluster created - gomega.Eventually(CheckObjExists(&testCtx, client.ObjectKeyFromObject(obj), + gomega.Eventually(CheckObjExists(testCtx, client.ObjectKeyFromObject(obj), obj, true)).Should(gomega.Succeed()) return obj } -func CheckedCreateK8sResource(testCtx testutil.TestContext, obj client.Object) client.Object { +func CheckedCreateK8sResource(testCtx *testutil.TestContext, obj client.Object) client.Object { gomega.Expect(testCtx.CheckedCreateObj(testCtx.Ctx, obj)).Should(gomega.Succeed()) // wait until cluster created - gomega.Eventually(CheckObjExists(&testCtx, client.ObjectKeyFromObject(obj), + gomega.Eventually(CheckObjExists(testCtx, client.ObjectKeyFromObject(obj), obj, true)).Should(gomega.Succeed()) return obj } // GetClusterComponentPhase gets the component phase of testing cluster for verification. -func GetClusterComponentPhase(testCtx testutil.TestContext, clusterName, componentName string) func(g gomega.Gomega) appsv1alpha1.ClusterComponentPhase { +func GetClusterComponentPhase(testCtx *testutil.TestContext, clusterKey types.NamespacedName, componentName string) func(g gomega.Gomega) appsv1alpha1.ClusterComponentPhase { return func(g gomega.Gomega) appsv1alpha1.ClusterComponentPhase { tmpCluster := &appsv1alpha1.Cluster{} - g.Expect(testCtx.Cli.Get(context.Background(), client.ObjectKey{Name: clusterName, - Namespace: testCtx.DefaultNamespace}, tmpCluster)).Should(gomega.Succeed()) + g.Expect(testCtx.Cli.Get(context.Background(), client.ObjectKey{Name: clusterKey.Name, + Namespace: clusterKey.Namespace}, tmpCluster)).Should(gomega.Succeed()) return tmpCluster.Status.Components[componentName].Phase } } @@ -91,6 +96,15 @@ func GetClusterPhase(testCtx *testutil.TestContext, clusterKey types.NamespacedN } } +// GetClusterGeneration gets the testing cluster's metadata.generation. +func GetClusterGeneration(testCtx *testutil.TestContext, clusterKey types.NamespacedName) func(gomega.Gomega) int64 { + return func(g gomega.Gomega) int64 { + cluster := &appsv1alpha1.Cluster{} + g.Expect(testCtx.Cli.Get(testCtx.Ctx, clusterKey, cluster)).Should(gomega.Succeed()) + return cluster.GetGeneration() + } +} + // GetClusterObservedGeneration gets the testing cluster's ObservedGeneration in status for verification. func GetClusterObservedGeneration(testCtx *testutil.TestContext, clusterKey types.NamespacedName) func(gomega.Gomega) int64 { return func(g gomega.Gomega) int64 { @@ -100,7 +114,7 @@ func GetClusterObservedGeneration(testCtx *testutil.TestContext, clusterKey type } } -// NewPVCSpec create appsv1alpha1.PersistentVolumeClaimSpec. +// NewPVCSpec creates appsv1alpha1.PersistentVolumeClaimSpec. func NewPVCSpec(size string) appsv1alpha1.PersistentVolumeClaimSpec { return appsv1alpha1.PersistentVolumeClaimSpec{ AccessModes: []corev1.PersistentVolumeAccessMode{corev1.ReadWriteOnce}, diff --git a/internal/testutil/apps/clusterdef_factory.go b/internal/testutil/apps/clusterdef_factory.go index 7c8add98f..7ee0764b6 100644 --- a/internal/testutil/apps/clusterdef_factory.go +++ b/internal/testutil/apps/clusterdef_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -22,13 +25,13 @@ import ( appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" ) -type ComponentTplType string +type ComponentDefTplType string const ( - StatefulMySQLComponent ComponentTplType = "stateful-mysql" - ConsensusMySQLComponent ComponentTplType = "consensus-mysql" - ReplicationRedisComponent ComponentTplType = "replication-redis" - StatelessNginxComponent ComponentTplType = "stateless-nginx" + StatefulMySQLComponent ComponentDefTplType = "stateful-mysql" + ConsensusMySQLComponent ComponentDefTplType = "consensus-mysql" + ReplicationRedisComponent ComponentDefTplType = "replication-redis" + StatelessNginxComponent ComponentDefTplType = "stateless-nginx" ) type MockClusterDefFactory struct { @@ -49,12 +52,12 @@ func NewClusterDefFactory(name string) *MockClusterDefFactory { func NewClusterDefFactoryWithConnCredential(name string) *MockClusterDefFactory { f := NewClusterDefFactory(name) - f.AddComponent(StatefulMySQLComponent, "conn-cred") + f.AddComponentDef(StatefulMySQLComponent, "conn-cred") f.SetConnectionCredential(defaultConnectionCredential, &defaultSvcSpec) return f } -func (factory *MockClusterDefFactory) AddComponent(tplType ComponentTplType, newName string) *MockClusterDefFactory { +func (factory *MockClusterDefFactory) AddComponentDef(tplType ComponentDefTplType, compDefName string) *MockClusterDefFactory { var component *appsv1alpha1.ClusterComponentDefinition switch tplType { case StatefulMySQLComponent: @@ -68,7 +71,7 @@ func (factory *MockClusterDefFactory) AddComponent(tplType ComponentTplType, new } factory.get().Spec.ComponentDefs = append(factory.get().Spec.ComponentDefs, *component) comp := factory.getLastCompDef() - comp.Name = newName + comp.Name = compDefName return factory } @@ -218,7 +221,7 @@ func (factory *MockClusterDefFactory) AddContainerVolumeMounts(containerName str return factory } -func (factory *MockClusterDefFactory) AddReplicationSpec(replicationSpec *appsv1alpha1.ReplicationSpec) *MockClusterDefFactory { +func (factory *MockClusterDefFactory) AddReplicationSpec(replicationSpec *appsv1alpha1.ReplicationSetSpec) *MockClusterDefFactory { comp := factory.getLastCompDef() if comp == nil { return factory diff --git a/internal/testutil/apps/clusterversion_factory.go b/internal/testutil/apps/clusterversion_factory.go index 3f506d557..3ed7d3052 100644 --- a/internal/testutil/apps/clusterversion_factory.go +++ b/internal/testutil/apps/clusterversion_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -38,9 +41,9 @@ func NewClusterVersionFactory(name, cdRef string) *MockClusterVersionFactory { return f } -func (factory *MockClusterVersionFactory) AddComponent(compType string) *MockClusterVersionFactory { +func (factory *MockClusterVersionFactory) AddComponentVersion(compDefName string) *MockClusterVersionFactory { comp := appsv1alpha1.ClusterComponentVersion{ - ComponentDefRef: compType, + ComponentDefRef: compDefName, } factory.get().Spec.ComponentVersions = append(factory.get().Spec.ComponentVersions, comp) return factory diff --git a/internal/testutil/apps/common_util.go b/internal/testutil/apps/common_util.go index 19d91b701..1e56df7ab 100644 --- a/internal/testutil/apps/common_util.go +++ b/internal/testutil/apps/common_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -49,7 +52,7 @@ func ResetToIgnoreFinalizers() { // REVIEW: adding following is a hack, if tests are running as // controller-runtime manager setup. constant.ConfigurationTemplateFinalizerName, - "cluster.kubeblocks.io/finalizer", + constant.DBClusterFinalizerName, } } @@ -61,9 +64,9 @@ func ResetToIgnoreFinalizers() { // })).Should(Succeed()) func ChangeObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext, - pobj PT, action func()) error { + pobj PT, action func(PT)) error { patch := client.MergeFrom(PT(pobj.DeepCopy())) - action() + action(pobj) return testCtx.Cli.Patch(testCtx.Ctx, pobj, patch) } @@ -95,7 +98,9 @@ func GetAndChangeObj[T intctrlutil.Object, PT intctrlutil.PObject[T]]( if err := testCtx.Cli.Get(testCtx.Ctx, namespacedName, pobj); err != nil { return err } - return ChangeObj(testCtx, pobj, func() { action(pobj) }) + return ChangeObj(testCtx, pobj, func(lobj PT) { + action(lobj) + }) } } @@ -147,14 +152,13 @@ func CheckObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil // Helper functions to check fields of resource lists when writing unit tests. -func GetListLen[T intctrlutil.Object, PT intctrlutil.PObject[T], +func List[T intctrlutil.Object, PT intctrlutil.PObject[T], L intctrlutil.ObjList[T], PL intctrlutil.PObjList[T, L]]( - testCtx *testutil.TestContext, _ func(T, L), opt ...client.ListOption) func(gomega.Gomega) int { - return func(g gomega.Gomega) int { + testCtx *testutil.TestContext, _ func(T, L), opt ...client.ListOption) func(gomega.Gomega) []T { + return func(g gomega.Gomega) []T { var objList L g.Expect(testCtx.Cli.List(testCtx.Ctx, PL(&objList), opt...)).To(gomega.Succeed()) - items := reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T) - return len(items) + return reflect.ValueOf(&objList).Elem().FieldByName("Items").Interface().([]T) } } @@ -260,13 +264,13 @@ func NewCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]]( func CreateCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext, filePath string, pobj PT, actions ...any) PT { pobj = NewCustomizedObj(filePath, pobj, actions...) - return CreateK8sResource(*testCtx, pobj).(PT) + return CreateK8sResource(testCtx, pobj).(PT) } func CheckedCreateCustomizedObj[T intctrlutil.Object, PT intctrlutil.PObject[T]](testCtx *testutil.TestContext, filePath string, pobj PT, actions ...any) PT { pobj = NewCustomizedObj(filePath, pobj, actions...) - return CheckedCreateK8sResource(*testCtx, pobj).(PT) + return CheckedCreateK8sResource(testCtx, pobj).(PT) } // Helper functions to delete object. @@ -322,7 +326,7 @@ func ClearResourcesWithRemoveFinalizerOption[T intctrlutil.Object, PT intctrluti finalizers := pobj.GetFinalizers() if len(finalizers) > 0 { if removeFinalizer { - g.Expect(ChangeObj(testCtx, pobj, func() { + g.Expect(ChangeObj(testCtx, pobj, func(lobj PT) { pobj.SetFinalizers([]string{}) })).To(gomega.Succeed()) } else { diff --git a/internal/testutil/apps/componentclassdefinition_factory.go b/internal/testutil/apps/componentclassdefinition_factory.go new file mode 100644 index 000000000..0b38b953d --- /dev/null +++ b/internal/testutil/apps/componentclassdefinition_factory.go @@ -0,0 +1,64 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" +) + +type MockComponentClassDefinitionFactory struct { + BaseFactory[appsv1alpha1.ComponentClassDefinition, *appsv1alpha1.ComponentClassDefinition, MockComponentClassDefinitionFactory] +} + +func NewComponentClassDefinitionFactory(name, clusterDefinitionRef, componentType string) *MockComponentClassDefinitionFactory { + f := &MockComponentClassDefinitionFactory{} + f.init("", name, &appsv1alpha1.ComponentClassDefinition{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + constant.ClassProviderLabelKey: "kubeblocks", + constant.ClusterDefLabelKey: clusterDefinitionRef, + constant.KBAppComponentDefRefLabelKey: componentType, + }, + }, + }, f) + return f +} + +func (factory *MockComponentClassDefinitionFactory) AddClasses(constraintRef string, classNames []string) *MockComponentClassDefinitionFactory { + var classes []appsv1alpha1.ComponentClass + for _, name := range classNames { + classes = append(classes, DefaultClasses[name]) + } + groups := factory.get().Spec.Groups + groups = append(groups, appsv1alpha1.ComponentClassGroup{ + ResourceConstraintRef: constraintRef, + Series: []appsv1alpha1.ComponentClassSeries{ + { + Classes: classes, + }, + }, + }) + factory.get().Spec.Groups = groups + return factory +} diff --git a/internal/testutil/apps/componentresourceconstraint_factory.go b/internal/testutil/apps/componentresourceconstraint_factory.go new file mode 100644 index 000000000..288e7327f --- /dev/null +++ b/internal/testutil/apps/componentresourceconstraint_factory.go @@ -0,0 +1,102 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package apps + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/yaml" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +type ResourceConstraintTplType string + +const ( + GeneralResourceConstraint ResourceConstraintTplType = "general" + MemoryOptimizedResourceConstraint ResourceConstraintTplType = "memory-optimized" + + generalResourceConstraintTemplate = ` +- cpu: + min: 0.5 + max: 2 + step: 0.5 + memory: + sizePerCPU: 1Gi +- cpu: + min: 2 + max: 2 + memory: + sizePerCPU: 2Gi +- cpu: + slots: [2, 4, 8, 16, 24, 32, 48, 64, 96, 128] + memory: + sizePerCPU: 4Gi +` + + memoryResourceConstraintTemplate = ` +- cpu: + slots: [2, 4, 8, 12, 24, 48] + memory: + sizePerCPU: 8Gi +- cpu: + min: 2 + max: 128 + step: 2 + memory: + sizePerCPU: 16Gi +` +) + +type MockComponentResourceConstraintFactory struct { + BaseFactory[appsv1alpha1.ComponentResourceConstraint, *appsv1alpha1.ComponentResourceConstraint, MockComponentResourceConstraintFactory] +} + +func NewComponentResourceConstraintFactory(name string) *MockComponentResourceConstraintFactory { + f := &MockComponentResourceConstraintFactory{} + f.init("", name, &appsv1alpha1.ComponentResourceConstraint{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Labels: map[string]string{ + "resourceconstraint.kubeblocks.io/provider": "kubeblocks", + }, + }, + }, f) + return f +} + +func (factory *MockComponentResourceConstraintFactory) AddConstraints(constraintTplType ResourceConstraintTplType) *MockComponentResourceConstraintFactory { + var ( + tpl string + newConstraints []appsv1alpha1.ResourceConstraint + constraints = factory.get().Spec.Constraints + ) + switch constraintTplType { + case GeneralResourceConstraint: + tpl = generalResourceConstraintTemplate + case MemoryOptimizedResourceConstraint: + tpl = memoryResourceConstraintTemplate + } + if err := yaml.Unmarshal([]byte(tpl), &newConstraints); err != nil { + panic(err) + } + constraints = append(constraints, newConstraints...) + factory.get().Spec.Constraints = constraints + return factory +} diff --git a/internal/testutil/apps/constant.go b/internal/testutil/apps/constant.go index 40c805f62..80b84cc57 100644 --- a/internal/testutil/apps/constant.go +++ b/internal/testutil/apps/constant.go @@ -1,23 +1,27 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps import ( corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/intstr" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" @@ -32,8 +36,8 @@ const ( ScriptsVolumeName = "scripts" ServiceDefaultName = "" ServiceHeadlessName = "headless" - ServiceVPCName = "a-vpc-lb-service-for-app" - ServiceInternetName = "a-internet-lb-service-for-app" + ServiceVPCName = "vpc-lb" + ServiceInternetName = "internet-lb" ReplicationPodRoleVolume = "pod-role" ReplicationRoleLabelFieldPath = "metadata.labels['kubeblocks.io/role']" @@ -48,11 +52,15 @@ const ( DefaultNginxContainerName = "nginx" RedisType = "state.redis" - DefaultRedisCompType = "redis" + DefaultRedisCompDefName = "redis" DefaultRedisCompName = "redis-rsts" DefaultRedisImageName = "redis:7.0.5" DefaultRedisContainerName = "redis" DefaultRedisInitContainerName = "redis-init-container" + + Class1c1gName = "general-1c1g" + Class2c4gName = "general-2c4g" + DefaultResourceConstraintName = "kb-resource-constraint" ) var ( @@ -61,7 +69,8 @@ var ( CharacterType: "stateless", PodSpec: &corev1.PodSpec{ Containers: []corev1.Container{{ - Name: DefaultNginxContainerName, + Name: DefaultNginxContainerName, + Image: NginxImage, }}, }, Service: &appsv1alpha1.ServiceSpec{ @@ -73,15 +82,16 @@ var ( } defaultConnectionCredential = map[string]string{ - "username": "root", - "SVC_FQDN": "$(SVC_FQDN)", - "RANDOM_PASSWD": "$(RANDOM_PASSWD)", - "tcpEndpoint": "tcp:$(SVC_FQDN):$(SVC_PORT_mysql)", - "paxosEndpoint": "paxos:$(SVC_FQDN):$(SVC_PORT_paxos)", - "UUID": "$(UUID)", - "UUID_B64": "$(UUID_B64)", - "UUID_STR_B64": "$(UUID_STR_B64)", - "UUID_HEX": "$(UUID_HEX)", + "username": "root", + "SVC_FQDN": "$(SVC_FQDN)", + "HEADLESS_SVC_FQDN": "$(HEADLESS_SVC_FQDN)", + "RANDOM_PASSWD": "$(RANDOM_PASSWD)", + "tcpEndpoint": "tcp:$(SVC_FQDN):$(SVC_PORT_mysql)", + "paxosEndpoint": "paxos:$(SVC_FQDN):$(SVC_PORT_paxos)", + "UUID": "$(UUID)", + "UUID_B64": "$(UUID_B64)", + "UUID_STR_B64": "$(UUID_STR_B64)", + "UUID_HEX": "$(UUID_HEX)", } // defaultSvc value are corresponding to defaultMySQLContainer.Ports name mapping and @@ -109,6 +119,7 @@ var ( defaultMySQLContainer = corev1.Container{ Name: DefaultMySQLContainerName, + Image: ApeCloudMySQLImage, ImagePullPolicy: corev1.PullIfNotPresent, Ports: []corev1.ContainerPort{ { @@ -141,7 +152,7 @@ var ( ValueFrom: &corev1.EnvVarSource{ SecretKeyRef: &corev1.SecretKeySelector{ LocalObjectReference: corev1.LocalObjectReference{ - Name: constant.ConnCredentialPlaceHolder, + Name: constant.KBConnCredentialPlaceHolder, }, Key: "password", }, @@ -172,7 +183,9 @@ var ( Name: "follower", AccessMode: appsv1alpha1.Readonly, }}, - UpdateStrategy: appsv1alpha1.BestEffortParallelStrategy, + StatefulSetSpec: appsv1alpha1.StatefulSetSpec{ + UpdateStrategy: appsv1alpha1.BestEffortParallelStrategy, + }, } defaultMySQLService = appsv1alpha1.ServiceSpec{ @@ -187,7 +200,7 @@ var ( CharacterType: "mysql", ConsensusSpec: &defaultConsensusSpec, Probes: &appsv1alpha1.ClusterDefinitionProbes{ - RoleChangedProbe: &appsv1alpha1.ClusterDefinitionProbe{ + RoleProbe: &appsv1alpha1.ClusterDefinitionProbe{ FailureThreshold: 3, PeriodSeconds: 1, TimeoutSeconds: 5, @@ -197,6 +210,10 @@ var ( PodSpec: &corev1.PodSpec{ Containers: []corev1.Container{defaultMySQLContainer}, }, + VolumeTypes: []appsv1alpha1.VolumeTypeSpec{{ + Name: DataVolumeName, + Type: appsv1alpha1.VolumeTypeData, + }}, } defaultRedisService = appsv1alpha1.ServiceSpec{ @@ -227,6 +244,7 @@ var ( defaultRedisInitContainer = corev1.Container{ Name: DefaultRedisInitContainerName, + Image: DefaultRedisImageName, ImagePullPolicy: corev1.PullIfNotPresent, VolumeMounts: defaultReplicationRedisVolumeMounts, Command: []string{"/scripts/init.sh"}, @@ -234,6 +252,7 @@ var ( defaultRedisContainer = corev1.Container{ Name: DefaultRedisContainerName, + Image: DefaultRedisImageName, ImagePullPolicy: corev1.PullIfNotPresent, Ports: []corev1.ContainerPort{ { @@ -257,6 +276,10 @@ var ( WorkloadType: appsv1alpha1.Replication, CharacterType: "redis", Service: &defaultRedisService, + VolumeTypes: []appsv1alpha1.VolumeTypeSpec{{ + Name: DataVolumeName, + Type: appsv1alpha1.VolumeTypeData, + }}, PodSpec: &corev1.PodSpec{ Volumes: []corev1.Volume{ { @@ -285,4 +308,21 @@ var ( Containers: []corev1.Container{defaultRedisContainer}, }, } + + Class1c1g = appsv1alpha1.ComponentClass{ + Name: Class1c1gName, + CPU: resource.MustParse("1"), + Memory: resource.MustParse("1Gi"), + } + + Class2c4g = appsv1alpha1.ComponentClass{ + Name: Class2c4gName, + CPU: resource.MustParse("2"), + Memory: resource.MustParse("4Gi"), + } + + DefaultClasses = map[string]appsv1alpha1.ComponentClass{ + Class1c1gName: Class1c1g, + Class2c4gName: Class2c4g, + } ) diff --git a/internal/testutil/apps/deployment_factoy.go b/internal/testutil/apps/deployment_factoy.go index 797df7615..75bcdf7e8 100644 --- a/internal/testutil/apps/deployment_factoy.go +++ b/internal/testutil/apps/deployment_factoy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/internal/testutil/apps/native_object_util.go b/internal/testutil/apps/native_object_util.go index e1c07ff1b..6afc84476 100644 --- a/internal/testutil/apps/native_object_util.go +++ b/internal/testutil/apps/native_object_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -65,7 +68,8 @@ func NewPVC(size string) corev1.PersistentVolumeClaimSpec { } } -func CreateStorageClass(testCtx testutil.TestContext, storageClassName string, allowVolumeExpansion bool) *storagev1.StorageClass { +func CreateStorageClass(testCtx *testutil.TestContext, storageClassName string, + allowVolumeExpansion bool) *storagev1.StorageClass { storageClass := &storagev1.StorageClass{ ObjectMeta: metav1.ObjectMeta{ Name: storageClassName, diff --git a/internal/testutil/apps/opsrequest_util.go b/internal/testutil/apps/opsrequest_util.go index c442e61c0..bef523ff5 100644 --- a/internal/testutil/apps/opsrequest_util.go +++ b/internal/testutil/apps/opsrequest_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -25,11 +28,12 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" + "github.com/apecloud/kubeblocks/internal/constant" "github.com/apecloud/kubeblocks/internal/testutil" ) -// CreateRestartOpsRequest creates a OpsRequest of restart type for testing. -func CreateRestartOpsRequest(testCtx testutil.TestContext, clusterName, opsRequestName string, componentNames []string) *appsv1alpha1.OpsRequest { +// CreateRestartOpsRequest creates an OpsRequest of restart type for testing. +func CreateRestartOpsRequest(testCtx *testutil.TestContext, clusterName, opsRequestName string, componentNames []string) *appsv1alpha1.OpsRequest { opsRequest := NewOpsRequestObj(opsRequestName, testCtx.DefaultNamespace, clusterName, appsv1alpha1.RestartType) componentList := make([]appsv1alpha1.ComponentOps, len(componentNames)) for i := range componentNames { @@ -45,6 +49,10 @@ func NewOpsRequestObj(opsRequestName, namespace, clusterName string, opsType app ObjectMeta: metav1.ObjectMeta{ Name: opsRequestName, Namespace: namespace, + Labels: map[string]string{ + constant.AppInstanceLabelKey: clusterName, + constant.OpsRequestTypeLabelKey: string(opsType), + }, }, Spec: appsv1alpha1.OpsRequestSpec{ ClusterRef: clusterName, @@ -61,7 +69,7 @@ func CreateOpsRequest(ctx context.Context, testCtx testutil.TestContext, opsRequ return opsRequest } -// GetOpsRequestCompPhase gets the component phase of testing OpsRequest for verification. +// GetOpsRequestCompPhase gets the component phase of testing OpsRequest for verification. func GetOpsRequestCompPhase(ctx context.Context, testCtx testutil.TestContext, opsName, componentName string) func(g gomega.Gomega) appsv1alpha1.ClusterComponentPhase { return func(g gomega.Gomega) appsv1alpha1.ClusterComponentPhase { tmpOps := &appsv1alpha1.OpsRequest{} diff --git a/internal/testutil/apps/pod_factory.go b/internal/testutil/apps/pod_factory.go index c0c0e2881..2054fc445 100644 --- a/internal/testutil/apps/pod_factory.go +++ b/internal/testutil/apps/pod_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -49,3 +52,8 @@ func (factory *MockPodFactory) AddVolume(volume corev1.Volume) *MockPodFactory { *volumes = append(*volumes, volume) return factory } + +func (factory *MockPodFactory) AddNodeName(nodeName string) *MockPodFactory { + factory.get().Spec.NodeName = nodeName + return factory +} diff --git a/internal/testutil/apps/pvc_factoy.go b/internal/testutil/apps/pvc_factoy.go index 85dc44d6d..3e32cf06b 100644 --- a/internal/testutil/apps/pvc_factoy.go +++ b/internal/testutil/apps/pvc_factoy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/internal/testutil/apps/restorejob_factory.go b/internal/testutil/apps/restorejob_factory.go index fba1d561c..1c46ab9b5 100644 --- a/internal/testutil/apps/restorejob_factory.go +++ b/internal/testutil/apps/restorejob_factory.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps @@ -55,8 +58,7 @@ func (factory *MockRestoreJobFactory) AddTargetMatchLabels(keyAndValues ...strin } func (factory *MockRestoreJobFactory) SetTargetSecretName(name string) *MockRestoreJobFactory { - factory.get().Spec.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{} - factory.get().Spec.Target.Secret.Name = name + factory.get().Spec.Target.Secret = &dataprotectionv1alpha1.BackupPolicySecret{Name: name} return factory } diff --git a/internal/testutil/apps/statefulset_factoy.go b/internal/testutil/apps/statefulset_factoy.go index c73919593..18366814f 100644 --- a/internal/testutil/apps/statefulset_factoy.go +++ b/internal/testutil/apps/statefulset_factoy.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package apps diff --git a/internal/testutil/k8s/deployment_util.go b/internal/testutil/k8s/deployment_util.go index 6c8363d85..cadabf438 100644 --- a/internal/testutil/k8s/deployment_util.go +++ b/internal/testutil/k8s/deployment_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil @@ -19,9 +22,15 @@ package testutil import ( "fmt" + "github.com/onsi/gomega" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/apecloud/kubeblocks/internal/constant" + "github.com/apecloud/kubeblocks/internal/testutil" ) // MockDeploymentReady mocks deployment is ready @@ -51,3 +60,15 @@ func MockPodAvailable(pod *corev1.Pod, lastTransitionTime metav1.Time) { }, } } + +func ListAndCheckDeployment(testCtx *testutil.TestContext, key types.NamespacedName) *appsv1.DeploymentList { + deployList := &appsv1.DeploymentList{} + gomega.Eventually(func(g gomega.Gomega) { + g.Expect(testCtx.Cli.List(testCtx.Ctx, deployList, client.MatchingLabels{ + constant.AppInstanceLabelKey: key.Name, + }, client.InNamespace(key.Namespace))).Should(gomega.Succeed()) + g.Expect(deployList.Items).ShouldNot(gomega.BeNil()) + g.Expect(deployList.Items).ShouldNot(gomega.BeEmpty()) + }).Should(gomega.Succeed()) + return deployList +} diff --git a/internal/testutil/k8s/k8sclient_util.go b/internal/testutil/k8s/k8sclient_util.go index 47a19df72..5c337d94c 100644 --- a/internal/testutil/k8s/k8sclient_util.go +++ b/internal/testutil/k8s/k8sclient_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil @@ -263,16 +266,16 @@ type MockGetReturned struct { func WithConstructSequenceResult(mockObjs map[client.ObjectKey][]MockGetReturned) HandleGetReturnedObject { sequenceAccessCounter := make(map[client.ObjectKey]int, len(mockObjs)) return func(key client.ObjectKey, obj client.Object) error { - accessableSequence, ok := mockObjs[key] + accessibleSequence, ok := mockObjs[key] if !ok { - return fmt.Errorf("not exist: %v", key) + return fmt.Errorf("not existed key: %v", key) } index := sequenceAccessCounter[key] - mockReturned := accessableSequence[index] + mockReturned := accessibleSequence[index] if mockReturned.Err == nil { SetGetReturnedObject(obj, mockReturned.Object) } - if index < len(accessableSequence)-1 { + if index < len(accessibleSequence)-1 { sequenceAccessCounter[key]++ } return mockReturned.Err @@ -329,7 +332,7 @@ func WithGetReturned(action HandleGetReturnedObject, times ...CallMockOptions) C }) handleTimes(call, times...) default: - panic("not walk here!") + panic("impossible dead end!") } } } @@ -343,7 +346,7 @@ func WithPatchReturned(action HandlePatchReturnedObject, times ...CallMockOption }) handleTimes(call, times...) default: - panic("not walk here!") + panic("impossible dead end!") } } } diff --git a/internal/testutil/k8s/mocks/generate.go b/internal/testutil/k8s/mocks/generate.go index 2acc5a69a..9ed9b29be 100644 --- a/internal/testutil/k8s/mocks/generate.go +++ b/internal/testutil/k8s/mocks/generate.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package mocks diff --git a/internal/testutil/k8s/statefulset_util.go b/internal/testutil/k8s/statefulset_util.go index 6af9cf6d8..050ba2d55 100644 --- a/internal/testutil/k8s/statefulset_util.go +++ b/internal/testutil/k8s/statefulset_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil @@ -22,7 +25,7 @@ import ( "reflect" "github.com/onsi/gomega" - apps "k8s.io/api/apps/v1" + appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -39,7 +42,7 @@ const ( ) // NewFakeStatefulSet creates a fake StatefulSet workload object for testing. -func NewFakeStatefulSet(name string, replicas int) *apps.StatefulSet { +func NewFakeStatefulSet(name string, replicas int) *appsv1.StatefulSet { template := corev1.PodTemplateSpec{ Spec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -54,12 +57,12 @@ func NewFakeStatefulSet(name string, replicas int) *apps.StatefulSet { template.Labels = map[string]string{"foo": "bar"} statefulSetReplicas := int32(replicas) Revision := name + "-d5df5b8d6" - return &apps.StatefulSet{ + return &appsv1.StatefulSet{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: corev1.NamespaceDefault, }, - Spec: apps.StatefulSetSpec{ + Spec: appsv1.StatefulSetSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{"foo": "bar"}, }, @@ -67,7 +70,7 @@ func NewFakeStatefulSet(name string, replicas int) *apps.StatefulSet { Template: template, ServiceName: "governingsvc", }, - Status: apps.StatefulSetStatus{ + Status: appsv1.StatefulSetStatus{ AvailableReplicas: statefulSetReplicas, ObservedGeneration: 0, ReadyReplicas: statefulSetReplicas, @@ -79,14 +82,14 @@ func NewFakeStatefulSet(name string, replicas int) *apps.StatefulSet { } // NewFakeStatefulSetPod creates a fake pod of the StatefulSet workload for testing. -func NewFakeStatefulSetPod(set *apps.StatefulSet, ordinal int) *corev1.Pod { +func NewFakeStatefulSetPod(set *appsv1.StatefulSet, ordinal int) *corev1.Pod { pod := &corev1.Pod{} pod.Name = fmt.Sprintf("%s-%d", set.Name, ordinal) return pod } // MockStatefulSetReady mocks the StatefulSet workload is ready. -func MockStatefulSetReady(sts *apps.StatefulSet) { +func MockStatefulSetReady(sts *appsv1.StatefulSet) { sts.Status.AvailableReplicas = *sts.Spec.Replicas sts.Status.ObservedGeneration = sts.Generation sts.Status.Replicas = *sts.Spec.Replicas @@ -111,19 +114,20 @@ func DeletePodLabelKey(ctx context.Context, testCtx testutil.TestContext, podNam }).Should(gomega.Succeed()) } -// UpdatePodStatusNotReady updates the pod status to make it not ready. -func UpdatePodStatusNotReady(ctx context.Context, testCtx testutil.TestContext, podName string) { +// UpdatePodStatusScheduleFailed updates the pod status to mock the schedule failure. +func UpdatePodStatusScheduleFailed(ctx context.Context, testCtx testutil.TestContext, podName, namespace string) { pod := &corev1.Pod{} - gomega.Expect(testCtx.Cli.Get(ctx, client.ObjectKey{Name: podName, Namespace: testCtx.DefaultNamespace}, pod)).Should(gomega.Succeed()) + gomega.Expect(testCtx.Cli.Get(ctx, client.ObjectKey{Name: podName, Namespace: namespace}, pod)).Should(gomega.Succeed()) patch := client.MergeFrom(pod.DeepCopy()) - pod.Status.Conditions = nil + pod.Status.Conditions = []corev1.PodCondition{ + { + Type: corev1.PodScheduled, + Status: corev1.ConditionFalse, + Message: "0/1 node cpu Insufficient", + Reason: "Unschedulable", + }, + } gomega.Expect(testCtx.Cli.Status().Patch(ctx, pod, patch)).Should(gomega.Succeed()) - gomega.Eventually(func(g gomega.Gomega) { - tmpPod := &corev1.Pod{} - _ = testCtx.Cli.Get(context.Background(), - client.ObjectKey{Name: podName, Namespace: testCtx.DefaultNamespace}, tmpPod) - g.Expect(tmpPod.Status.Conditions).Should(gomega.BeNil()) - }).Should(gomega.Succeed()) } // MockPodIsTerminating mocks pod is terminating. @@ -151,8 +155,8 @@ func RemovePodFinalizer(ctx context.Context, testCtx testutil.TestContext, pod * }).Should(gomega.Satisfy(apierrors.IsNotFound)) } -func ListAndCheckStatefulSet(testCtx *testutil.TestContext, key types.NamespacedName) *apps.StatefulSetList { - stsList := &apps.StatefulSetList{} +func ListAndCheckStatefulSet(testCtx *testutil.TestContext, key types.NamespacedName) *appsv1.StatefulSetList { + stsList := &appsv1.StatefulSetList{} gomega.Eventually(func(g gomega.Gomega) { g.Expect(testCtx.Cli.List(testCtx.Ctx, stsList, client.MatchingLabels{ constant.AppInstanceLabelKey: key.Name, @@ -163,8 +167,8 @@ func ListAndCheckStatefulSet(testCtx *testutil.TestContext, key types.Namespaced return stsList } -func ListAndCheckStatefulSetCount(testCtx *testutil.TestContext, key types.NamespacedName, cnt int) *apps.StatefulSetList { - stsList := &apps.StatefulSetList{} +func ListAndCheckStatefulSetItemsCount(testCtx *testutil.TestContext, key types.NamespacedName, cnt int) *appsv1.StatefulSetList { + stsList := &appsv1.StatefulSetList{} gomega.Eventually(func(g gomega.Gomega) { g.Expect(testCtx.Cli.List(testCtx.Ctx, stsList, client.MatchingLabels{ constant.AppInstanceLabelKey: key.Name, @@ -174,8 +178,8 @@ func ListAndCheckStatefulSetCount(testCtx *testutil.TestContext, key types.Names return stsList } -func ListAndCheckStatefulSetWithComponent(testCtx *testutil.TestContext, key types.NamespacedName, componentName string) *apps.StatefulSetList { - stsList := &apps.StatefulSetList{} +func ListAndCheckStatefulSetWithComponent(testCtx *testutil.TestContext, key types.NamespacedName, componentName string) *appsv1.StatefulSetList { + stsList := &appsv1.StatefulSetList{} gomega.Eventually(func(g gomega.Gomega) { g.Expect(testCtx.Cli.List(testCtx.Ctx, stsList, client.MatchingLabels{ constant.AppInstanceLabelKey: key.Name, @@ -199,17 +203,17 @@ func ListAndCheckPodCountWithComponent(testCtx *testutil.TestContext, key types. return podList } -func PatchStatefulSetStatus(testCtx *testutil.TestContext, stsName string, status apps.StatefulSetStatus) { +func PatchStatefulSetStatus(testCtx *testutil.TestContext, stsName string, status appsv1.StatefulSetStatus) { objectKey := client.ObjectKey{Name: stsName, Namespace: testCtx.DefaultNamespace} - gomega.Expect(testapps.GetAndChangeObjStatus(testCtx, objectKey, func(newSts *apps.StatefulSet) { + gomega.Expect(testapps.GetAndChangeObjStatus(testCtx, objectKey, func(newSts *appsv1.StatefulSet) { newSts.Status = status })()).Should(gomega.Succeed()) - gomega.Eventually(testapps.CheckObj(testCtx, objectKey, func(g gomega.Gomega, newSts *apps.StatefulSet) { + gomega.Eventually(testapps.CheckObj(testCtx, objectKey, func(g gomega.Gomega, newSts *appsv1.StatefulSet) { g.Expect(reflect.DeepEqual(newSts.Status, status)).Should(gomega.BeTrue()) })).Should(gomega.Succeed()) } -func InitStatefulSetStatus(testCtx testutil.TestContext, statefulset *apps.StatefulSet, controllerRevision string) { +func InitStatefulSetStatus(testCtx testutil.TestContext, statefulset *appsv1.StatefulSet, controllerRevision string) { gomega.Expect(testapps.ChangeObjStatus(&testCtx, statefulset, func() { statefulset.Status.UpdateRevision = controllerRevision statefulset.Status.CurrentRevision = controllerRevision diff --git a/internal/testutil/k8s/storage_util.go b/internal/testutil/k8s/storage_util.go index b96ddb063..77f0fb013 100644 --- a/internal/testutil/k8s/storage_util.go +++ b/internal/testutil/k8s/storage_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil diff --git a/internal/testutil/k8s/tunnel_util.go b/internal/testutil/k8s/tunnel_util.go index f5f09eb06..921bc18d1 100644 --- a/internal/testutil/k8s/tunnel_util.go +++ b/internal/testutil/k8s/tunnel_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil @@ -92,7 +95,7 @@ func (m *Mysql) Init(metadata map[string]string) error { return nil } -// Close will close the DB. +// Close closes the DB. func (m *Mysql) Close() error { if m.db != nil { return m.db.Close() diff --git a/internal/testutil/type.go b/internal/testutil/type.go index 8a2c1a961..4e0f2a59c 100644 --- a/internal/testutil/type.go +++ b/internal/testutil/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package testutil @@ -68,7 +71,7 @@ func init() { viper.SetDefault("ClearResourcePollingInterval", time.Millisecond) } -// NewDefaultTestContext create default test context, if provided namespace optional arg, a namespace +// NewDefaultTestContext creates default test context, if provided namespace optional arg, a namespace // will be created if not exist func NewDefaultTestContext(ctx context.Context, cli client.Client, testEnv *envtest.Environment, namespace ...string) TestContext { if cli == nil { @@ -182,7 +185,7 @@ func (testCtx TestContext) UseDefaultNamespace() func(client.Object) { } } -// SetKubeServerVersionWithDistro provide "_KUBE_SERVER_INFO" viper settings helper function. +// SetKubeServerVersionWithDistro provides "_KUBE_SERVER_INFO" viper settings helper function. func SetKubeServerVersionWithDistro(major, minor, patch, distro string) { ver := version.Info{ Major: major, @@ -192,7 +195,7 @@ func SetKubeServerVersionWithDistro(major, minor, patch, distro string) { viper.Set(constant.CfgKeyServerInfo, ver) } -// SetKubeServerVersion provide "_KUBE_SERVER_INFO" viper settings helper function. +// SetKubeServerVersion provides "_KUBE_SERVER_INFO" viper settings helper function. func SetKubeServerVersion(major, minor, patch string) { ver := version.Info{ Major: major, diff --git a/internal/unstructured/config_object.go b/internal/unstructured/config_object.go index 79999b21b..89ab70851 100644 --- a/internal/unstructured/config_object.go +++ b/internal/unstructured/config_object.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured @@ -48,7 +51,7 @@ func (c *ConfigObjectRegistry) RegisterConfigCreator(format appsv1alpha1.CfgFile func (c *ConfigObjectRegistry) GetConfigObject(name string, format appsv1alpha1.CfgFileFormat) (ConfigObject, error) { creator, ok := c.objectCreator[format] if !ok { - return nil, fmt.Errorf("not support type[%s]", format) + return nil, fmt.Errorf("not supported type[%s]", format) } return creator(name), nil } diff --git a/internal/unstructured/redis/lexer.go b/internal/unstructured/redis/lexer.go index ed9be0442..996aff60d 100644 --- a/internal/unstructured/redis/lexer.go +++ b/internal/unstructured/redis/lexer.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis diff --git a/internal/unstructured/redis/parser_fsm.go b/internal/unstructured/redis/parser_fsm.go index 82adbb165..881354c16 100644 --- a/internal/unstructured/redis/parser_fsm.go +++ b/internal/unstructured/redis/parser_fsm.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis diff --git a/internal/unstructured/redis/parser_fsm_test.go b/internal/unstructured/redis/parser_fsm_test.go index a12336265..77c9b47c8 100644 --- a/internal/unstructured/redis/parser_fsm_test.go +++ b/internal/unstructured/redis/parser_fsm_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis diff --git a/internal/unstructured/redis/rune_util.go b/internal/unstructured/redis/rune_util.go index c60e4dfa9..38ea1e3e7 100644 --- a/internal/unstructured/redis/rune_util.go +++ b/internal/unstructured/redis/rune_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis diff --git a/internal/unstructured/redis/rune_util_test.go b/internal/unstructured/redis/rune_util_test.go index 027c7d2a4..27d94b825 100644 --- a/internal/unstructured/redis/rune_util_test.go +++ b/internal/unstructured/redis/rune_util_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package redis diff --git a/internal/unstructured/redis_config.go b/internal/unstructured/redis_config.go index 6d0e04fda..ff7bb5170 100644 --- a/internal/unstructured/redis_config.go +++ b/internal/unstructured/redis_config.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured @@ -47,7 +50,7 @@ func (r *redisConfig) Update(key string, value any) error { } func (r *redisConfig) setString(key string, value string) error { - keys := strings.Split(key, ".") + keys := strings.Split(key, " ") v := r.GetItem(keys) lineNo := math.MaxInt32 if v != nil { @@ -104,7 +107,7 @@ func (r *redisConfig) GetItem(keys []string) *redis.Item { } func (r *redisConfig) GetString(key string) (string, error) { - keys := strings.Split(key, ".") + keys := strings.Split(key, " ") item := r.GetItem(keys) if item == nil { return "", nil diff --git a/internal/unstructured/redis_config_test.go b/internal/unstructured/redis_config_test.go index c7e87141a..38dc74b40 100644 --- a/internal/unstructured/redis_config_test.go +++ b/internal/unstructured/redis_config_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured @@ -55,21 +58,21 @@ func TestRedisConfig(t *testing.T) { valueArgs: "256mb 64mb 60", wantErr: false, testKey: map[string]string{ - "client-output-buffer-limit.pubsub": "256mb 64mb 60", + "client-output-buffer-limit pubsub": "256mb 64mb 60", }, }, { keyArgs: []string{"client-output-buffer-limit", "normal"}, valueArgs: "128mb 32mb 0", wantErr: false, testKey: map[string]string{ - "client-output-buffer-limit.normal": "128mb 32mb 0", - "client-output-buffer-limit.pubsub": "256mb 64mb 60", + "client-output-buffer-limit normal": "128mb 32mb 0", + "client-output-buffer-limit pubsub": "256mb 64mb 60", "port": "6379", }, }} for _, tt := range tests { t.Run("config_test", func(t *testing.T) { - if err := c.Update(strings.Join(tt.keyArgs, "."), tt.valueArgs); (err != nil) != tt.wantErr { + if err := c.Update(strings.Join(tt.keyArgs, " "), tt.valueArgs); (err != nil) != tt.wantErr { t.Errorf("Update() error = %v, wantErr %v", err, tt.wantErr) } @@ -91,14 +94,14 @@ func TestRedisConfigGetAllParameters(t *testing.T) { fn mockfn want map[string]interface{} }{{ - name: "xxx", + name: "multi field update test", fn: func() ConfigObject { c, _ := LoadConfig("test", "", appsv1alpha1.RedisCfg) _ = c.Update("port", "123") - _ = c.Update("a.b", "123 234") - _ = c.Update("a.c", "345") - _ = c.Update("a.d", "1 2") - _ = c.Update("a.d.e", "1 2") + _ = c.Update("a b", "123 234") + _ = c.Update("a c", "345") + _ = c.Update("a d", "1 2") + _ = c.Update("a d e", "1 2") return c }, want: map[string]interface{}{ diff --git a/internal/unstructured/type.go b/internal/unstructured/type.go index 2ec4300e1..e9b8febe4 100644 --- a/internal/unstructured/type.go +++ b/internal/unstructured/type.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured diff --git a/internal/unstructured/viper_util.go b/internal/unstructured/viper_util.go index 5d94e8870..a69bd1b4a 100644 --- a/internal/unstructured/viper_util.go +++ b/internal/unstructured/viper_util.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured diff --git a/internal/unstructured/viper_wrap.go b/internal/unstructured/viper_wrap.go index 6b251be6a..36571089c 100644 --- a/internal/unstructured/viper_wrap.go +++ b/internal/unstructured/viper_wrap.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured @@ -37,7 +40,7 @@ type viperWrap struct { func init() { CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.Ini, createViper(appsv1alpha1.Ini)) - CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.YAML, createViper(appsv1alpha1.YAML)) + // CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.YAML, createViper(appsv1alpha1.YAML)) CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.JSON, createViper(appsv1alpha1.JSON)) CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.Dotenv, createViper(appsv1alpha1.Dotenv)) CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.HCL, createViper(appsv1alpha1.HCL)) diff --git a/internal/unstructured/viper_wrap_test.go b/internal/unstructured/viper_wrap_test.go index 5ad7ea87b..7043ef086 100644 --- a/internal/unstructured/viper_wrap_test.go +++ b/internal/unstructured/viper_wrap_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured @@ -59,8 +62,8 @@ func TestPropertiesFormat(t *testing.T) { const propertiesContext = ` listen_addresses = '*' port = '5432' +archive_command = '[[ $(date +%H%M) == 1200 ]] && rm -rf /home/postgres/pgdata/pgroot/arcwal/$(date -d"yesterday" +%Y%m%d); mkdir -p /home/postgres/pgdata/pgroot/arcwal/$(date +%Y%m%d) && gzip -kqc %p > /home/postgres/pgdata/pgroot/arcwal/$(date +%Y%m%d)/%f.gz' -#archive_command = 'wal_dir=/pg/arcwal; [[ $(date +%H%M) == 1200 ]] && rm -rf ${wal_dir}/$(date -d"yesterday" +%Y%m%d); /bin/mkdir -p ${wal_dir}/$(date +%Y%m%d) && /usr/bin/lz4 -q -z %p > ${wal_dir}/$(date +%Y%m%d)/%f.lz4' #archive_mode = 'True' auto_explain.log_analyze = 'True' auto_explain.log_min_duration = '1s' @@ -78,6 +81,7 @@ autovacuum_naptime = '1min' assert.EqualValues(t, propConfigObj.Get("auto_explain.log_nested_statements"), "'True'") assert.EqualValues(t, propConfigObj.Get("auto_explain.log_min_duration"), "'1s'") assert.EqualValues(t, propConfigObj.Get("autovacuum_naptime"), "'1min'") + assert.EqualValues(t, propConfigObj.Get("archive_command"), `'[[ $(date +%H%M) == 1200 ]] && rm -rf /home/postgres/pgdata/pgroot/arcwal/$(date -d"yesterday" +%Y%m%d); mkdir -p /home/postgres/pgdata/pgroot/arcwal/$(date +%Y%m%d) && gzip -kqc %p > /home/postgres/pgdata/pgroot/arcwal/$(date +%Y%m%d)/%f.gz'`) dumpContext, err := propConfigObj.Marshal() assert.Nil(t, err) @@ -118,32 +122,3 @@ func TestJSONFormat(t *testing.T) { assert.EqualValues(t, jsonConfigObj.Get("name"), "test") } - -func TestYAMLFormat(t *testing.T) { - const yamlContext = ` -spec: - clusterref: pg - reconfigure: - componentname: postgresql - configurations: - - keys: - - key: postgresql.conf - parameters: - - key: max_connections - value: "2666" - name: postgresql-configuration -` - - yamlConfigObj, err := LoadConfig("yaml_test", yamlContext, appsv1alpha1.YAML) - assert.Nil(t, err) - - assert.EqualValues(t, yamlConfigObj.Get("spec.clusterRef"), "pg") - assert.EqualValues(t, yamlConfigObj.Get("spec.reconfigure.componentName"), "postgresql") - - dumpContext, err := yamlConfigObj.Marshal() - assert.Nil(t, err) - assert.EqualValues(t, dumpContext, yamlContext[1:]) // trim "\n" - - assert.Nil(t, yamlConfigObj.Update("spec.my_test", "100")) - assert.EqualValues(t, yamlConfigObj.Get("spec.my_test"), "100") -} diff --git a/internal/unstructured/xml_config.go b/internal/unstructured/xml_config.go index 267f99162..015886805 100644 --- a/internal/unstructured/xml_config.go +++ b/internal/unstructured/xml_config.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured diff --git a/internal/unstructured/xml_config_test.go b/internal/unstructured/xml_config_test.go index 4289d7e5b..c71af5c80 100644 --- a/internal/unstructured/xml_config_test.go +++ b/internal/unstructured/xml_config_test.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package unstructured diff --git a/internal/unstructured/yaml_config.go b/internal/unstructured/yaml_config.go new file mode 100644 index 000000000..26db238bf --- /dev/null +++ b/internal/unstructured/yaml_config.go @@ -0,0 +1,146 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package unstructured + +import ( + "strings" + + "github.com/spf13/cast" + "gopkg.in/yaml.v2" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +type yamlConfig struct { + name string + config map[string]any +} + +func init() { + CfgObjectRegistry().RegisterConfigCreator(appsv1alpha1.YAML, func(name string) ConfigObject { + return &yamlConfig{name: name} + }) +} + +func (y *yamlConfig) Update(key string, value any) error { + path := strings.Split(key, ".") + lastKey := path[len(path)-1] + deepestMap := checkAndCreateNestedPrefixMap(y.config, path[0:len(path)-1]) + deepestMap[lastKey] = value + return nil +} + +func (y *yamlConfig) Get(key string) any { + keys := strings.Split(key, ".") + return searchMap(y.config, keys) +} + +func (y *yamlConfig) GetString(key string) (string, error) { + v := y.Get(key) + if v != nil { + return cast.ToStringE(v) + } + return "", nil +} + +func (y *yamlConfig) GetAllParameters() map[string]any { + return y.config +} + +func (y *yamlConfig) SubConfig(key string) ConfigObject { + v := y.Get(key) + if m, ok := v.(map[string]any); ok { + return &yamlConfig{ + name: y.name, + config: m, + } + } + return nil +} + +func (y *yamlConfig) Marshal() (string, error) { + b, err := yaml.Marshal(y.config) + return string(b), err +} + +func (y *yamlConfig) Unmarshal(str string) error { + config := make(map[any]any) + err := yaml.Unmarshal([]byte(str), config) + if err != nil { + return err + } + y.config = transKeyString(config) + return nil +} + +func checkAndCreateNestedPrefixMap(m map[string]any, path []string) map[string]any { + for _, k := range path { + m2, ok := m[k] + // if the key is not existed, create a new map + if !ok { + m3 := make(map[string]any) + m[k] = m3 + m = m3 + continue + } + m3, ok := m2.(map[string]any) + // if the type is not map, replace it with a new map + if !ok { + m3 = make(map[string]any) + m[k] = m3 + } + m = m3 + } + return m +} + +func searchMap(m map[string]any, path []string) any { + if len(path) == 0 { + return m + } + + next, ok := m[path[0]] + if !ok { + return nil + } + if len(path) == 1 { + return next + } + switch t := next.(type) { + default: + return nil + case map[any]any: + return searchMap(cast.ToStringMap(t), path[1:]) + case map[string]any: + return searchMap(t, path[1:]) + } +} + +func transKeyString(m map[any]any) map[string]any { + m2 := make(map[string]any, len(m)) + for k, v := range m { + if vi, ok := v.(map[any]any); ok { + m2[cast.ToString(k)] = transKeyString(vi) + } else { + m2[cast.ToString(k)] = v + } + } + return m2 +} diff --git a/internal/unstructured/yaml_config_test.go b/internal/unstructured/yaml_config_test.go new file mode 100644 index 000000000..289a90577 --- /dev/null +++ b/internal/unstructured/yaml_config_test.go @@ -0,0 +1,57 @@ +/* +Copyright (C) 2022-2023 ApeCloud Co., Ltd + +This file is part of KubeBlocks project + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . +*/ + +package unstructured + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" +) + +func TestYAMLFormat(t *testing.T) { + const yamlContext = ` +spec: + clusterRef: pg + reconfigure: + componentName: postgresql + configurations: + - keys: + - key: postgresql.conf + parameters: + - key: max_connections + value: "2666" + name: postgresql-configuration +` + + yamlConfigObj, err := LoadConfig("yaml_test", yamlContext, appsv1alpha1.YAML) + assert.Nil(t, err) + + assert.EqualValues(t, yamlConfigObj.Get("spec.clusterRef"), "pg") + assert.EqualValues(t, yamlConfigObj.Get("spec.reconfigure.componentName"), "postgresql") + + dumpContext, err := yamlConfigObj.Marshal() + assert.Nil(t, err) + assert.EqualValues(t, dumpContext, yamlContext[1:]) // trim "\n" + + assert.Nil(t, yamlConfigObj.Update("spec.my_test", "100")) + assert.EqualValues(t, yamlConfigObj.Get("spec.my_test"), "100") +} diff --git a/internal/webhook/pod_admission.go b/internal/webhook/pod_admission.go index e1e68eab3..44846baa5 100644 --- a/internal/webhook/pod_admission.go +++ b/internal/webhook/pod_admission.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package webhook diff --git a/internal/webhook/webhook.go b/internal/webhook/webhook.go index 6640a7b93..068d891be 100644 --- a/internal/webhook/webhook.go +++ b/internal/webhook/webhook.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package webhook diff --git a/staticcheck.conf b/staticcheck.conf index 7e997d1fe..b8afabf96 100644 --- a/staticcheck.conf +++ b/staticcheck.conf @@ -1,7 +1,7 @@ # This is config file for staticcheck. # Check https://staticcheck.io/docs/checks/ for check ID. -# If you need to add ignored checks, pls also add explaination in comments. +# If you need to add ignored checks, pls also add explanation in comments. checks = ["all", "-ST1000", "-SA1019", "-ST1001"] diff --git a/test/e2e/Makefile b/test/e2e/Makefile index 169399316..6de2cbd9e 100644 --- a/test/e2e/Makefile +++ b/test/e2e/Makefile @@ -30,10 +30,20 @@ ginkgo: # Make sure ginkgo is in $GOPATH/bin ifeq ($(origin VERSION), command line) VERSION ?= $(VERSION) endif +ifeq ($(origin VERSION), command line) + PROVIDER ?= $(PROVIDER) +else + PROVIDER ?= "" +endif +ifeq ($(origin REGION), command line) + REGION ?= $(REGION) +else + REGION ?= "" +endif .PHONY: run run: ginkgo ## Run end-to-end tests. #ACK_GINKGO_DEPRECATIONS=$(GINKGO_VERSION) $(GINKGO) run . - $(GINKGO) test -process -ginkgo.v . --json-report=report.json -- --VERSION=$(VERSION) + $(GINKGO) test -process -ginkgo.v . -- -VERSION=$(VERSION) -PROVIDER=$(PROVIDER) -REGION=$(REGION) -SECRET_ID=$(SECRET_ID) -SECRET_KEY=$(SECRET_KEY) --ginkgo.json-report=report.json build: ginkgo ## Run ginkgo build e2e test suite binary. $(GINKGO) build . diff --git a/test/e2e/e2e_suite_test.go b/test/e2e/e2e_suite_test.go index ad5a9c4c3..7505932f5 100644 --- a/test/e2e/e2e_suite_test.go +++ b/test/e2e/e2e_suite_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -48,10 +48,18 @@ var cfg *rest.Config var testEnv *envtest.Environment var TC *TestClient var version string +var provider string +var region string +var secretID string +var secretKey string func init() { viper.AutomaticEnv() flag.StringVar(&version, "VERSION", "", "kubeblocks test version") + flag.StringVar(&provider, "PROVIDER", "", "kubeblocks test cloud-provider") + flag.StringVar(®ion, "REGION", "", "kubeblocks test region") + flag.StringVar(&secretID, "SECRET_ID", "", "cloud-provider SECRET_ID") + flag.StringVar(&secretKey, "SECRET_KEY", "", "cloud-provider SECRET_KEY") } func TestE2e(t *testing.T) { @@ -91,6 +99,12 @@ var _ = BeforeSuite(func() { } log.Println("kb version:" + version) Version = version + if len(provider) > 0 && len(region) > 0 && len(secretID) > 0 && len(secretKey) > 0 { + Provider = provider + Region = region + SecretID = secretID + SecretKey = secretKey + } if viper.GetBool("ENABLE_DEBUG_LOG") { logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true), func(o *zap.Options) { o.TimeEncoder = zapcore.ISO8601TimeEncoder diff --git a/test/e2e/envcheck/envcheck.go b/test/e2e/envcheck/envcheck.go index 0aa9934dc..643a01903 100644 --- a/test/e2e/envcheck/envcheck.go +++ b/test/e2e/envcheck/envcheck.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/e2e/installation/installcheck.go b/test/e2e/installation/installcheck.go index d03432a5d..9cd815f7c 100644 --- a/test/e2e/installation/installcheck.go +++ b/test/e2e/installation/installcheck.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/e2e/testdata/.gitkeep b/test/e2e/testdata/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/test/e2e/testdata/smoketest/mongodb/00_mongodbcluster.yaml b/test/e2e/testdata/smoketest/mongodb/00_mongodbcluster.yaml new file mode 100644 index 000000000..e470730b9 --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/00_mongodbcluster.yaml @@ -0,0 +1,89 @@ +--- +# Source: mongodb-cluster/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kb-mycluster-mongodb-cluster + labels: + + helm.sh/chart: mongodb-cluster-0.5.1-beta.0 + app.kubernetes.io/name: mongodb-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "5.0.14" + app.kubernetes.io/managed-by: Helm +--- +# Source: mongodb-cluster/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-mycluster-mongodb-cluster + namespace: default + labels: + + helm.sh/chart: mongodb-cluster-0.5.1-beta.0 + app.kubernetes.io/name: mongodb-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "5.0.14" + app.kubernetes.io/managed-by: Helm +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +# Source: mongodb-cluster/templates/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-mycluster-mongodb-cluster + labels: + + helm.sh/chart: mongodb-cluster-0.5.1-beta.0 + app.kubernetes.io/name: mongodb-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "5.0.14" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-mycluster-mongodb-cluster +subjects: + - kind: ServiceAccount + name: kb-mycluster-mongodb-cluster + namespace: default +--- +# Source: mongodb-cluster/templates/replicaset.yaml +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: mycluster + labels: + helm.sh/chart: mongodb-cluster-0.5.1-beta.0 + app.kubernetes.io/name: mongodb-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "5.0.14" + app.kubernetes.io/managed-by: Helm +spec: + clusterDefinitionRef: mongodb + clusterVersionRef: mongodb-5.0.14 + terminationPolicy: Halt + affinity: + topologyKeys: + - kubernetes.io/hostname + componentSpecs: + - name: mongodb + componentDefRef: mongodb + monitor: false + replicas: 3 + serviceAccountName: kb-mycluster-mongodb-cluster + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + storageClassName: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi diff --git a/test/e2e/testdata/smoketest/mongodb/01_vexpand.yaml b/test/e2e/testdata/smoketest/mongodb/01_vexpand.yaml new file mode 100644 index 000000000..d4c845bcd --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/01_vexpand.yaml @@ -0,0 +1,12 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-vexpand +spec: + clusterRef: mycluster + type: VolumeExpansion + volumeExpansion: + - componentName: mongodb + volumeClaimTemplates: + - name: data + storage: "2Gi" \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/mongodb/02_stop.yaml b/test/e2e/testdata/smoketest/mongodb/02_stop.yaml new file mode 100644 index 000000000..063f11cf5 --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/02_stop.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-stop +spec: + clusterRef: mycluster + ttlSecondsAfterSucceed: 27017 + type: Stop + restart: + - componentName: mongodb \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/mongodb/03_start.yaml b/test/e2e/testdata/smoketest/mongodb/03_start.yaml new file mode 100644 index 000000000..96195dd4b --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/03_start.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-start +spec: + clusterRef: mycluster + ttlSecondsAfterSucceed: 27017 + type: Start + restart: + - componentName: mongodb \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/mongodb/04_vscale.yaml b/test/e2e/testdata/smoketest/mongodb/04_vscale.yaml new file mode 100644 index 000000000..5a145b32b --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/04_vscale.yaml @@ -0,0 +1,12 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-vscale +spec: + clusterRef: mycluster + type: VerticalScaling + verticalScaling: + - componentName: mongodb + requests: + cpu: "500m" + memory: 500Mi \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/mongodb/05_restart.yaml b/test/e2e/testdata/smoketest/mongodb/05_restart.yaml new file mode 100644 index 000000000..5a87555d3 --- /dev/null +++ b/test/e2e/testdata/smoketest/mongodb/05_restart.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-restart +spec: + clusterRef: mycluster + ttlSecondsAfterSucceed: 27017 + type: Restart + restart: + - componentName: mongodb \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/playgroundtest.go b/test/e2e/testdata/smoketest/playgroundtest.go index 5c042dacf..2324d1b79 100644 --- a/test/e2e/testdata/smoketest/playgroundtest.go +++ b/test/e2e/testdata/smoketest/playgroundtest.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + . "github.com/apecloud/kubeblocks/test/e2e" e2eutil "github.com/apecloud/kubeblocks/test/e2e/util" ) @@ -45,10 +46,37 @@ func PlaygroundInit() { } }) It("kbcli playground init", func() { - cmd := "kbcli playground init" - log.Println(cmd) - init := e2eutil.ExecuteCommand(cmd) - log.Println(init) + var cmd string + if len(Provider) > 0 && len(Region) > 0 && len(SecretID) > 0 && len(SecretKey) > 0 { + var id, key string + if Provider == "aws" { + id = "export AWS_ACCESS_KEY_ID=" + SecretID + key = "export AWS_SECRET_ACCESS_KEY=" + SecretKey + } else if Provider == "tencentcloud" { + id = "export TENCENTCLOUD_SECRET_ID=" + SecretID + key = "export TENCENTCLOUD_SECRET_KEY" + SecretKey + } else if Provider == "alicloud" { + id = "export ALICLOUD_ACCESS_KEY=" + SecretID + key = "export ALICLOUD_SECRET_KEY=" + SecretKey + } else { + log.Println("not support " + Provider + " cloud-provider") + } + idCmd := e2eutil.ExecuteCommand(id) + log.Println(idCmd) + keyCmd := e2eutil.ExecuteCommand(key) + log.Println(keyCmd) + cmd = "kbcli playground init --cloud-provider " + Provider + " --region " + Region + output, err := e2eutil.Check(cmd, "yes\n") + if err != nil { + log.Fatalf("Command execution failure: %v\n", err) + } + log.Println("Command execution result:", output) + } else { + cmd = "kbcli playground init" + log.Println(cmd) + init := e2eutil.ExecuteCommand(cmd) + log.Println(init) + } }) It("check kbcli playground cluster and pod status", func() { checkPlaygroundCluster() @@ -107,19 +135,18 @@ func PlaygroundDestroy() { } func checkPlaygroundCluster() { - commond := "kubectl get pod -n default -l 'app.kubernetes.io/instance in (mycluster)'| grep mycluster |" + - " awk '{print $3}'" - log.Println(commond) Eventually(func(g Gomega) { - podStatus := e2eutil.ExecCommand(commond) - log.Println(e2eutil.StringStrip(podStatus)) - g.Expect(e2eutil.StringStrip(podStatus)).Should(Equal("Running")) + e2eutil.WaitTime(100000) + podStatusResult := e2eutil.CheckPodStatus() + for _, result := range podStatusResult { + g.Expect(result).Should(BeTrue()) + } }, time.Second*180, time.Second*1).Should(Succeed()) cmd := "kbcli cluster list | grep mycluster | awk '{print $6}'" log.Println(cmd) Eventually(func(g Gomega) { clusterStatus := e2eutil.ExecCommand(cmd) - log.Println(e2eutil.StringStrip(clusterStatus)) + log.Println("clusterStatus is " + e2eutil.StringStrip(clusterStatus)) g.Expect(e2eutil.StringStrip(clusterStatus)).Should(Equal("Running")) }, time.Second*360, time.Second*1).Should(Succeed()) } diff --git a/test/e2e/testdata/smoketest/postgresql/00_postgresqlcluster.yaml b/test/e2e/testdata/smoketest/postgresql/00_postgresqlcluster.yaml index f43e24127..058a6f0fd 100644 --- a/test/e2e/testdata/smoketest/postgresql/00_postgresqlcluster.yaml +++ b/test/e2e/testdata/smoketest/postgresql/00_postgresqlcluster.yaml @@ -1,26 +1,120 @@ --- +# Source: pgcluster/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kb-mycluster-pgcluster + labels: + + helm.sh/chart: pgcluster-0.5.1-beta.0 + app.kubernetes.io/name: pgcluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "14.7.1" + app.kubernetes.io/managed-by: Helm +--- +# Source: pgcluster/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-mycluster-pgcluster + namespace: default + labels: + + helm.sh/chart: pgcluster-0.5.1-beta.0 + app.kubernetes.io/name: pgcluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "14.7.1" + app.kubernetes.io/managed-by: Helm +rules: + - apiGroups: + - "" + resources: + - configmaps + verbs: + - create + - get + - list + - patch + - update + - watch + # delete is required only for 'patronictl remove' + - delete + - apiGroups: + - "" + resources: + - endpoints + verbs: + - get + - patch + - update + - create + - list + - watch + # delete is required only for 'patronictl remove' + - delete + - apiGroups: + - "" + resources: + - pods + verbs: + - get + - list + - patch + - update + - watch + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +# Source: pgcluster/templates/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-mycluster-pgcluster + labels: + + helm.sh/chart: pgcluster-0.5.1-beta.0 + app.kubernetes.io/name: pgcluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "14.7.1" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-mycluster-pgcluster +subjects: + - kind: ServiceAccount + name: kb-mycluster-pgcluster + namespace: default +--- # Source: pgcluster/templates/cluster.yaml apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: mycluster + name: mycluster-pgcluster labels: - helm.sh/chart: pgcluster-0.5.0-alpha.3 + helm.sh/chart: pgcluster-0.5.1-beta.0 app.kubernetes.io/name: pgcluster app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "14.7.0" + app.kubernetes.io/version: "14.7.1" app.kubernetes.io/managed-by: Helm spec: clusterDefinitionRef: postgresql # ref clusterdefinition.name - clusterVersionRef: postgresql-14.7.0 # ref clusterversion.name + clusterVersionRef: postgresql-14.7.1 # ref clusterversion.name terminationPolicy: Delete affinity: componentSpecs: - name: postgresql # user-defined - componentDefRef: pg-replication # ref clusterdefinition components.name + componentDefRef: postgresql # ref clusterdefinition components.name monitor: false replicas: 2 + serviceAccountName: kb-mycluster-pgcluster primaryIndex: 0 + switchPolicy: + type: Noop enabledLogs: ["running"] volumeClaimTemplates: - name: data # ref clusterdefinition components.containers.volumeMounts.name diff --git a/test/e2e/testdata/smoketest/postgresql/01_vscale.yaml b/test/e2e/testdata/smoketest/postgresql/01_vscale.yaml index 15085de54..488d51b6c 100644 --- a/test/e2e/testdata/smoketest/postgresql/01_vscale.yaml +++ b/test/e2e/testdata/smoketest/postgresql/01_vscale.yaml @@ -3,7 +3,7 @@ kind: OpsRequest metadata: name: ops-vscale spec: - clusterRef: mycluster + clusterRef: mycluster-pgcluster type: VerticalScaling verticalScaling: - componentName: postgresql diff --git a/test/e2e/testdata/smoketest/postgresql/02_vexpand.yaml b/test/e2e/testdata/smoketest/postgresql/02_vexpand.yaml index ba32370ce..438a5d65e 100644 --- a/test/e2e/testdata/smoketest/postgresql/02_vexpand.yaml +++ b/test/e2e/testdata/smoketest/postgresql/02_vexpand.yaml @@ -3,7 +3,7 @@ kind: OpsRequest metadata: name: ops-vexpand spec: - clusterRef: mycluster + clusterRef: mycluster-pgcluster type: VolumeExpansion volumeExpansion: - componentName: postgresql diff --git a/test/e2e/testdata/smoketest/postgresql/03_cv.yaml b/test/e2e/testdata/smoketest/postgresql/03_cv.yaml deleted file mode 100644 index b87f3bbcc..000000000 --- a/test/e2e/testdata/smoketest/postgresql/03_cv.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterVersion -metadata: - name: postgresql-14.7.0-latest -spec: - clusterDefinitionRef: postgresql - componentVersions: - - componentDefRef: pg-replication - versionsContext: - containers: - - name: postgresql - image: docker.io/apecloud/postgresql:14.7.0 diff --git a/test/e2e/testdata/smoketest/postgresql/03_stop.yaml b/test/e2e/testdata/smoketest/postgresql/03_stop.yaml new file mode 100644 index 000000000..b7432c544 --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/03_stop.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-stop +spec: + clusterRef: mycluster-pgcluster + ttlSecondsAfterSucceed: 5432 + type: Stop + restart: + - componentName: postgresql \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/04_start.yaml b/test/e2e/testdata/smoketest/postgresql/04_start.yaml new file mode 100644 index 000000000..13d612e05 --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/04_start.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-start +spec: + clusterRef: mycluster-pgcluster + ttlSecondsAfterSucceed: 5432 + type: Start + restart: + - componentName: postgresql \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/05_hscale_up.yaml b/test/e2e/testdata/smoketest/postgresql/05_hscale_up.yaml new file mode 100644 index 000000000..79bc8bf2b --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/05_hscale_up.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-hscale-up +spec: + clusterRef: mycluster-pgcluster + type: HorizontalScaling + horizontalScaling: + - componentName: postgresql + replicas: 3 \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/06_backuppolicy.yaml b/test/e2e/testdata/smoketest/postgresql/06_backuppolicy.yaml deleted file mode 100644 index a9a2a82be..000000000 --- a/test/e2e/testdata/smoketest/postgresql/06_backuppolicy.yaml +++ /dev/null @@ -1,25 +0,0 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: BackupPolicy -metadata: - name: backup-policy-mycluster -spec: - backupToolName: xtrabackup-postgresql - hooks: - postCommands: - - rm -f /data/mysql/data/.restore_new_cluster; sync - preCommands: - - touch /data/mysql/data/.restore_new_cluster; sync - remoteVolume: - name: backup-remote-volume - persistentVolumeClaim: - claimName: backup-host-path-pvc - schedule: 0 3 * * * - target: - labelsSelector: - matchLabels: - app.kubernetes.io/instance: mycluster - secret: - name: mycluster-conn-credential - ttl: 168h0m0s - - diff --git a/test/e2e/testdata/smoketest/postgresql/06_hscale_down.yaml b/test/e2e/testdata/smoketest/postgresql/06_hscale_down.yaml new file mode 100644 index 000000000..fbb8ed33c --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/06_hscale_down.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-hscale-down +spec: + clusterRef: mycluster-pgcluster + type: HorizontalScaling + horizontalScaling: + - componentName: postgresql + replicas: 2 \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/07_cv.yaml b/test/e2e/testdata/smoketest/postgresql/07_cv.yaml new file mode 100644 index 000000000..47fd51ac3 --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/07_cv.yaml @@ -0,0 +1,15 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterVersion +metadata: + name: postgresql-14.7.1-latest +spec: + clusterDefinitionRef: postgresql + componentVersions: + - componentDefRef: postgresql + versionsContext: + containers: + - name: postgresql + image: registry.cn-hangzhou.aliyuncs.com/apecloud/spilo:14.7.1 + initContainers: + - image: registry.cn-hangzhou.aliyuncs.com/apecloud/spilo:14.7.1 + name: pg-init-container \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/08_backup_snapshot_restore.yaml b/test/e2e/testdata/smoketest/postgresql/08_backup_snapshot_restore.yaml deleted file mode 100644 index 83df02f3d..000000000 --- a/test/e2e/testdata/smoketest/postgresql/08_backup_snapshot_restore.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - name: snapshot-mycluster -spec: - clusterDefinitionRef: postgresql - clusterVersionRef: postgresql-14.7.0 - terminationPolicy: WipeOut - affinity: - topologyKeys: - - kubernetes.io/hostname - componentSpecs: - - name: postgresql - componentDefRef: pg-replication - monitor: false - replicas: 2 - enabledLogs: ["running"] - volumeClaimTemplates: - - name: data - spec: - storageClassName: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 11Gi - dataSource: - apiGroup: snapshot.storage.k8s.io - kind: VolumeSnapshot - name: backup-sbapshot-mycluster \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/04_upgrade.yaml b/test/e2e/testdata/smoketest/postgresql/08_upgrade.yaml similarity index 59% rename from test/e2e/testdata/smoketest/postgresql/04_upgrade.yaml rename to test/e2e/testdata/smoketest/postgresql/08_upgrade.yaml index abf304c68..7018f4b86 100644 --- a/test/e2e/testdata/smoketest/postgresql/04_upgrade.yaml +++ b/test/e2e/testdata/smoketest/postgresql/08_upgrade.yaml @@ -3,7 +3,7 @@ kind: OpsRequest metadata: name: ops-upgrade spec: - clusterRef: mycluster + clusterRef: mycluster-pgcluster type: Upgrade upgrade: - clusterVersionRef: postgresql-14.7.0-latest \ No newline at end of file + clusterVersionRef: postgresql-14.7.1-latest \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/05_restart.yaml b/test/e2e/testdata/smoketest/postgresql/09_restart.yaml similarity index 84% rename from test/e2e/testdata/smoketest/postgresql/05_restart.yaml rename to test/e2e/testdata/smoketest/postgresql/09_restart.yaml index 331c103b6..dfe4cb3d0 100644 --- a/test/e2e/testdata/smoketest/postgresql/05_restart.yaml +++ b/test/e2e/testdata/smoketest/postgresql/09_restart.yaml @@ -3,7 +3,7 @@ kind: OpsRequest metadata: name: ops-restart spec: - clusterRef: mycluster + clusterRef: mycluster-pgcluster ttlSecondsAfterSucceed: 5432 type: Restart restart: diff --git a/test/e2e/testdata/smoketest/postgresql/07_backup_snapshot.yaml b/test/e2e/testdata/smoketest/postgresql/10_backup_snapshot.yaml similarity index 60% rename from test/e2e/testdata/smoketest/postgresql/07_backup_snapshot.yaml rename to test/e2e/testdata/smoketest/postgresql/10_backup_snapshot.yaml index 65b22b227..4afcd662e 100644 --- a/test/e2e/testdata/smoketest/postgresql/07_backup_snapshot.yaml +++ b/test/e2e/testdata/smoketest/postgresql/10_backup_snapshot.yaml @@ -4,8 +4,7 @@ metadata: labels: app.kubernetes.io/instance: mycluster dataprotection.kubeblocks.io/backup-type: snapshot - name: backup-sbapshot-mycluster + name: backup-snapshot-mycluster spec: - backupPolicyName: backup-policy-mycluster - backupType: snapshot - ttl: 168h0m0s \ No newline at end of file + backupPolicyName: mycluster-pgcluster-postgresql-backup-policy + backupType: snapshot \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/postgresql/11_backup_snapshot_restore.yaml b/test/e2e/testdata/smoketest/postgresql/11_backup_snapshot_restore.yaml new file mode 100644 index 000000000..48ae36239 --- /dev/null +++ b/test/e2e/testdata/smoketest/postgresql/11_backup_snapshot_restore.yaml @@ -0,0 +1,24 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: mycluster-snapshot + annotations: + kubeblocks.io/restore-from-backup: "{\"postgresql\":\"backup-snapshot-mycluster\"}" +spec: + clusterDefinitionRef: postgresql + clusterVersionRef: postgresql-14.7.1 + terminationPolicy: WipeOut + componentSpecs: + - name: postgresql + componentDefRef: postgresql + serviceAccountName: kb-sa-mycluster + monitor: false + replicas: 1 + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 20Gi \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/00_rediscluster.yaml b/test/e2e/testdata/smoketest/redis/00_rediscluster.yaml index dfe5e35a2..404a5493a 100644 --- a/test/e2e/testdata/smoketest/redis/00_rediscluster.yaml +++ b/test/e2e/testdata/smoketest/redis/00_rediscluster.yaml @@ -1,526 +1,112 @@ --- -# Source: redis/templates/configmap.yaml +# Source: redis-cluster/templates/serviceaccount.yaml apiVersion: v1 -kind: ConfigMap +kind: ServiceAccount metadata: - name: redis7-config-template + name: kb-mycluster-redis-cluster labels: - helm.sh/chart: redis-0.5.0-alpha.3 - app.kubernetes.io/name: redis + + helm.sh/chart: redis-cluster-0.5.1-beta.0 + app.kubernetes.io/name: redis-cluster app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" + app.kubernetes.io/version: "7.0.6" app.kubernetes.io/managed-by: Helm -data: - redis.conf: |- - bind 0.0.0.0 - port 6379 - tcp-backlog 511 - timeout 0 - tcp-keepalive 300 - daemonize no - pidfile /var/run/redis_6379.pid - loglevel notice - logfile "/data/running.log" - databases 16 - always-show-logo no - set-proc-title yes - proc-title-template "{title} {listen-addr} {server-mode}" - stop-writes-on-bgsave-error yes - rdbcompression yes - rdbchecksum yes - dbfilename dump.rdb - rdb-del-sync-files no - dir ./ - replica-serve-stale-data yes - replica-read-only yes - repl-diskless-sync yes - repl-diskless-sync-delay 5 - repl-diskless-sync-max-replicas 0 - repl-diskless-load disabled - repl-disable-tcp-nodelay no - replica-priority 100 - acllog-max-len 128 - lazyfree-lazy-eviction no - lazyfree-lazy-expire no - lazyfree-lazy-server-del no - replica-lazy-flush no - lazyfree-lazy-user-del no - lazyfree-lazy-user-flush no - oom-score-adj no - oom-score-adj-values 0 200 800 - disable-thp yes - appendonly yes - appendfilename "appendonly.aof" - appenddirname "appendonlydir" - appendfsync everysec - no-appendfsync-on-rewrite no - auto-aof-rewrite-percentage 100 - auto-aof-rewrite-min-size 64mb - aof-load-truncated yes - aof-use-rdb-preamble yes - aof-timestamp-enabled no - slowlog-log-slower-than 10000 - slowlog-max-len 128 - latency-monitor-threshold 0 - notify-keyspace-events "" - hash-max-listpack-entries 512 - hash-max-listpack-value 64 - list-max-listpack-size -2 - list-compress-depth 0 - set-max-intset-entries 512 - zset-max-listpack-entries 128 - zset-max-listpack-value 64 - hll-sparse-max-bytes 3000 - stream-node-max-bytes 4096 - stream-node-max-entries 100 - activerehashing yes - client-output-buffer-limit normal 0 0 0 - client-output-buffer-limit replica 256mb 64mb 60 - client-output-buffer-limit pubsub 32mb 8mb 60 - hz 10 - dynamic-hz yes - aof-rewrite-incremental-fsync yes - rdb-save-incremental-fsync yes - jemalloc-bg-thread yes --- -# Source: redis/templates/scripts.yaml -apiVersion: v1 -kind: ConfigMap +# Source: redis-cluster/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role metadata: - name: redis-scripts -data: - setup.sh: | - #!/bin/sh - set -ex - KB_PRIMARY_POD_NAME_PREFIX=${KB_PRIMARY_POD_NAME%%\.*} - if [ "$KB_PRIMARY_POD_NAME_PREFIX" = "$KB_POD_NAME" ]; then - echo "primary instance skip create a replication relationship." - exit 0 - else - until redis-cli -h $KB_PRIMARY_POD_NAME -p 6379 ping; do sleep 1; done - redis-cli -h 127.0.0.1 -p 6379 replicaof $KB_PRIMARY_POD_NAME 6379 || exit 1 - fi ---- -# Source: redis/templates/backuppolicytemplate.yaml -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: BackupPolicyTemplate -metadata: - name: backup-policy-template-redis + name: kb-mycluster-redis-cluster + namespace: default labels: - clusterdefinition.kubeblocks.io/name: redis - helm.sh/chart: redis-0.5.0-alpha.3 - app.kubernetes.io/name: redis - app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" - app.kubernetes.io/managed-by: Helm -spec: - # which backup tool to perform database backup, only support one tool. - backupToolName: volumesnapshot - ttl: 168h0m0s ---- -# Source: redis/templates/clusterdefinition.yaml -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterDefinition -metadata: - name: redis - labels: - helm.sh/chart: redis-0.5.0-alpha.3 - app.kubernetes.io/name: redis - app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" - app.kubernetes.io/managed-by: Helm -spec: - connectionCredential: - username: "" - password: "" - endpoint: "$(SVC_FQDN):$(SVC_PORT_redis)" - host: "$(SVC_FQDN)" - port: "$(SVC_PORT_redis)" - componentDefs: - - name: redis - workloadType: Replication - characterType: redis - replicationSpec: - switchPolicies: - - type: MaximumAvailability - switchStatements: - demote: - - replicaof $KB_NEW_PRIMARY_ROLE_NAME 6379 - promote: - - replicaof no one - follow: - - replicaof $KB_NEW_PRIMARY_ROLE_NAME 6379 - - type: MaximumDataProtection - switchStatements: - demote: - - replicaof $KB_NEW_PRIMARY_ROLE_NAME 6379 - promote: - - replicaof no one - follow: - - replicaof $KB_NEW_PRIMARY_ROLE_NAME 6379 - switchCmdExecutorConfig: - image: redis:7.0.5 - switchSteps: - - role: NewPrimary - command: - - /bin/sh - - -c - args: - - redis-cli -h $(KB_SWITCH_ROLE_ENDPOINT) -p 6379 $(KB_SWITCH_PROMOTE_STATEMENT) - - role: Secondaries - command: - - /bin/sh - - -c - args: - - redis-cli -h $(KB_SWITCH_ROLE_ENDPOINT) -p 6379 $(KB_SWITCH_FOLLOW_STATEMENT) - - role: OldPrimary - command: - - /bin/sh - - -c - args: - - redis-cli -h $(KB_SWITCH_ROLE_ENDPOINT) -p 6379 $(KB_SWITCH_DEMOTE_STATEMENT) - service: - ports: - - protocol: TCP - port: 6379 - configSpecs: - - name: redis-replication-config - templateRef: redis7-config-template - constraintRef: redis7-config-constraints - namespace: default - volumeName: redis-config - scriptSpecs: - - name: redis-scripts - templateRef: redis-scripts - namespace: default - volumeName: scripts - defaultMode: 493 - monitor: - builtIn: false - exporterConfig: - scrapePort: 9121 - scrapePath: "/metrics" - logConfigs: - - name: running - filePathPattern: /data/running.log - volumeTypes: - - name: data - type: data - podSpec: - containers: - - name: redis - image: redis:7.0.5 - ports: - - name: redis - containerPort: 6379 - volumeMounts: - - name: data - mountPath: /data - - name: redis-config - mountPath: /etc/conf - - name: scripts - mountPath: /scripts - args: [ "/etc/conf/redis.conf" ] - lifecycle: - postStart: - exec: - command: ["/scripts/setup.sh"] - - name: redis-exporter - image: oliver006/redis_exporter:latest - imagePullPolicy: IfNotPresent - resources: - requests: - cpu: 100m - memory: 100Mi - ports: - - name: metrics - containerPort: 9121 - protocol: TCP - livenessProbe: - httpGet: - path: / - port: 9121 - readinessProbe: - httpGet: - path: / - port: 9121 - systemAccounts: -# Seems redis-cli has its own mechanism to parse input tokens and there is no elegent way -# to pass $(KB_ACCOUNT_STATEMENT) to redis-cli without causing parsing error. -# Instead, using a shell script to wrap redis-cli and pass $(KB_ACCOUNT_STATEMENT) to it will do. - cmdExecutorConfig: - image: docker.io/redis:7.0.5 - command: - - sh - - -c - args: - - "redis-cli -h $(KB_ACCOUNT_ENDPOINT) $(KB_ACCOUNT_STATEMENT)" - passwordConfig: - length: 10 - numDigits: 5 - numSymbols: 0 - letterCase: MixedCases - accounts: - - name: kbadmin - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allcommands allkeys - - name: kbdataprotection - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allcommands allkeys - - name: kbmonitoring - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allkeys +get - - name: kbprobe - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) allkeys +get - - name: kbreplicator - provisionPolicy: - type: CreateByStmt - scope: AnyPods - statements: - creation: ACL SETUSER $(USERNAME) ON >$(PASSWD) +psync +replconf +ping ---- -# Source: redis/templates/clusterversion.yaml -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterVersion -metadata: - name: redis-7.0.5 - labels: - helm.sh/chart: redis-0.5.0-alpha.3 - app.kubernetes.io/name: redis + + helm.sh/chart: redis-cluster-0.5.1-beta.0 + app.kubernetes.io/name: redis-cluster app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" + app.kubernetes.io/version: "7.0.6" app.kubernetes.io/managed-by: Helm -spec: - clusterDefinitionRef: redis - componentVersions: - - componentDefRef: redis - versionsContext: - containers: - - name: redis - image: redis:7.0.5 - imagePullPolicy: IfNotPresent +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create --- -# Source: redis/templates/configconstraint.yaml -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ConfigConstraint +# Source: redis-cluster/templates/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding metadata: - name: redis7-config-constraints + name: kb-mycluster-redis-cluster labels: - helm.sh/chart: redis-0.5.0-alpha.3 - app.kubernetes.io/name: redis + + helm.sh/chart: redis-cluster-0.5.1-beta.0 + app.kubernetes.io/name: redis-cluster app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" + app.kubernetes.io/version: "7.0.6" app.kubernetes.io/managed-by: Helm -spec: - - cfgSchemaTopLevelName: RedisParameter - - # ConfigurationSchema that impose restrictions on engine parameter's rule - configurationSchema: - cue: |- - // Copyright ApeCloud, Inc. - // - // 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. - - #RedisParameter: { - - "acllog-max-len": int & >=1 & <=10000 | *128 - - "acl-pubsub-default"?: string & "resetchannels" | "allchannels" - - activedefrag?: string & "yes" | "no" - - "active-defrag-cycle-max": int & >=1 & <=75 | *75 - - "active-defrag-cycle-min": int & >=1 & <=75 | *5 - - "active-defrag-ignore-bytes": int | *104857600 - - "active-defrag-max-scan-fields": int & >=1 & <=1000000 | *1000 - - "active-defrag-threshold-lower": int & >=1 & <=100 | *10 - - "active-defrag-threshold-upper": int & >=1 & <=100 | *100 - - "active-expire-effort": int & >=1 & <=10 | *1 - - appendfsync?: string & "always" | "everysec" | "no" - - appendonly?: string & "yes" | "no" - - "client-output-buffer-limit-normal-hard-limit": int | *0 - - "client-output-buffer-limit-normal-soft-limit": int | *0 - - "client-output-buffer-limit-normal-soft-seconds": int | *0 - - "client-output-buffer-limit-pubsub-hard-limit": int | *33554432 - - "client-output-buffer-limit-pubsub-soft-limit": int | *8388608 - - "client-output-buffer-limit-pubsub-soft-seconds": int | *60 - - "client-output-buffer-limit-replica-soft-seconds": int | *60 - - "client-query-buffer-limit": int & >=1048576 & <=1073741824 | *1073741824 - - "close-on-replica-write"?: string & "yes" | "no" - - "cluster-allow-pubsubshard-when-down"?: string & "yes" | "no" - - "cluster-allow-reads-when-down"?: string & "yes" | "no" - - "cluster-enabled"?: string & "yes" | "no" - - "cluster-preferred-endpoint-type"?: string & "tls-dynamic" | "ip" - - "cluster-require-full-coverage"?: string & "yes" | "no" - - databases: int & >=1 & <=10000 | *16 - - "hash-max-listpack-entries": int | *512 - - "hash-max-listpack-value": int | *64 - - "hll-sparse-max-bytes": int & >=1 & <=16000 | *3000 - - "latency-tracking"?: string & "yes" | "no" - - "lazyfree-lazy-eviction"?: string & "yes" | "no" - - "lazyfree-lazy-expire"?: string & "yes" | "no" - - "lazyfree-lazy-server-del"?: string & "yes" | "no" - - "lazyfree-lazy-user-del"?: string & "yes" | "no" - - "lfu-decay-time": int | *1 - - "lfu-log-factor": int | *10 - - "list-compress-depth": int | *0 - - "list-max-listpack-size": int | *-2 - - "lua-time-limit": int & 5000 | *5000 - - maxclients: int & >=1 & <=65000 | *65000 - - "maxmemory-policy"?: string & "volatile-lru" | "allkeys-lru" | "volatile-lfu" | "allkeys-lfu" | "volatile-random" | "allkeys-random" | "volatile-ttl" | "noeviction" - - "maxmemory-samples": int | *3 - - "min-replicas-max-lag": int | *10 - - "min-replicas-to-write": int | *0 - - "notify-keyspace-events"?: string - - "proto-max-bulk-len": int & >=1048576 & <=536870912 | *536870912 - - "rename-commands"?: string & "APPEND" | "BITCOUNT" | "BITFIELD" | "BITOP" | "BITPOS" | "BLPOP" | "BRPOP" | "BRPOPLPUSH" | "BZPOPMIN" | "BZPOPMAX" | "CLIENT" | "COMMAND" | "DBSIZE" | "DECR" | "DECRBY" | "DEL" | "DISCARD" | "DUMP" | "ECHO" | "EVAL" | "EVALSHA" | "EXEC" | "EXISTS" | "EXPIRE" | "EXPIREAT" | "FLUSHALL" | "FLUSHDB" | "GEOADD" | "GEOHASH" | "GEOPOS" | "GEODIST" | "GEORADIUS" | "GEORADIUSBYMEMBER" | "GET" | "GETBIT" | "GETRANGE" | "GETSET" | "HDEL" | "HEXISTS" | "HGET" | "HGETALL" | "HINCRBY" | "HINCRBYFLOAT" | "HKEYS" | "HLEN" | "HMGET" | "HMSET" | "HSET" | "HSETNX" | "HSTRLEN" | "HVALS" | "INCR" | "INCRBY" | "INCRBYFLOAT" | "INFO" | "KEYS" | "LASTSAVE" | "LINDEX" | "LINSERT" | "LLEN" | "LPOP" | "LPUSH" | "LPUSHX" | "LRANGE" | "LREM" | "LSET" | "LTRIM" | "MEMORY" | "MGET" | "MONITOR" | "MOVE" | "MSET" | "MSETNX" | "MULTI" | "OBJECT" | "PERSIST" | "PEXPIRE" | "PEXPIREAT" | "PFADD" | "PFCOUNT" | "PFMERGE" | "PING" | "PSETEX" | "PSUBSCRIBE" | "PUBSUB" | "PTTL" | "PUBLISH" | "PUNSUBSCRIBE" | "RANDOMKEY" | "READONLY" | "READWRITE" | "RENAME" | "RENAMENX" | "RESTORE" | "ROLE" | "RPOP" | "RPOPLPUSH" | "RPUSH" | "RPUSHX" | "SADD" | "SCARD" | "SCRIPT" | "SDIFF" | "SDIFFSTORE" | "SELECT" | "SET" | "SETBIT" | "SETEX" | "SETNX" | "SETRANGE" | "SINTER" | "SINTERSTORE" | "SISMEMBER" | "SLOWLOG" | "SMEMBERS" | "SMOVE" | "SORT" | "SPOP" | "SRANDMEMBER" | "SREM" | "STRLEN" | "SUBSCRIBE" | "SUNION" | "SUNIONSTORE" | "SWAPDB" | "TIME" | "TOUCH" | "TTL" | "TYPE" | "UNSUBSCRIBE" | "UNLINK" | "UNWATCH" | "WAIT" | "WATCH" | "ZADD" | "ZCARD" | "ZCOUNT" | "ZINCRBY" | "ZINTERSTORE" | "ZLEXCOUNT" | "ZPOPMAX" | "ZPOPMIN" | "ZRANGE" | "ZRANGEBYLEX" | "ZREVRANGEBYLEX" | "ZRANGEBYSCORE" | "ZRANK" | "ZREM" | "ZREMRANGEBYLEX" | "ZREMRANGEBYRANK" | "ZREMRANGEBYSCORE" | "ZREVRANGE" | "ZREVRANGEBYSCORE" | "ZREVRANK" | "ZSCORE" | "ZUNIONSTORE" | "SCAN" | "SSCAN" | "HSCAN" | "ZSCAN" | "XINFO" | "XADD" | "XTRIM" | "XDEL" | "XRANGE" | "XREVRANGE" | "XLEN" | "XREAD" | "XGROUP" | "XREADGROUP" | "XACK" | "XCLAIM" | "XPENDING" | "GEORADIUS_RO" | "GEORADIUSBYMEMBER_RO" | "LOLWUT" | "XSETID" | "SUBSTR" | "BITFIELD_RO" | "ACL" | "STRALGO" - - "repl-backlog-size": int | *1048576 - - "repl-backlog-ttl": int | *3600 - - "replica-allow-chaining"?: string & "yes" | "no" - - "replica-ignore-maxmemory"?: string & "yes" | "no" - - "replica-lazy-flush"?: string & "yes" | "no" - - "reserved-memory-percent": int & >=0 & <=100 | *25 - - "set-max-intset-entries": int & >=0 & <=500000000 | *512 - - "slowlog-log-slower-than": int | *10000 - - "slowlog-max-len": int | *128 - - "stream-node-max-bytes": int | *4096 - - "stream-node-max-entries": int | *100 - - "tcp-keepalive": int | *300 - - timeout: int | *0 - - "tracking-table-max-keys": int & >=1 & <=100000000 | *1000000 - - "zset-max-listpack-entries": int | *128 - - "zset-max-listpack-value": int | *64 - - ... - } - - - ## require db instance restart - staticParameters: - - cluster-enabled - - databases - - maxclients - - ## reload parameters - ## dynamicParameters - dynamicParameters: - - - # redis configuration file format - formatterConfig: - format: redis +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-mycluster-redis-cluster +subjects: + - kind: ServiceAccount + name: kb-mycluster-redis-cluster + namespace: default --- # Source: redis-cluster/templates/cluster.yaml apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: mycluster + name: mycluster-redis-cluster labels: - helm.sh/chart: redis-cluster-0.5.0-alpha.3 + helm.sh/chart: redis-cluster-0.5.1-beta.0 app.kubernetes.io/name: redis-cluster app.kubernetes.io/instance: mycluster - app.kubernetes.io/version: "7.0.5" + app.kubernetes.io/version: "7.0.6" app.kubernetes.io/managed-by: Helm spec: clusterDefinitionRef: redis # ref clusterDefinition.name - clusterVersionRef: redis-7.0.5 # ref clusterVersion.name + clusterVersionRef: redis-7.0.6 # ref clusterVersion.name terminationPolicy: Delete affinity: topologyKeys: - kubernetes.io/hostname componentSpecs: - - name: redis-repl # user-defined + - name: redis # user-defined componentDefRef: redis # ref clusterDefinition componentDefs.name monitor: false enabledLogs: ["running"] replicas: 2 + serviceAccountName: kb-mycluster-redis-cluster primaryIndex: 0 switchPolicy: - type: MaximumAvailability + type: Noop + resources: + limits: + cpu: "500m" + memory: "3Gi" + requests: + cpu: "500m" + memory: "1Gi" + volumeClaimTemplates: + - name: data # ref clusterdefinition components.containers.volumeMounts.name + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 1Gi + - name: redis-sentinel # user-defined + componentDefRef: redis-sentinel # ref clusterDefinition componentDefs.name + replicas: 3 + resources: + limits: + cpu: "500m" + memory: "3Gi" + requests: + cpu: "500m" + memory: "1Gi" volumeClaimTemplates: - name: data # ref clusterdefinition components.containers.volumeMounts.name spec: diff --git a/test/e2e/testdata/smoketest/redis/01_vscale.yaml b/test/e2e/testdata/smoketest/redis/01_vscale.yaml index 18ddbd8de..6997618ce 100644 --- a/test/e2e/testdata/smoketest/redis/01_vscale.yaml +++ b/test/e2e/testdata/smoketest/redis/01_vscale.yaml @@ -3,10 +3,10 @@ kind: OpsRequest metadata: name: ops-vscale spec: - clusterRef: mycluster + clusterRef: mycluster-redis-cluster type: VerticalScaling verticalScaling: - - componentName: redis-repl + - componentName: redis requests: memory: "500Mi" cpu: "500m" diff --git a/test/e2e/testdata/smoketest/redis/02_hscale_up.yaml b/test/e2e/testdata/smoketest/redis/02_hscale_up.yaml index e7897b2e7..1e6fa4e39 100644 --- a/test/e2e/testdata/smoketest/redis/02_hscale_up.yaml +++ b/test/e2e/testdata/smoketest/redis/02_hscale_up.yaml @@ -1,10 +1,10 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: OpsRequest metadata: - name: ops-hscale + name: ops-hscale-up spec: - clusterRef: mycluster + clusterRef: mycluster-redis-cluster type: HorizontalScaling horizontalScaling: - - componentName: redis-repl + - componentName: redis replicas: 3 \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/03_hscale_down.yaml b/test/e2e/testdata/smoketest/redis/03_hscale_down.yaml index feeebb615..c5be5d225 100644 --- a/test/e2e/testdata/smoketest/redis/03_hscale_down.yaml +++ b/test/e2e/testdata/smoketest/redis/03_hscale_down.yaml @@ -1,10 +1,10 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: OpsRequest metadata: - name: ops-hscale + name: ops-hscale-down spec: - clusterRef: mycluster + clusterRef: mycluster-redis-cluster type: HorizontalScaling horizontalScaling: - - componentName: redis-repl + - componentName: redis replicas: 2 \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/04_cv.yaml b/test/e2e/testdata/smoketest/redis/04_cv.yaml deleted file mode 100644 index a66882dfd..000000000 --- a/test/e2e/testdata/smoketest/redis/04_cv.yaml +++ /dev/null @@ -1,12 +0,0 @@ -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: ClusterVersion -metadata: - name: redis-7.0.5-latest -spec: - clusterDefinitionRef: redis - componentVersions: - - componentDefRef: redis - versionsContext: - containers: - - name: redis - image: docker.io/apecloud/redis:latest diff --git a/test/e2e/testdata/smoketest/redis/04_stop.yaml b/test/e2e/testdata/smoketest/redis/04_stop.yaml new file mode 100644 index 000000000..3b80455c7 --- /dev/null +++ b/test/e2e/testdata/smoketest/redis/04_stop.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-stop +spec: + clusterRef: mycluster-redis-cluster + ttlSecondsAfterSucceed: 3600 + type: Stop + restart: + - componentName: redis \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/05_start.yaml b/test/e2e/testdata/smoketest/redis/05_start.yaml new file mode 100644 index 000000000..78c5539e7 --- /dev/null +++ b/test/e2e/testdata/smoketest/redis/05_start.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-start +spec: + clusterRef: mycluster-redis-cluster + ttlSecondsAfterSucceed: 3600 + type: Start + restart: + - componentName: redis \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/06_restart.yaml b/test/e2e/testdata/smoketest/redis/06_restart.yaml index e9a31e1e2..993d4135a 100644 --- a/test/e2e/testdata/smoketest/redis/06_restart.yaml +++ b/test/e2e/testdata/smoketest/redis/06_restart.yaml @@ -3,8 +3,8 @@ kind: OpsRequest metadata: name: ops-restart spec: - clusterRef: mycluster + clusterRef: mycluster-redis-cluster ttlSecondsAfterSucceed: 3600 type: Restart restart: - - componentName: redis-repl \ No newline at end of file + - componentName: redis \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/07_vexpand.yaml b/test/e2e/testdata/smoketest/redis/07_vexpand.yaml new file mode 100644 index 000000000..8e97ff4f2 --- /dev/null +++ b/test/e2e/testdata/smoketest/redis/07_vexpand.yaml @@ -0,0 +1,12 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-vexpand +spec: + clusterRef: mycluster-redis-cluster + type: VolumeExpansion + volumeExpansion: + - componentName: postgresql + volumeClaimTemplates: + - name: data + storage: "11Gi" \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/08_cv.yaml b/test/e2e/testdata/smoketest/redis/08_cv.yaml new file mode 100644 index 000000000..93848ebf0 --- /dev/null +++ b/test/e2e/testdata/smoketest/redis/08_cv.yaml @@ -0,0 +1,26 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ClusterVersion +metadata: + name: redis-7.0.6-latest +spec: + clusterDefinitionRef: redis + componentVersions: + - componentDefRef: redis + versionsContext: + containers: + - image: redis/redis-stack-server:7.0.6-RC8 + imagePullPolicy: IfNotPresent + name: redis + resources: {} + - componentDefRef: redis-sentinel + versionsContext: + containers: + - image: redis/redis-stack-server:7.0.6-RC8 + imagePullPolicy: IfNotPresent + name: redis-sentinel + resources: {} + initContainers: + - image: redis/redis-stack-server:7.0.6-RC8 + imagePullPolicy: IfNotPresent + name: init-redis-sentinel + resources: {} \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/redis/05_upgrade.yaml b/test/e2e/testdata/smoketest/redis/09_upgrade.yaml similarity index 60% rename from test/e2e/testdata/smoketest/redis/05_upgrade.yaml rename to test/e2e/testdata/smoketest/redis/09_upgrade.yaml index d4935e0b7..a789b1995 100644 --- a/test/e2e/testdata/smoketest/redis/05_upgrade.yaml +++ b/test/e2e/testdata/smoketest/redis/09_upgrade.yaml @@ -3,7 +3,7 @@ kind: OpsRequest metadata: name: ops-upgrade spec: - clusterRef: mycluster + clusterRef: mycluster-redis-cluster type: Upgrade upgrade: - clusterVersionRef: redis-7.0.5-latest \ No newline at end of file + clusterVersionRef: redis-7.0.6-latest \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/smoketestrun.go b/test/e2e/testdata/smoketest/smoketestrun.go index 3429ef371..879a884d2 100644 --- a/test/e2e/testdata/smoketest/smoketestrun.go +++ b/test/e2e/testdata/smoketest/smoketestrun.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -90,6 +90,13 @@ func SmokeTest() { } } }) + It("check addon", func() { + enabledSc := " kbcli addon enable csi-hostpath-driver" + log.Println(enabledSc) + csi := e2eutil.ExecCommand(enabledSc) + log.Println(csi) + }) + It("run test cases", func() { dir, err := os.Getwd() if err != nil { @@ -122,9 +129,11 @@ func SmokeTest() { func runTestCases(files []string) { for _, file := range files { By("test " + file) - b := e2eutil.OpsYaml(file, "apply") + + b := e2eutil.OpsYaml(file, "create") Expect(b).Should(BeTrue()) Eventually(func(g Gomega) { + e2eutil.WaitTime(100000) podStatusResult := e2eutil.CheckPodStatus() for _, result := range podStatusResult { g.Expect(result).Should(BeTrue()) @@ -133,7 +142,7 @@ func runTestCases(files []string) { Eventually(func(g Gomega) { clusterStatusResult := e2eutil.CheckClusterStatus() g.Expect(clusterStatusResult).Should(BeTrue()) - }, time.Second*180, time.Second*1).Should(Succeed()) + }, time.Second*300, time.Second*1).Should(Succeed()) } if len(files) > 0 { diff --git a/test/e2e/testdata/smoketest/wesql/00_wesqlcluster.yaml b/test/e2e/testdata/smoketest/wesql/00_wesqlcluster.yaml index 0de7e31dd..e9646d56d 100644 --- a/test/e2e/testdata/smoketest/wesql/00_wesqlcluster.yaml +++ b/test/e2e/testdata/smoketest/wesql/00_wesqlcluster.yaml @@ -1,11 +1,66 @@ --- +# Source: apecloud-mysql-cluster/templates/serviceaccount.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: kb-mycluster-apecloud-mysql-cluster + labels: + + helm.sh/chart: apecloud-mysql-cluster-0.5.1-beta.0 + app.kubernetes.io/name: apecloud-mysql-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "8.0.30" + app.kubernetes.io/managed-by: Helm +--- +# Source: apecloud-mysql-cluster/templates/role.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: kb-mycluster-apecloud-mysql-cluster + namespace: default + labels: + + helm.sh/chart: apecloud-mysql-cluster-0.5.1-beta.0 + app.kubernetes.io/name: apecloud-mysql-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "8.0.30" + app.kubernetes.io/managed-by: Helm +rules: + - apiGroups: + - "" + resources: + - events + verbs: + - create +--- +# Source: apecloud-mysql-cluster/templates/rolebinding.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: kb-mycluster-apecloud-mysql-cluster + labels: + + helm.sh/chart: apecloud-mysql-cluster-0.5.1-beta.0 + app.kubernetes.io/name: apecloud-mysql-cluster + app.kubernetes.io/instance: mycluster + app.kubernetes.io/version: "8.0.30" + app.kubernetes.io/managed-by: Helm +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: kb-mycluster-apecloud-mysql-cluster +subjects: + - kind: ServiceAccount + name: kb-mycluster-apecloud-mysql-cluster + namespace: default +--- # Source: apecloud-mysql-cluster/templates/cluster.yaml apiVersion: apps.kubeblocks.io/v1alpha1 kind: Cluster metadata: - name: mycluster + name: mycluster-apecloud-mysql-cluster labels: - helm.sh/chart: apecloud-mysql-cluster-0.5.0-alpha.3 + helm.sh/chart: apecloud-mysql-cluster-0.5.1-beta.0 app.kubernetes.io/name: apecloud-mysql-cluster app.kubernetes.io/instance: mycluster app.kubernetes.io/version: "8.0.30" @@ -22,6 +77,7 @@ spec: componentDefRef: mysql # ref clusterdefinition componentDefs.name monitor: false replicas: 3 + serviceAccountName: kb-mycluster-apecloud-mysql-cluster enabledLogs: ["slow","error"] volumeClaimTemplates: - name: data # ref clusterdefinition components.containers.volumeMounts.name diff --git a/test/e2e/testdata/smoketest/wesql/01_componentresourceconstraint_custom.yaml b/test/e2e/testdata/smoketest/wesql/01_componentresourceconstraint_custom.yaml new file mode 100644 index 000000000..85c63d336 --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/01_componentresourceconstraint_custom.yaml @@ -0,0 +1,42 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ComponentResourceConstraint +metadata: + annotations: + meta.helm.sh/release-name: kubeblocks + meta.helm.sh/release-namespace: default + labels: + app.kubernetes.io/managed-by: Helm + resourceconstraint.kubeblocks.io/provider: kubeblocks + name: kb-resource-constraint-custom +spec: + constraints: + - cpu: + max: 2 + min: "0.1" + step: "0.1" + memory: + sizePerCPU: 1Gi + - cpu: + max: 2 + min: "0.1" + step: "0.1" + memory: + sizePerCPU: 2Gi + - cpu: + max: 2 + min: "0.1" + step: "0.1" + memory: + sizePerCPU: 3Gi + - cpu: + max: 2 + min: "0.1" + step: "0.1" + memory: + sizePerCPU: 4Gi + - cpu: + max: 2 + min: "0.1" + step: "0.1" + memory: + sizePerCPU: 5Gi \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/02_custom_class.yaml b/test/e2e/testdata/smoketest/wesql/02_custom_class.yaml new file mode 100644 index 000000000..6529601dd --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/02_custom_class.yaml @@ -0,0 +1,19 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: ComponentClassDefinition +metadata: + name: custom-class + labels: + class.kubeblocks.io/provider: kubeblocks + apps.kubeblocks.io/component-def-ref: mysql + clusterdefinition.kubeblocks.io/name: apecloud-mysql +spec: + groups: + - resourceConstraintRef: kb-resource-constraint-custom + template: | + cpu: "{{ or .cpu 1 }}" + memory: "{{ or .memory 4 }}Gi" + vars: [ cpu, memory] + series: + - namingTemplate: "general-{{ .cpu }}c{{ .memory }}g" + classes: + - args: [ "0.2", "0.6"] \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/01_vscale.yaml b/test/e2e/testdata/smoketest/wesql/03_vscale.yaml similarity index 62% rename from test/e2e/testdata/smoketest/wesql/01_vscale.yaml rename to test/e2e/testdata/smoketest/wesql/03_vscale.yaml index 59b66036f..e88906fe4 100644 --- a/test/e2e/testdata/smoketest/wesql/01_vscale.yaml +++ b/test/e2e/testdata/smoketest/wesql/03_vscale.yaml @@ -7,9 +7,5 @@ spec: type: VerticalScaling verticalScaling: - componentName: mysql - requests: - memory: "500Mi" - cpu: "0.5" - limits: - memory: "1000Mi" - cpu: "1" \ No newline at end of file + class: general-0.2c0.6g + diff --git a/test/e2e/testdata/smoketest/wesql/02_hscale.yaml b/test/e2e/testdata/smoketest/wesql/04_hscale_up.yaml similarity index 89% rename from test/e2e/testdata/smoketest/wesql/02_hscale.yaml rename to test/e2e/testdata/smoketest/wesql/04_hscale_up.yaml index 592bdb325..1b50f73f9 100644 --- a/test/e2e/testdata/smoketest/wesql/02_hscale.yaml +++ b/test/e2e/testdata/smoketest/wesql/04_hscale_up.yaml @@ -1,7 +1,7 @@ apiVersion: apps.kubeblocks.io/v1alpha1 kind: OpsRequest metadata: - name: ops-hscale + name: ops-hscale-up spec: clusterRef: mycluster type: HorizontalScaling diff --git a/test/e2e/testdata/smoketest/wesql/05_hscale_down.yaml b/test/e2e/testdata/smoketest/wesql/05_hscale_down.yaml new file mode 100644 index 000000000..85fb23836 --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/05_hscale_down.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-hscale-down +spec: + clusterRef: mycluster + type: HorizontalScaling + horizontalScaling: + - componentName: mysql + replicas: 3 \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/03_vexpand.yaml b/test/e2e/testdata/smoketest/wesql/06_vexpand.yaml similarity index 100% rename from test/e2e/testdata/smoketest/wesql/03_vexpand.yaml rename to test/e2e/testdata/smoketest/wesql/06_vexpand.yaml diff --git a/test/e2e/testdata/smoketest/wesql/07_backuppolicy_snapshot.yaml b/test/e2e/testdata/smoketest/wesql/07_backuppolicy_snapshot.yaml deleted file mode 100644 index 66fd6c2b9..000000000 --- a/test/e2e/testdata/smoketest/wesql/07_backuppolicy_snapshot.yaml +++ /dev/null @@ -1,24 +0,0 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: BackupPolicy -metadata: - name: backup-policy-mycluster -spec: - backupToolName: xtrabackup-mysql - hooks: - postCommands: - - rm -f /data/mysql/data/.restore_new_cluster; sync - preCommands: - - touch /data/mysql/data/.restore_new_cluster; sync - remoteVolume: - name: backup-remote-volume - persistentVolumeClaim: - claimName: backup-host-path-pvc - schedule: 0 3 * * * - target: - labelsSelector: - matchLabels: - app.kubernetes.io/instance: mycluster - secret: - name: mycluster-conn-credential - ttl: 168h0m0s - diff --git a/test/e2e/testdata/smoketest/wesql/04_cv.yaml b/test/e2e/testdata/smoketest/wesql/07_cv.yaml similarity index 100% rename from test/e2e/testdata/smoketest/wesql/04_cv.yaml rename to test/e2e/testdata/smoketest/wesql/07_cv.yaml diff --git a/test/e2e/testdata/smoketest/wesql/05_upgrade.yaml b/test/e2e/testdata/smoketest/wesql/08_upgrade.yaml similarity index 100% rename from test/e2e/testdata/smoketest/wesql/05_upgrade.yaml rename to test/e2e/testdata/smoketest/wesql/08_upgrade.yaml diff --git a/test/e2e/testdata/smoketest/wesql/09_backup_snapshot_restore.yaml b/test/e2e/testdata/smoketest/wesql/09_backup_snapshot_restore.yaml deleted file mode 100644 index c8c7748ce..000000000 --- a/test/e2e/testdata/smoketest/wesql/09_backup_snapshot_restore.yaml +++ /dev/null @@ -1,30 +0,0 @@ -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - name: snapshot-mycluster -spec: - clusterDefinitionRef: apecloud-mysql - clusterVersionRef: ac-mysql-8.0.30 - terminationPolicy: WipeOut - affinity: - topologyKeys: - - kubernetes.io/hostname - componentSpecs: - - name: mysql - componentDefRef: mysql - monitor: false - replicas: 3 - enabledLogs: [ "slow","error" ] - volumeClaimTemplates: - - name: data - spec: - storageClassName: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi - dataSource: - apiGroup: snapshot.storage.k8s.io - kind: VolumeSnapshot - name: backup-sbapshot-mycluster \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/09_stop.yaml b/test/e2e/testdata/smoketest/wesql/09_stop.yaml new file mode 100644 index 000000000..8b039e4d6 --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/09_stop.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-stop +spec: + clusterRef: mycluster + ttlSecondsAfterSucceed: 3600 + type: Stop + restart: + - componentName: mysql \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/10_start.yaml b/test/e2e/testdata/smoketest/wesql/10_start.yaml new file mode 100644 index 000000000..406a567bd --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/10_start.yaml @@ -0,0 +1,10 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: OpsRequest +metadata: + name: ops-start +spec: + clusterRef: mycluster + ttlSecondsAfterSucceed: 3600 + type: Start + restart: + - componentName: mysql \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/11_backuppolicy_full.yaml b/test/e2e/testdata/smoketest/wesql/11_backuppolicy_full.yaml deleted file mode 100644 index 587b00f2c..000000000 --- a/test/e2e/testdata/smoketest/wesql/11_backuppolicy_full.yaml +++ /dev/null @@ -1,26 +0,0 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: BackupPolicy -metadata: - name: backup-policy-mycluster-full -spec: - backupToolName: xtrabackup-mysql - backupType: full - hooks: - postCommands: - - rm -f /data/mysql/data/.restore_new_cluster; sync - preCommands: - - touch /data/mysql/data/.restore_new_cluster; sync - remoteVolume: - name: backup-remote-volume - persistentVolumeClaim: - claimName: backup-host-path-pvc - schedule: 0 3 * * * - target: - labelsSelector: - matchLabels: - app.kubernetes.io/instance: mycluster - secret: - name: mycluster-conn-credential - ttl: 168h0m0s - - diff --git a/test/e2e/testdata/smoketest/wesql/06_restart.yaml b/test/e2e/testdata/smoketest/wesql/11_restart.yaml similarity index 100% rename from test/e2e/testdata/smoketest/wesql/06_restart.yaml rename to test/e2e/testdata/smoketest/wesql/11_restart.yaml diff --git a/test/e2e/testdata/smoketest/wesql/12_backup_full.yaml b/test/e2e/testdata/smoketest/wesql/12_backup_full.yaml deleted file mode 100644 index 31aaeb249..000000000 --- a/test/e2e/testdata/smoketest/wesql/12_backup_full.yaml +++ /dev/null @@ -1,11 +0,0 @@ -apiVersion: dataprotection.kubeblocks.io/v1alpha1 -kind: Backup -metadata: - labels: - app.kubernetes.io/instance: mycluster - dataprotection.kubeblocks.io/backup-type: full - name: backup-full-mycluster -spec: - backupPolicyName: backup-policy-mycluster-full - backupType: full - ttl: 168h0m0s \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/10_reconfigure.yaml b/test/e2e/testdata/smoketest/wesql/12_reconfigure.yaml similarity index 100% rename from test/e2e/testdata/smoketest/wesql/10_reconfigure.yaml rename to test/e2e/testdata/smoketest/wesql/12_reconfigure.yaml diff --git a/test/e2e/testdata/smoketest/wesql/13_backup_full_restore.yaml b/test/e2e/testdata/smoketest/wesql/13_backup_full_restore.yaml deleted file mode 100644 index 09abd4007..000000000 --- a/test/e2e/testdata/smoketest/wesql/13_backup_full_restore.yaml +++ /dev/null @@ -1,32 +0,0 @@ -apiVersion: apps.kubeblocks.io/v1alpha1 -kind: Cluster -metadata: - name: snapshot-mycluster - annotations: - kubeblocks.io/restore-from-backup: "{\"wesql\":\"backup-policy-mycluster-full\"}" -spec: - clusterDefinitionRef: apecloud-mysql - clusterVersionRef: ac-mysql-8.0.30 - terminationPolicy: WipeOut - affinity: - topologyKeys: - - kubernetes.io/hostname - componentSpecs: - - name: mysql - componentDefRef: mysql - monitor: false - replicas: 3 - enabledLogs: [ "slow","error" ] - volumeClaimTemplates: - - name: data - spec: - storageClassName: - accessModes: - - ReadWriteOnce - resources: - requests: - storage: 2Gi - dataSource: - apiGroup: snapshot.storage.k8s.io - kind: VolumeSnapshot - name: backup-full-mycluster \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/08_backup_snapshot.yaml b/test/e2e/testdata/smoketest/wesql/13_backup_snapshot.yaml similarity index 72% rename from test/e2e/testdata/smoketest/wesql/08_backup_snapshot.yaml rename to test/e2e/testdata/smoketest/wesql/13_backup_snapshot.yaml index 65b22b227..749c83fed 100644 --- a/test/e2e/testdata/smoketest/wesql/08_backup_snapshot.yaml +++ b/test/e2e/testdata/smoketest/wesql/13_backup_snapshot.yaml @@ -6,6 +6,5 @@ metadata: dataprotection.kubeblocks.io/backup-type: snapshot name: backup-sbapshot-mycluster spec: - backupPolicyName: backup-policy-mycluster - backupType: snapshot - ttl: 168h0m0s \ No newline at end of file + backupPolicyName: mycluster-mysql-backup-policy + backupType: snapshot \ No newline at end of file diff --git a/test/e2e/testdata/smoketest/wesql/14_backup_snapshot_restore.yaml b/test/e2e/testdata/smoketest/wesql/14_backup_snapshot_restore.yaml new file mode 100644 index 000000000..995ee5570 --- /dev/null +++ b/test/e2e/testdata/smoketest/wesql/14_backup_snapshot_restore.yaml @@ -0,0 +1,23 @@ +apiVersion: apps.kubeblocks.io/v1alpha1 +kind: Cluster +metadata: + name: mycluster-sbapshot + annotations: + kubeblocks.io/restore-from-backup: "{\"mysql\":\"backup-sbapshot-mycluster\"}" +spec: + clusterDefinitionRef: apecloud-mysql + clusterVersionRef: ac-mysql-8.0.30 + terminationPolicy: WipeOut + componentSpecs: + - name: mysql + componentDefRef: mysql + monitor: false + replicas: 1 + volumeClaimTemplates: + - name: data + spec: + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 2Gi \ No newline at end of file diff --git a/test/e2e/types.go b/test/e2e/types.go index d73f50e98..766d91597 100644 --- a/test/e2e/types.go +++ b/test/e2e/types.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -28,3 +28,7 @@ var Ctx context.Context var Cancel context.CancelFunc var Logger logr.Logger var Version string +var Provider string +var Region string +var SecretID string +var SecretKey string diff --git a/test/e2e/util/client.go b/test/e2e/util/client.go index da3c368d4..780595770 100644 --- a/test/e2e/util/client.go +++ b/test/e2e/util/client.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -38,7 +38,7 @@ func NewTestClient(kubecontext string) (TestClient, error) { return InitTestClient(kubecontext) } -// InitTestClient init different type clients +// InitTestClient inits different type clients func InitTestClient(kubecontext string) (TestClient, error) { config, err := client.LoadConfig() if err != nil { diff --git a/test/e2e/util/common.go b/test/e2e/util/common.go index f6e21ca83..22a870e8f 100644 --- a/test/e2e/util/common.go +++ b/test/e2e/util/common.go @@ -1,21 +1,6 @@ -/* -Copyright ApeCloud, Inc. - -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. -*/ - /* Copyright the Velero contributors. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -71,7 +56,7 @@ func CreateSecretFromFiles(ctx context.Context, client TestClient, namespace str return err } -// WaitForPods waits until all of the pods have gone to PodRunning state +// WaitForPods waits till all pods arrive at PodRunning state func WaitForPods(ctx context.Context, client TestClient, namespace string, pods []string) error { timeout := 5 * time.Minute interval := 5 * time.Second @@ -82,13 +67,13 @@ func WaitForPods(ctx context.Context, client TestClient, namespace string, pods fmt.Println(errors.Wrap(err, fmt.Sprintf("Failed to verify pod %s/%s is %s, try again...\n", namespace, podName, corev1api.PodRunning))) return false, nil } - // If any pod is still waiting we don't need to check any more so return and wait for next poll interval + // If any pod is still waiting, no need to check, just return and wait for next poll interval if checkPod.Status.Phase != corev1api.PodRunning { fmt.Printf("Pod %s is in state %s waiting for it to be %s\n", podName, checkPod.Status.Phase, corev1api.PodRunning) return false, nil } } - // All pods were in PodRunning state, we're successful + // All pods were in PodRunning state return true, nil }) if err != nil { diff --git a/test/e2e/util/smoke_util.go b/test/e2e/util/smoke_util.go index 2ebc2ca72..2dfc89300 100644 --- a/test/e2e/util/smoke_util.go +++ b/test/e2e/util/smoke_util.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -18,9 +18,11 @@ package util import ( "bufio" + "bytes" "io" "log" "os" + executil "os/exec" "path/filepath" "strings" "sync" @@ -187,7 +189,7 @@ func ExecCommand(strCommand string) string { cmd := exec.New().Command("/bin/bash", "-c", strCommand) stdout, _ := cmd.StdoutPipe() if err := cmd.Start(); err != nil { - log.Printf("Execute failed when Start:%s\n", err.Error()) + log.Printf("failed to Start:%s\n", err.Error()) return "" } outBytes, _ := io.ReadAll(stdout) @@ -196,7 +198,7 @@ func ExecCommand(strCommand string) string { return "" } if err := cmd.Wait(); err != nil { - log.Printf("Execute failed when Wait:%s\n", err.Error()) + log.Printf("failed to Wait:%s\n", err.Error()) return "" } return string(outBytes) @@ -250,10 +252,10 @@ func ReplaceClusterVersionRef(fileName string, clusterVersionRef string) { line, err := reader.ReadString('\n') if err != nil { if err == io.EOF { - log.Println("File read ok!") + log.Println("file read ok!") break } else { - log.Println("Read file error!", err) + log.Println("file read error ", err) return } } @@ -262,14 +264,14 @@ func ReplaceClusterVersionRef(fileName string, clusterVersionRef string) { bytes := []byte(" app.kubernetes.io/version: \"" + version + "\"\n") _, err := file.WriteAt(bytes, pos) if err != nil { - log.Println("open file filed.", err) + log.Println("file open failed ", err) } } if strings.Contains(line, "clusterVersionRef") { bytes := []byte(" clusterVersionRef: " + clusterVersionRef + "\n") _, err := file.WriteAt(bytes, pos) if err != nil { - log.Println("open file filed.", err) + log.Println("file open failed ", err) } } pos += int64(len(line)) @@ -286,3 +288,32 @@ func CheckKbcliExists() error { _, err := exec.New().LookPath("kbcli") return err } + +func Check(command string, input string) (string, error) { + cmd := executil.Command("bash", "-c", command) + + var output bytes.Buffer + cmd.Stdout = &output + + inPipe, err := cmd.StdinPipe() + if err != nil { + return "", err + } + + err = cmd.Start() + if err != nil { + return "", err + } + + _, e := io.WriteString(inPipe, input) + if e != nil { + return "", e + } + + err = cmd.Wait() + if err != nil { + return "", err + } + + return output.String(), nil +} diff --git a/test/integration/backup_mysql_test.go b/test/integration/backup_mysql_test.go index 6e18d32f3..812c2b0d4 100644 --- a/test/integration/backup_mysql_test.go +++ b/test/integration/backup_mysql_test.go @@ -1,11 +1,11 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd 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 + 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, @@ -13,6 +13,7 @@ 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 appstest import ( @@ -36,16 +37,11 @@ var _ = Describe("MySQL data protection function", func() { const clusterVersionName = "test-clusterversion" const clusterNamePrefix = "test-cluster" const scriptConfigName = "test-cluster-mysql-scripts" - - const mysqlCompType = "replicasets" + const mysqlCompDefName = "replicasets" const mysqlCompName = "mysql" - const backupPolicyTemplateName = "test-backup-policy-template" const backupPolicyName = "test-backup-policy" - const backupRemoteVolumeName = "backup-remote-volume" const backupRemotePVCName = "backup-remote-pvc" - const defaultSchedule = "0 3 * * *" - const defaultTTL = "168h0m0s" const backupName = "test-backup-job" // Cleanups @@ -66,7 +62,6 @@ var _ = Describe("MySQL data protection function", func() { testapps.ClearResources(&testCtx, intctrlutil.OpsRequestSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.ConfigMapSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.BackupSignature, inNS, ml) - testapps.ClearResources(&testCtx, intctrlutil.BackupPolicyTemplateSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.BackupPolicySignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.BackupToolSignature, inNS, ml) testapps.ClearResources(&testCtx, intctrlutil.RestoreJobSignature, inNS, ml) @@ -95,13 +90,13 @@ var _ = Describe("MySQL data protection function", func() { By("Create a clusterDef obj") mode := int32(0755) clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() By("Create a cluster obj") @@ -109,7 +104,7 @@ var _ = Describe("MySQL data protection function", func() { pvcSpec := testapps.NewPVCSpec("1Gi") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetReplicas(1). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). Create(&testCtx).GetObject() @@ -127,20 +122,14 @@ var _ = Describe("MySQL data protection function", func() { backupTool := testapps.CreateCustomizedObj(&testCtx, "backup/backuptool.yaml", &dpv1alpha1.BackupTool{}, testapps.RandomizedObjName()) - By("By creating a backupPolicyTemplate from backupTool: " + backupTool.Name) - _ = testapps.NewBackupPolicyTemplateFactory(backupPolicyTemplateName). - SetBackupToolName(backupTool.Name). - SetSchedule(defaultSchedule). - SetTTL(defaultTTL). - Create(&testCtx).GetObject() - By("By creating a backupPolicy from backupPolicyTemplate: " + backupPolicyTemplateName) backupPolicyObj := testapps.NewBackupPolicyFactory(testCtx.DefaultNamespace, backupPolicyName). WithRandomName(). - SetBackupPolicyTplName(backupPolicyTemplateName). + AddFullPolicy(). + SetBackupToolName(backupTool.Name). AddMatchLabels(constant.AppInstanceLabelKey, clusterKey.Name). SetTargetSecretName(component.GenerateConnCredential(clusterKey.Name)). - SetRemoteVolumePVC(backupRemoteVolumeName, backupRemotePVCName). + SetPVC(backupRemotePVCName). Create(&testCtx).GetObject() backupPolicyKey := client.ObjectKeyFromObject(backupPolicyObj) @@ -153,15 +142,14 @@ var _ = Describe("MySQL data protection function", func() { By("By check backupPolicy available") Eventually(testapps.CheckObj(&testCtx, backupPolicyKey, func(g Gomega, backupPolicy *dpv1alpha1.BackupPolicy) { - g.Expect(backupPolicy.Status.Phase).To(Equal(dpv1alpha1.ConfigAvailable)) + g.Expect(backupPolicy.Status.Phase).To(Equal(dpv1alpha1.PolicyAvailable)) })).Should(Succeed()) By("By creating a backup from backupPolicy: " + backupPolicyKey.Name) backup := testapps.NewBackupFactory(testCtx.DefaultNamespace, backupName). WithRandomName(). - SetTTL(defaultTTL). SetBackupPolicyName(backupPolicyKey.Name). - SetBackupType(dpv1alpha1.BackupTypeFull). + SetBackupType(dpv1alpha1.BackupTypeDataFile). Create(&testCtx).GetObject() backupKey = client.ObjectKeyFromObject(backup) } diff --git a/test/integration/controller_suite_test.go b/test/integration/controller_suite_test.go index 1a922b817..110d4646a 100644 --- a/test/integration/controller_suite_test.go +++ b/test/integration/controller_suite_test.go @@ -1,11 +1,11 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd 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 + 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, @@ -127,18 +127,14 @@ func CreateSimpleConsensusMySQLClusterWithConfig( mysqlConfigConstraintPath, mysqlScriptsPath string) ( *appsv1alpha1.ClusterDefinition, *appsv1alpha1.ClusterVersion, *appsv1alpha1.Cluster) { - const mysqlCompName = "mysql" - const mysqlCompType = "mysql" - + const mysqlCompDefName = "mysql" const mysqlConfigName = "mysql-component-config" const mysqlConfigConstraintName = "mysql8.0-config-constraints" const mysqlScriptsConfigName = "apecloud-mysql-scripts" - const mysqlDataVolumeName = "data" const mysqlConfigVolumeName = "mysql-config" const mysqlScriptsVolumeName = "scripts" - const mysqlErrorFilePath = "/data/mysql/log/mysqld-error.log" const mysqlGeneralFilePath = "/data/mysql/log/mysqld.log" const mysqlSlowlogFilePath = "/data/mysql/log/mysqld-slowquery.log" @@ -185,7 +181,7 @@ func CreateSimpleConsensusMySQLClusterWithConfig( mode := int32(0755) clusterDefObj := testapps.NewClusterDefFactory(clusterDefName). SetConnectionCredential(map[string]string{"username": "root", "password": ""}, nil). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). AddConfigTemplate(mysqlConfigName, configmap.Name, constraint.Name, testCtx.DefaultNamespace, mysqlConfigVolumeName). AddScriptTemplate(mysqlScriptsConfigName, mysqlScriptsConfigName, @@ -203,7 +199,7 @@ func CreateSimpleConsensusMySQLClusterWithConfig( By("Create a clusterVersion obj") clusterVersionObj := testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType). + AddComponentVersion(mysqlCompDefName). AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). AddLabels(cfgcore.GenerateTPLUniqLabelKeyWithConfig(mysqlConfigName), configmap.Name, cfgcore.GenerateConstraintsUniqLabelKeyWithConfig(constraint.Name), constraint.Name). @@ -220,7 +216,7 @@ func CreateSimpleConsensusMySQLClusterWithConfig( } clusterObj := testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterName, clusterDefObj.Name, clusterVersionObj.Name). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetReplicas(3). SetEnabledLogs("error", "general", "slow"). AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). @@ -327,18 +323,10 @@ var _ = BeforeSuite(func() { }).SetupWithManager(k8sManager) Expect(err).ToNot(HaveOccurred()) - err = (&components.StatefulSetReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("stateful-set-controller"), - }).SetupWithManager(k8sManager) + err = components.NewStatefulSetReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) - err = (&components.DeploymentReconciler{ - Client: k8sManager.GetClient(), - Scheme: k8sManager.GetScheme(), - Recorder: k8sManager.GetEventRecorderFor("deployment-controller"), - }).SetupWithManager(k8sManager) + err = components.NewDeploymentReconciler(k8sManager) Expect(err).ToNot(HaveOccurred()) err = (&k8score.EventReconciler{ diff --git a/test/integration/mysql_ha_test.go b/test/integration/mysql_ha_test.go index 94930d643..4319d934d 100644 --- a/test/integration/mysql_ha_test.go +++ b/test/integration/mysql_ha_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -41,10 +41,8 @@ var _ = Describe("MySQL High-Availability function", func() { const clusterVersionName = "test-clusterversion" const clusterNamePrefix = "test-cluster" const scriptConfigName = "test-cluster-mysql-scripts" - - const mysqlCompType = "replicasets" + const mysqlCompDefName = "replicasets" const mysqlCompName = "mysql" - const leader = "leader" const follower = "follower" @@ -112,7 +110,7 @@ var _ = Describe("MySQL High-Availability function", func() { pvcSpec := testapps.NewPVCSpec("1Gi") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetReplicas(3).AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) @@ -193,14 +191,14 @@ var _ = Describe("MySQL High-Availability function", func() { mode := int32(0755) clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). SetConnectionCredential(map[string]string{"username": "root", "password": ""}, nil). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). AddContainerEnv(testapps.DefaultMySQLContainerName, corev1.EnvVar{Name: "MYSQL_ALLOW_EMPTY_PASSWORD", Value: "yes"}). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) diff --git a/test/integration/mysql_reconfigure_test.go b/test/integration/mysql_reconfigure_test.go index d0fc56576..e0b7f74a7 100644 --- a/test/integration/mysql_reconfigure_test.go +++ b/test/integration/mysql_reconfigure_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/test/integration/mysql_scale_test.go b/test/integration/mysql_scale_test.go index 2cfe6bf2b..5c2016e2a 100644 --- a/test/integration/mysql_scale_test.go +++ b/test/integration/mysql_scale_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -41,8 +41,7 @@ var _ = Describe("MySQL Scaling function", func() { const clusterVersionName = "test-clusterversion" const clusterNamePrefix = "test-cluster" const scriptConfigName = "test-cluster-mysql-scripts" - - const mysqlCompType = "replicasets" + const mysqlCompDefName = "replicasets" const mysqlCompName = "mysql" // Cleanups @@ -93,7 +92,7 @@ var _ = Describe("MySQL Scaling function", func() { } clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). SetResources(resources).SetReplicas(1). Create(&testCtx).GetObject() clusterKey = client.ObjectKeyFromObject(clusterObj) @@ -135,8 +134,8 @@ var _ = Describe("MySQL Scaling function", func() { })).Should(Succeed()) By("check OpsRequest reclaimed after ttl") - Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func() { - verticalScalingOpsRequest.Spec.TTLSecondsAfterSucceed = 1 + Expect(testapps.ChangeObj(&testCtx, verticalScalingOpsRequest, func(lopsReq *appsv1alpha1.OpsRequest) { + lopsReq.Spec.TTLSecondsAfterSucceed = 1 })).Should(Succeed()) By("OpsRequest reclaimed after ttl") @@ -163,7 +162,7 @@ var _ = Describe("MySQL Scaling function", func() { logPvcSpec.StorageClassName = &defaultStorageClass.Name clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(mysqlCompName, mysqlCompType). + AddComponent(mysqlCompName, mysqlCompDefName). AddVolumeClaimTemplate(testapps.DataVolumeName, dataPvcSpec). AddVolumeClaimTemplate(testapps.LogVolumeName, logPvcSpec). Create(&testCtx).GetObject() @@ -226,13 +225,13 @@ var _ = Describe("MySQL Scaling function", func() { By("Create a clusterDef obj") mode := int32(0755) clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.StatefulMySQLComponent, mysqlCompType). + AddComponentDef(testapps.StatefulMySQLComponent, mysqlCompDefName). AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) @@ -254,13 +253,13 @@ var _ = Describe("MySQL Scaling function", func() { By("Create a clusterDef obj") mode := int32(0755) clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ConsensusMySQLComponent, mysqlCompType). + AddComponentDef(testapps.ConsensusMySQLComponent, mysqlCompDefName). AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). Create(&testCtx).GetObject() By("Create a clusterVersion obj") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.GetName()). - AddComponent(mysqlCompType).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). + AddComponentVersion(mysqlCompDefName).AddContainerShort(testapps.DefaultMySQLContainerName, testapps.ApeCloudMySQLImage). Create(&testCtx).GetObject() }) diff --git a/test/integration/redis_hscale_test.go b/test/integration/redis_hscale_test.go index 5fec39ab2..3173a7509 100644 --- a/test/integration/redis_hscale_test.go +++ b/test/integration/redis_hscale_test.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" appsv1alpha1 "github.com/apecloud/kubeblocks/apis/apps/v1alpha1" - "github.com/apecloud/kubeblocks/controllers/apps/components/replicationset" + "github.com/apecloud/kubeblocks/controllers/apps/components/replication" "github.com/apecloud/kubeblocks/controllers/apps/components/util" "github.com/apecloud/kubeblocks/internal/constant" intctrlutil "github.com/apecloud/kubeblocks/internal/generics" @@ -91,7 +91,7 @@ var _ = Describe("Redis Horizontal Scale function", func() { pvcSpec := testapps.NewPVCSpec("1Gi") clusterObj = testapps.NewClusterFactory(testCtx.DefaultNamespace, clusterNamePrefix, clusterDefObj.Name, clusterVersionObj.Name).WithRandomName(). - AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompType). + AddComponent(testapps.DefaultRedisCompName, testapps.DefaultRedisCompDefName). SetPrimaryIndex(testapps.DefaultReplicationPrimaryIndex). SetReplicas(replicas).AddVolumeClaimTemplate(testapps.DataVolumeName, pvcSpec). Create(&testCtx).GetObject() @@ -110,9 +110,9 @@ var _ = Describe("Redis Horizontal Scale function", func() { By("Checking statefulSet role label") for _, sts := range stsList.Items { if strings.HasSuffix(sts.Name, strconv.Itoa(testapps.DefaultReplicationPrimaryIndex)) { - Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Primary)) + Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replication.Primary)) } else { - Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Secondary)) + Expect(sts.Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replication.Secondary)) } } @@ -122,9 +122,9 @@ var _ = Describe("Redis Horizontal Scale function", func() { Expect(err).To(Succeed()) Expect(len(podList)).Should(BeEquivalentTo(1)) if strings.HasSuffix(sts.Name, strconv.Itoa(testapps.DefaultReplicationPrimaryIndex)) { - Expect(podList[0].Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Primary)) + Expect(podList[0].Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replication.Primary)) } else { - Expect(podList[0].Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replicationset.Secondary)) + Expect(podList[0].Labels[constant.RoleLabelKey]).Should(BeEquivalentTo(replication.Secondary)) } } @@ -145,8 +145,8 @@ var _ = Describe("Redis Horizontal Scale function", func() { for _, newReplicas := range []int32{4, 2, 7, 1} { By(fmt.Sprintf("horizontal scale out to %d", newReplicas)) - Expect(testapps.ChangeObj(&testCtx, clusterObj, func() { - clusterObj.Spec.ComponentSpecs[0].Replicas = newReplicas + Expect(testapps.ChangeObj(&testCtx, clusterObj, func(lcluster *appsv1alpha1.Cluster) { + lcluster.Spec.ComponentSpecs[0].Replicas = newReplicas })).Should(Succeed()) By("Wait for the cluster to be running") @@ -179,11 +179,11 @@ var _ = Describe("Redis Horizontal Scale function", func() { replicationRedisConfigVolumeMounts := []corev1.VolumeMount{ { - Name: string(replicationset.Primary), + Name: string(replication.Primary), MountPath: "/etc/conf/primary", }, { - Name: string(replicationset.Secondary), + Name: string(replication.Secondary), MountPath: "/etc/conf/secondary", }, } @@ -191,17 +191,17 @@ var _ = Describe("Redis Horizontal Scale function", func() { By("Create a clusterDefinition obj with replication workloadType.") mode := int32(0755) clusterDefObj = testapps.NewClusterDefFactory(clusterDefName). - AddComponent(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompType). + AddComponentDef(testapps.ReplicationRedisComponent, testapps.DefaultRedisCompDefName). AddScriptTemplate(scriptConfigName, scriptConfigName, testCtx.DefaultNamespace, testapps.ScriptsVolumeName, &mode). - AddConfigTemplate(primaryConfigName, primaryConfigName, "", testCtx.DefaultNamespace, string(replicationset.Primary)). - AddConfigTemplate(secondaryConfigName, secondaryConfigName, "", testCtx.DefaultNamespace, string(replicationset.Secondary)). + AddConfigTemplate(primaryConfigName, primaryConfigName, "", testCtx.DefaultNamespace, string(replication.Primary)). + AddConfigTemplate(secondaryConfigName, secondaryConfigName, "", testCtx.DefaultNamespace, string(replication.Secondary)). AddInitContainerVolumeMounts(testapps.DefaultRedisInitContainerName, replicationRedisConfigVolumeMounts). AddContainerVolumeMounts(testapps.DefaultRedisContainerName, replicationRedisConfigVolumeMounts). Create(&testCtx).GetObject() By("Create a clusterVersion obj with replication workloadType.") clusterVersionObj = testapps.NewClusterVersionFactory(clusterVersionName, clusterDefObj.Name). - AddComponent(testapps.DefaultRedisCompType). + AddComponentVersion(testapps.DefaultRedisCompDefName). AddInitContainerShort(testapps.DefaultRedisInitContainerName, testapps.DefaultRedisImageName). AddContainerShort(testapps.DefaultRedisContainerName, testapps.DefaultRedisImageName). Create(&testCtx).GetObject() diff --git a/test/testdata/addon/addon.yaml b/test/testdata/addon/addon.yaml index 29baf41e7..a129352da 100644 --- a/test/testdata/addon/addon.yaml +++ b/test/testdata/addon/addon.yaml @@ -33,7 +33,7 @@ spec: # via YAML contents reside in secret.data. secretRefs: # - name: - # namepsace: + # namespace: # key: setValues: [ ] setJSONValues: [ ] diff --git a/test/testdata/backup/backuptool.yaml b/test/testdata/backup/backuptool.yaml index 354c72d41..e08dd26d9 100644 --- a/test/testdata/backup/backuptool.yaml +++ b/test/testdata/backup/backuptool.yaml @@ -5,10 +5,6 @@ metadata: spec: image: registry.cn-hangzhou.aliyuncs.com/apecloud/percona-xtrabackup deployKind: job - resources: - limits: - cpu: "1" - memory: 2Gi env: - name: DATA_DIR value: /var/lib/mysql diff --git a/test/testdata/backup/pitr_backuptool.yaml b/test/testdata/backup/pitr_backuptool.yaml new file mode 100644 index 000000000..19e9a2c07 --- /dev/null +++ b/test/testdata/backup/pitr_backuptool.yaml @@ -0,0 +1,25 @@ +apiVersion: dataprotection.kubeblocks.io/v1alpha1 +kind: BackupTool +metadata: + name: backup-tool- +spec: + image: registry.cn-hangzhou.aliyuncs.com/apecloud/percona-xtrabackup + deployKind: job + type: pitr + env: + - name: DATA_DIR + value: /var/lib/mysql + - name: RECOVERY_TIME + value: $KB_RECOVERY_TIME + physical: + restoreCommands: + - | + echo $RECOVERY_TIME + incrementalRestoreCommands: [] + logical: + restoreCommands: + - | + echo $RECOVERY_TIME + incrementalRestoreCommands: [] + backupCommands: [] + incrementalBackupCommands: [] \ No newline at end of file diff --git a/test/testdata/cue_testdata/clickhouse.cue b/test/testdata/cue_testdata/clickhouse.cue index dbc488290..ff024ef72 100644 --- a/test/testdata/cue_testdata/clickhouse.cue +++ b/test/testdata/cue_testdata/clickhouse.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #ProfilesParameter: { profiles: [string]: #ClickhouseParameter diff --git a/test/testdata/cue_testdata/mongod.conf b/test/testdata/cue_testdata/mongod.conf new file mode 100644 index 000000000..c07e89dba --- /dev/null +++ b/test/testdata/cue_testdata/mongod.conf @@ -0,0 +1,21 @@ +storage: + dbPath: "/var/lib/mongo" +systemLog: + destination: file + path: "/var/log/mongodb/mongod.log" +net: + port: 2000 + bindIp: + __exec: "python /home/user/getIPAddresses.py" + type: "string" + trim: "whitespace" + digest: 85fed8997aac3f558e779625f2e51b4d142dff11184308dc6aca06cff26ee9ad + digest_key: 68656c6c30303030307365637265746d796f6c64667269656e64 + tls: + mode: requireTLS + certificateKeyFile: "/etc/tls/mongod.pem" + certificateKeyFilePassword: + __rest: "https://myrestserver.example.net/api/config/myCertKeyFilePassword" + type: "string" + digest: b08519162ba332985ac18204851949611ef73835ec99067b85723e10113f5c26 + digest_key: 6d795365637265744b65795374756666 \ No newline at end of file diff --git a/test/testdata/cue_testdata/mongod.cue b/test/testdata/cue_testdata/mongod.cue new file mode 100644 index 000000000..f4477eb76 --- /dev/null +++ b/test/testdata/cue_testdata/mongod.cue @@ -0,0 +1,54 @@ +//Copyright (C) 2022-2023 ApeCloud Co., Ltd +// +//This file is part of KubeBlocks project +// +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. +// +//This program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . + +#MongodParameter: { + net: { + port: int & >=0 & <=65535 + + unixDomainSocket: { + // Enables Unix Domain Sockets used for all network connections + enabled: bool | *false + pathPrefix: string + ... + } + tls: { + // Enables TLS used for all network connections + mode: string & "disabled" | "allowTLS" | "preferTLS" | "requireTLS" + + certificateKeyFile: string + CAFile: string + CRLFile: string + ... + } + ... + } + tls: { + // Enables TLS used for all network connections + mode: string & "disabled" | "allowTLS" | "preferTLS" | "requireTLS" + + certificateKeyFile: string + CAFile: string + CRLFile: string + ... + } + + ... +} + +// configuration require +configuration: #MongodParameter & { +} diff --git a/test/testdata/cue_testdata/mysql.cue b/test/testdata/cue_testdata/mysql.cue index d87fd990b..35f5f6173 100644 --- a/test/testdata/cue_testdata/mysql.cue +++ b/test/testdata/cue_testdata/mysql.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // mysql config validator // mysql server param: a set of name/value pairs. @@ -27,12 +30,12 @@ mysqld: { // [0|1|2] default: 2 innodb_autoinc_lock_mode?: int & 0 | 1 | 2 | *2 - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } -// ingore client parameter validate +// ignore client parameter validate // mysql client: a set of name/value pairs. client?: { [string]: string diff --git a/test/testdata/cue_testdata/mysql_for_cli.cue b/test/testdata/cue_testdata/mysql_for_cli.cue index db743d4f6..418c61399 100644 --- a/test/testdata/cue_testdata/mysql_for_cli.cue +++ b/test/testdata/cue_testdata/mysql_for_cli.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // top level configuration type // mysql server param: a set of name/value pairs. @@ -23,13 +26,13 @@ binlog_stmt_cache_size?: int & >=4096 & <=16777216 | *2097152 // [0|1|2] default: 2 innodb_autoinc_lock_mode?: int & 0 | 1 | 2 | *2 - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } mysqld: #MysqlParameter & { } -// ingore client parameter validate +// ignore client parameter validate // mysql client: a set of name/value pairs. client?: { [string]: string diff --git a/test/testdata/cue_testdata/mysql_openapi.cue b/test/testdata/cue_testdata/mysql_openapi.cue index 6fd1e1778..779a90028 100644 --- a/test/testdata/cue_testdata/mysql_openapi.cue +++ b/test/testdata/cue_testdata/mysql_openapi.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . // mysql config validator #MysqlParameter: { @@ -28,12 +31,12 @@ // [0|1|2] default: 2 innodb_autoinc_lock_mode?: int & 0 | 1 | 2 | *2 - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } - // ingore client parameter validate + // ignore client parameter validate // mysql client: a set of name/value pairs. client?: { [string]: string diff --git a/test/testdata/cue_testdata/mysql_openapi.json b/test/testdata/cue_testdata/mysql_openapi.json index 6a7cb5298..21e963937 100644 --- a/test/testdata/cue_testdata/mysql_openapi.json +++ b/test/testdata/cue_testdata/mysql_openapi.json @@ -9,7 +9,7 @@ ], "properties": { "client": { - "description": "ingore client parameter validate\nmysql client: a set of name/value pairs.", + "description": "ignore client parameter validate\nmysql client: a set of name/value pairs.", "type": "object", "additionalProperties": { "type": "string" diff --git a/test/testdata/cue_testdata/mysql_openapi_v2.cue b/test/testdata/cue_testdata/mysql_openapi_v2.cue index f42bd584b..57e2c7857 100644 --- a/test/testdata/cue_testdata/mysql_openapi_v2.cue +++ b/test/testdata/cue_testdata/mysql_openapi_v2.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #SectionParameter: { // [OFF|ON] default ON @@ -41,7 +44,7 @@ // mysql config validator mysqlld: #SectionParameter - // ingore client parameter validate + // ignore client parameter validate // mysql client: a set of name/value pairs. client?: { [string]: string diff --git a/test/testdata/cue_testdata/mysql_openapi_v2.json b/test/testdata/cue_testdata/mysql_openapi_v2.json index 7fbf6de6e..bf1fadbb4 100644 --- a/test/testdata/cue_testdata/mysql_openapi_v2.json +++ b/test/testdata/cue_testdata/mysql_openapi_v2.json @@ -9,7 +9,7 @@ ], "properties": { "client": { - "description": "ingore client parameter validate\nmysql client: a set of name/value pairs.", + "description": "ignore client parameter validate\nmysql client: a set of name/value pairs.", "type": "object", "additionalProperties": { "type": "string" diff --git a/test/testdata/cue_testdata/mysql_simple.cue b/test/testdata/cue_testdata/mysql_simple.cue index e1a9faca9..d696fb31b 100644 --- a/test/testdata/cue_testdata/mysql_simple.cue +++ b/test/testdata/cue_testdata/mysql_simple.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #Section: { // SectionName is extract section name @@ -25,8 +28,8 @@ // [0|1|2] default: 2 innodb_autoinc_lock_mode?: int & 0 | 1 | 2 | *2 - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } diff --git a/test/testdata/cue_testdata/pg14.cue b/test/testdata/cue_testdata/pg14.cue index 7e58446c0..687474175 100644 --- a/test/testdata/cue_testdata/pg14.cue +++ b/test/testdata/cue_testdata/pg14.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #PGPameter: { // PostgreSQL parameters: https://postgresqlco.nf/doc/en/param/ diff --git a/test/testdata/cue_testdata/wesql.cnf b/test/testdata/cue_testdata/wesql.cnf index 9afaf7281..58c2f1c27 100644 --- a/test/testdata/cue_testdata/wesql.cnf +++ b/test/testdata/cue_testdata/wesql.cnf @@ -105,7 +105,7 @@ log_bin_index=mysql-bin.index max_binlog_size=134217728 log_replica_updates=1 # binlog_rows_query_log_events=ON #AWS not set -# binlog_transaction_dependency_tracking=WRITESET #Defautl Commit Order, Aws not set +# binlog_transaction_dependency_tracking=WRITESET #Default Commit Order, Aws not set # replay log # relay_log_info_repository=TABLE diff --git a/test/testdata/cue_testdata/wesql.cue b/test/testdata/cue_testdata/wesql.cue index 24108dc7f..19104e267 100644 --- a/test/testdata/cue_testdata/wesql.cue +++ b/test/testdata/cue_testdata/wesql.cue @@ -1,16 +1,19 @@ -// Copyright ApeCloud, Inc. +//Copyright (C) 2022-2023 ApeCloud Co., Ltd // -// 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 +//This file is part of KubeBlocks project // -// http://www.apache.org/licenses/LICENSE-2.0 +//This program is free software: you can redistribute it and/or modify +//it under the terms of the GNU Affero General Public License as published by +//the Free Software Foundation, either version 3 of the License, or +//(at your option) any later version. // -// 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 program is distributed in the hope that it will be useful +//but WITHOUT ANY WARRANTY; without even the implied warranty of +//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +//GNU Affero General Public License for more details. +// +//You should have received a copy of the GNU Affero General Public License +//along with this program. If not, see . #MysqlParameter: { @@ -83,7 +86,7 @@ // If this variable is enabled (the default), transactions are committed in the same order they are written to the binary log. If disabled, transactions may be committed in parallel. binlog_order_commits?: string & "0" | "1" | "OFF" | "ON" - // Whether the server logs full or minmal rows with row-based replication. + // Whether the server logs full or minimal rows with row-based replication. binlog_row_image?: string & "FULL" | "MINIMAL" | "NOBLOB" // Controls whether metadata is logged using FULL or MINIMAL format. FULL causes all metadata to be logged; MINIMAL means that only metadata actually required by slave is logged. Default: MINIMAL. @@ -1526,8 +1529,8 @@ // For SQL window functions, determines whether to enable inversion optimization for moving window frames also for floating values. windowing_use_high_precision: string & "0" | "1" | "OFF" | "ON" | *"1" - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } diff --git a/test/testdata/resources/mysql-config-constraint.yaml b/test/testdata/resources/mysql-config-constraint.yaml index b5faedf86..cfb5a9c98 100644 --- a/test/testdata/resources/mysql-config-constraint.yaml +++ b/test/testdata/resources/mysql-config-constraint.yaml @@ -28,13 +28,13 @@ spec: binlog_stmt_cache_size?: int & >= 4096 & <= 16777216 | *2097152 // [0|1|2] default: 2 innodb_autoinc_lock_mode?: int & 0 | 1 | 2 | *2 - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } mysqld: #MysqlParameter - // ingore client parameter validate + // ignore client parameter validate // mysql client: a set of name/value pairs. client?: { [string]: string diff --git a/test/testdata/resources/mysql-consensus-config-constraint.yaml b/test/testdata/resources/mysql-consensus-config-constraint.yaml index 783ddaf2e..2ff293225 100644 --- a/test/testdata/resources/mysql-consensus-config-constraint.yaml +++ b/test/testdata/resources/mysql-consensus-config-constraint.yaml @@ -26,19 +26,22 @@ spec: # schema: auto generate from mmmcue scripts # example: ../../internal/configuration/testdata/mysql_openapi.json cue: |- - // Copyright ApeCloud, Inc. + //Copyright (C) 2022-2023 ApeCloud Co., Ltd // - // 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 + //This file is part of KubeBlocks project // - // http://www.apache.org/licenses/LICENSE-2.0 + //This program is free software: you can redistribute it and/or modify + //it under the terms of the GNU Affero General Public License as published by + //the Free Software Foundation, either version 3 of the License, or + //(at your option) any later version. // - // 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 program is distributed in the hope that it will be useful + //but WITHOUT ANY WARRANTY; without even the implied warranty of + //MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + //GNU Affero General Public License for more details. + // + //You should have received a copy of the GNU Affero General Public License + //along with this program. If not, see . #MysqlParameter: { @@ -111,7 +114,7 @@ spec: // If this variable is enabled (the default), transactions are committed in the same order they are written to the binary log. If disabled, transactions may be committed in parallel. binlog_order_commits?: string & "0" | "1" | "OFF" | "ON" - // Whether the server logs full or minmal rows with row-based replication. + // Whether the server logs full or minimal rows with row-based replication. binlog_row_image?: string & "FULL" | "MINIMAL" | "NOBLOB" // Controls whether metadata is logged using FULL or MINIMAL format. FULL causes all metadata to be logged; MINIMAL means that only metadata actually required by slave is logged. Default: MINIMAL. @@ -1554,8 +1557,8 @@ spec: // For SQL window functions, determines whether to enable inversion optimization for moving window frames also for floating values. windowing_use_high_precision: string & "0" | "1" | "OFF" | "ON" | *"1" - // other parmeters - // reference mysql parmeters + // other parameters + // reference mysql parameters ... } diff --git a/test/testdata/resources/mysql-consensus-config-template.yaml b/test/testdata/resources/mysql-consensus-config-template.yaml index 315b040f8..767f28b23 100644 --- a/test/testdata/resources/mysql-consensus-config-template.yaml +++ b/test/testdata/resources/mysql-consensus-config-template.yaml @@ -155,7 +155,7 @@ data: max_binlog_size=134217728 log_replica_updates=1 # binlog_rows_query_log_events=ON #AWS not set - # binlog_transaction_dependency_tracking=WRITESET #Defautl Commit Order, Aws not set + # binlog_transaction_dependency_tracking=WRITESET #Default Commit Order, Aws not set # replay log # relay_log_info_repository=TABLE diff --git a/test/testdata/resources/mysql-consensus-scripts.yaml b/test/testdata/resources/mysql-consensus-scripts.yaml index 387ddc896..1e04b1a26 100644 --- a/test/testdata/resources/mysql-consensus-scripts.yaml +++ b/test/testdata/resources/mysql-consensus-scripts.yaml @@ -11,128 +11,4 @@ metadata: data: setup.sh: | #!/bin/bash - leader=$KB_MYSQL_LEADER - followers=$KB_MYSQL_FOLLOWERS - echo $leader - echo $followers - sub_follower=`echo "$followers" | grep "$KB_POD_NAME"` - echo $KB_POD_NAME - echo $sub_follower - if [ -z "$leader" -o "$KB_POD_NAME" = "$leader" -o ! -z "$sub_follower" ]; then - echo "no need to call add" - else - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - echo "$host" - leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) - if [ ! -z $leader_host ]; then - host_flag="-h$leader_host" - fi - if [ ! -z $MYSQL_ROOT_PASSWORD ]; then - password_flag="-p$MYSQL_ROOT_PASSWORD" - fi - echo "mysql $host_flag -uroot $password_flag -e \"call dbms_consensus.add_learner('$host:13306');\" >> /tmp/setup_error.log 2>&1 " - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.add_learner('$host:13306');" >> /tmp/setup_error.log 2>&1 - code=$? - echo "exit code: $code" - if [ $code -ne 0 ]; then - cat /tmp/setup_error.log - already_exists=`cat /tmp/setup_error.log | grep "Target node already exists"` - if [ -z "$already_exists" ]; then - exit $code - fi - fi - /scripts/upgrade-learner.sh & - fi - cluster_info=""; for (( i=0; i< $KB_MYSQL_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_MYSQL_"$i"_HOSTNAME) - cluster_info="$cluster_info$host:13306"; - done; - idx=${KB_POD_NAME##*-} - echo $idx - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - cluster_info="$cluster_info@$(($idx+1))"; - echo $cluster_info; - mkdir -p /data/mysql/data; - mkdir -p /data/mysql/log; - chmod +777 -R /data/mysql; - leader=$KB_MYSQL_LEADER - echo $leader - if [ -z $leader ] || [ ! -f "/data/mysql/data/.restore" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$cluster_info\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$cluster_info" --cluster-id=$CLUSTER_ID - elif [ "$KB_POD_NAME" != "$leader" ]; then - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306" --cluster-id=$CLUSTER_ID - else - echo "docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info=\"$host:13306@1\" --cluster-id=$CLUSTER_ID" - exec docker-entrypoint.sh mysqld --defaults-file=/opt/mysql/my.cnf --cluster-start-index=$CLUSTER_START_INDEX --cluster-info="$host:13306@1" --cluster-id=$CLUSTER_ID - fi - upgrade-learner.sh: | - #!/bin/bash - leader=$KB_MYSQL_LEADER - idx=${KB_POD_NAME##*-} - host=$(eval echo \$KB_MYSQL_"$idx"_HOSTNAME) - leader_idx=${leader##*-} - leader_host=$(eval echo \$KB_MYSQL_"$leader_idx"_HOSTNAME) - if [ ! -z $leader_host ]; then - host_flag="-h$leader_host" - fi - if [ ! -z $MYSQL_ROOT_PASSWORD ]; then - password_flag="-p$MYSQL_ROOT_PASSWORD" - fi - while true - do - sleep 5 - mysql -uroot $password_flag -e "select ROLE from information_schema.wesql_cluster_local" > /tmp/role.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/role.log >> /tmp/upgrade-learner.log - else - role=`cat /tmp/role.log` - echo "role: $role" >> /tmp/upgrade-learner.log - if [ -z "$role" ]; then - echo "cannot get role" >> /tmp/upgrade-learner.log - else - break - fi - fi - else - kill -9 $pid - echo "mysql timeout" >> /tmp/upgrade-learner.log - fi - done - grep_learner=`echo $role | grep "Learner"` - echo "grep learner: $grep_learner" >> /tmp/upgrade-learner.log - if [ -z "$grep_learner" ]; then - exit 0 - fi - while true - do - mysql $host_flag -uroot $password_flag -e "call dbms_consensus.upgrade_learner('$host:13306');" >> /tmp/upgrade.log 2>&1 & - pid=$!; sleep 2; - if ! ps $pid > /dev/null; then - wait $pid; - code=$?; - if [ $code -ne 0 ]; then - cat /tmp/upgrade.log >> /tmp/upgrade-learner.log - already_exists=`cat /tmp/upgrade.log | grep "Target node already exists"` - if [ ! -z "$already_exists" ]; then - break - fi - else - break - fi - else - kill -9 $pid - echo "mysql call leader timeout" >> /tmp/upgrade-learner.log - fi - sleep 5 - done + exec docker-entrypoint.sh diff --git a/test/testdata/resources/mysql-scripts.yaml b/test/testdata/resources/mysql-scripts.yaml index 96a46945f..5605e5f7b 100644 --- a/test/testdata/resources/mysql-scripts.yaml +++ b/test/testdata/resources/mysql-scripts.yaml @@ -5,24 +5,4 @@ metadata: data: setup.sh: | #!/bin/bash - cluster_info=""; - for (( i=0; i<$KB_REPLICASETS_N; i++ )); do - if [ $i -ne 0 ]; then - cluster_info="$cluster_info;"; - fi; - host=$(eval echo \$KB_REPLICASETS_"$i"_HOSTNAME) - cluster_info="$cluster_info$host:13306"; - done; - idx=0; - while IFS='-' read -ra ADDR; do - for i in "${ADDR[@]}"; do - idx=$i; - done; - done <<< "$KB_POD_NAME"; - echo $idx; - cluster_info="$cluster_info@$(($idx+1))"; - echo $cluster_info; - mkdir -p /data/mysql/data; - mkdir -p /data/mysql/log; - chmod +777 -R /data/mysql; - docker-entrypoint.sh mysqld --cluster-start-index=1 --cluster-info="$cluster_info" --cluster-id=1 \ No newline at end of file + exec docker-entrypoint.sh \ No newline at end of file diff --git a/test/testdata/testdata.go b/test/testdata/testdata.go index b0408255e..8ae6376d8 100644 --- a/test/testdata/testdata.go +++ b/test/testdata/testdata.go @@ -1,5 +1,5 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/tools/tools.go b/tools/tools.go index 9108b71b2..b58bde5f9 100644 --- a/tools/tools.go +++ b/tools/tools.go @@ -2,7 +2,7 @@ // +build tools /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -20,4 +20,6 @@ limitations under the License. // This package imports things required by build scripts, to force `go mod` to see them as dependencies package tools -import _ "github.com/golang/mock/mockgen" +import ( + _ "github.com/golang/mock/mockgen" +) diff --git a/version/version.go b/version/version.go index 754f87756..687dd9db7 100644 --- a/version/version.go +++ b/version/version.go @@ -1,17 +1,20 @@ /* -Copyright ApeCloud, Inc. +Copyright (C) 2022-2023 ApeCloud Co., Ltd -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 +This file is part of KubeBlocks project - http://www.apache.org/licenses/LICENSE-2.0 +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. -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 program is distributed in the hope that it will be useful +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program. If not, see . */ package version @@ -34,10 +37,10 @@ var K3dVersion = "5.4.4" // K3sImageTag is k3s image tag var K3sImageTag = "v1.23.8-k3s1" -// DefaultKubeBlocksVersion the default KubeBlocks version that kbcli installed +// DefaultKubeBlocksVersion is the default KubeBlocks version that installed by kbcli var DefaultKubeBlocksVersion string -// GetVersion returns the version for cli, it gets it from "git describe --tags" or returns "dev" when doing simple go build +// GetVersion returns the version for cli, either got from "git describe --tags" or "dev" when doing simple go build func GetVersion() string { if len(Version) == 0 { return "v1-dev"