diff options
29 files changed, 1000 insertions, 184 deletions
diff --git a/.github/actions/check_cirrus_cron/cron_failures.sh b/.github/actions/check_cirrus_cron/cron_failures.sh new file mode 100755 index 000000000..2693df417 --- /dev/null +++ b/.github/actions/check_cirrus_cron/cron_failures.sh @@ -0,0 +1,116 @@ +#!/bin/bash + +set -eo pipefail + +# Intended to be executed from a github action workflow step. +# Outputs the Cirrus cron names and IDs of any failed builds + +err() { + # Ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-commands-for-github-actions + echo "::error file=${BASH_SOURCE[0]},line=${BASH_LINENO[0]}::${1:-No error message given}" + exit 1 +} + +_errfmt="Expecting %s value to not be empty" +if [[ -z "$GITHUB_REPOSITORY" ]]; then + err $(printf "$_errfmt" "\$GITHUB_REPOSITORY") +elif [[ -z "$NAME_ID_FILEPATH" ]]; then + err $(printf "$_errfmt" "\$NAME_ID_FILEPATH") +fi + +mkdir -p artifacts +cat > ./artifacts/query_raw.json << "EOF" +{"query":" + query CronNameStatus($owner: String!, $repo: String!) { + githubRepository(owner: $owner, name: $repo) { + cronSettings { + name + lastInvocationBuild { + id + status + } + } + } + } +", +"variables":"{ + \"owner\": \"@@OWNER@@\", + \"repo\": \"@@REPO@@\" +}"} +EOF +# Makes for easier copy/pasting query to/from +# https://cirrus-ci.com/explorer +owner=$(cut -d '/' -f 1 <<<"$GITHUB_REPOSITORY") +repo=$(cut -d '/' -f 2 <<<"$GITHUB_REPOSITORY") +sed -i -r -e "s/@@OWNER@@/$owner/g" -e "s/@@REPO@@/$repo/g" ./artifacts/query_raw.json + +echo "::group::Posting GraphQL Query" +# Easier to debug in error-reply when query is compacted +tr -d '\n' < ./artifacts/query_raw.json | tr -s ' ' | tee ./artifacts/query.json | \ + jq --indent 4 --color-output . + +if grep -q '@@' ./artifacts/query.json; then + err "Found unreplaced substitution token in raw query JSON" +fi +curl \ + --request POST \ + --silent \ + --location \ + --header 'content-type: application/json' \ + --url 'https://api.cirrus-ci.com/graphql' \ + --data @./artifacts/query.json \ + --output ./artifacts/reply.json +echo "::endgroup::" + +echo "::group::Received GraphQL Reply" +jq --indent 4 --color-output . <./artifacts/reply.json || \ + cat ./artifacts/reply.json +echo "::endgroup::" + +# Desireable to catch non-JSON encoded errors in reply. +if grep -qi 'error' ./artifacts/reply.json; then + err "Found the word 'error' in reply" +fi + +# e.x. reply.json +# { +# "data": { +# "githubRepository": { +# "cronSettings": [ +# { +# "name": "Keepalive_v2.0", +# "lastInvocationBuild": { +# "id": "5776050544181248", +# "status": "EXECUTING" +# } +# }, +# { +# "name": "Keepalive_v1.9", +# "lastInvocationBuild": { +# "id": "5962921081569280", +# "status": "COMPLETED" +# } +# }, +# { +# "name": "Keepalive_v2.0.5-rhel", +# "lastInvocationBuild": { +# "id": "5003065549914112", +# "status": "FAILED" +# } +# } +# ] +# } +# } +# } +_filt='.data.githubRepository.cronSettings | map(select(.lastInvocationBuild.status=="FAILED") | { name:.name, id:.lastInvocationBuild.id} | join(" ")) | join("\n")' +jq --raw-output "$_filt" ./artifacts/reply.json > "$NAME_ID_FILEPATH" + +echo "<Cron Name> <Failed Build ID>" +cat "$NAME_ID_FILEPATH" + +# Don't rely on a newline present for zero/one output line, always count words +records=$(wc --words "$NAME_ID_FILEPATH" | cut -d ' ' -f 1) +# Always two words per record +failures=$((records/2)) +echo "::set-output name=failures::$failures" +echo "Total failed Cirrus-CI cron builds: $failures" diff --git a/.github/workflows/check_cirrus_cron.yml b/.github/workflows/check_cirrus_cron.yml new file mode 100644 index 000000000..86f8c26dc --- /dev/null +++ b/.github/workflows/check_cirrus_cron.yml @@ -0,0 +1,81 @@ +--- + +# Format Ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions + +# Required to un-FUBAR default ${{github.workflow}} value +name: check_cirrus_cron + +on: + schedule: + # Assume cirrus cron jobs runs at least once per day + - cron: '59 23 * * *' + # Debug: Allow triggering job manually in github-actions WebUI + workflow_dispatch: {} + +env: + # Debug-mode can reveal secrets, only enable by a secret value. + # Ref: https://help.github.com/en/actions/configuring-and-managing-workflows/managing-a-workflow-run#enabling-step-debug-logging + ACTIONS_STEP_DEBUG: '${{ secrets.ACTIONS_STEP_DEBUG }}' + # File with CSV listing of zero or more e-mail addresses for delivery + # of daily failure notice e-mails. + FAILMAILCSV: './contrib/cirrus/cron-fail_addrs.csv' + # Filename for table of cron-name to build-id data + # (must be in $GITHUB_WORKSPACE/artifacts/) + NAME_ID_FILEPATH: './artifacts/name_id.txt' + +jobs: + cron_failures: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + with: + ref: master + persist-credentials: false + + - name: Get failed cron names and Build IDs + id: cron + run: './.github/actions/${{ github.workflow }}/${{ github.job }}.sh' + + - if: steps.cron.outputs.failures > 0 + shell: bash + # Must be inline, since context expressions are used. + # Ref: https://docs.github.com/en/free-pro-team@latest/actions/reference/context-and-expression-syntax-for-github-actions + run: | + set -eo pipefail + ( + echo "Detected one or more Cirrus-CI cron-triggered jobs have failed recently:" + echo "" + + while read -r NAME BID; do + echo "Cron build '$NAME' Failed: https://cirrus-ci.com/build/$BID" + done < "$NAME_ID_FILEPATH" + + echo "" + echo "# Source: ${{ github.workflow }} workflow on ${{ github.repository }}." + # Separate content from sendgrid.com automatic footer. + echo "" + ) > ./artifacts/email_body.txt + + - if: steps.cron.outputs.failures > 0 + id: mailto + run: printf "::set-output name=csv::%s\n" $(cat "$FAILMAILCSV") + + - if: steps.mailto.outputs.csv != '' + name: Send failure notification e-mail + # Ref: https://github.com/dawidd6/action-send-mail + uses: dawidd6/action-send-mail@v2.2.2 + with: + server_address: ${{secrets.ACTION_MAIL_SERVER}} + server_port: 465 + username: ${{secrets.ACTION_MAIL_USERNAME}} + password: ${{secrets.ACTION_MAIL_PASSWORD}} + subject: Cirrus-CI cron build failures on ${{github.repository}} + to: ${{steps.mailto.outputs.csv}} + from: ${{secrets.ACTION_MAIL_SENDER}} + body: file://./artifacts/email_body.txt + + - if: always() + uses: actions/upload-artifact@v2 + with: + name: ${{ github.job }}_artifacts + path: artifacts/* diff --git a/cmd/podman/common/completion.go b/cmd/podman/common/completion.go index 00123f9e6..db4d3d0d3 100644 --- a/cmd/podman/common/completion.go +++ b/cmd/podman/common/completion.go @@ -23,90 +23,75 @@ var ( LogLevels = []string{"debug", "info", "warn", "error", "fatal", "panic"} ) -func getContainers(toComplete string, statuses ...string) ([]string, cobra.ShellCompDirective) { +type completeType int + +const ( + // complete names and IDs after two chars + completeDefault completeType = iota + // only complete IDs + completeIDs + // only complete Names + completeNames +) + +type keyValueCompletion map[string]func(s string) ([]string, cobra.ShellCompDirective) + +func getContainers(toComplete string, cType completeType, statuses ...string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} listOpts := entities.ContainerListOptions{ Filters: make(map[string][]string), } listOpts.All = true listOpts.Pod = true + if len(statuses) > 0 { + listOpts.Filters["status"] = statuses + } - // TODO: The api doesn't handle several different statuses correct see: - // https://github.com/containers/podman/issues/8344 - // Instead of looping over the statuses we should be able to set - // listOpts.Filters["status"] = statuses - - var containers []entities.ListContainer - var err error - if len(statuses) == 0 { - containers, err = registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError - } - } else { - for _, s := range statuses { - listOpts.Filters["status"] = []string{s} - res, err := registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError - } - containers = append(containers, res...) - } + containers, err := registry.ContainerEngine().ContainerList(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError } for _, c := range containers { - // include ids in suggestions if more then 2 chars are typed - if len(toComplete) > 1 && strings.HasPrefix(c.ID, toComplete) { + // include ids in suggestions if cType == completeIDs or + // more then 2 chars are typed and cType == completeDefault + if ((len(toComplete) > 1 && cType == completeDefault) || + cType == completeIDs) && strings.HasPrefix(c.ID, toComplete) { suggestions = append(suggestions, c.ID[0:12]+"\t"+c.PodName) } // include name in suggestions - if strings.HasPrefix(c.Names[0], toComplete) { + if cType != completeIDs && strings.HasPrefix(c.Names[0], toComplete) { suggestions = append(suggestions, c.Names[0]+"\t"+c.PodName) } } return suggestions, cobra.ShellCompDirectiveNoFileComp } -func getPods(toComplete string, statuses ...string) ([]string, cobra.ShellCompDirective) { +func getPods(toComplete string, cType completeType, statuses ...string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} listOpts := entities.PodPSOptions{ Filters: make(map[string][]string), } + if len(statuses) > 0 { + listOpts.Filters["status"] = statuses + } - // TODO: The api doesn't handle several different statuses correct see: - // https://github.com/containers/podman/issues/8344 - // Instead of looping over the statuses we should be able to set - // listOpts.Filters["status"] = statuses - - var pods []*entities.ListPodsReport - var err error - if len(statuses) == 0 { - pods, err = registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError - } - } else { - for _, s := range statuses { - listOpts.Filters["status"] = []string{s} - res, err := registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) - if err != nil { - cobra.CompErrorln(err.Error()) - return nil, cobra.ShellCompDirectiveError - } - pods = append(pods, res...) - } + pods, err := registry.ContainerEngine().PodPs(registry.GetContext(), listOpts) + if err != nil { + cobra.CompErrorln(err.Error()) + return nil, cobra.ShellCompDirectiveError } for _, pod := range pods { - // include ids in suggestions if more then 2 chars are typed - if len(toComplete) > 1 && strings.HasPrefix(pod.Id, toComplete) { + // include ids in suggestions if cType == completeIDs or + // more then 2 chars are typed and cType == completeDefault + if ((len(toComplete) > 1 && cType == completeDefault) || + cType == completeIDs) && strings.HasPrefix(pod.Id, toComplete) { suggestions = append(suggestions, pod.Id[0:12]) } // include name in suggestions - if strings.HasPrefix(pod.Name, toComplete) { + if cType != completeIDs && strings.HasPrefix(pod.Name, toComplete) { suggestions = append(suggestions, pod.Name) } } @@ -188,9 +173,7 @@ func getRegistries() ([]string, cobra.ShellCompDirective) { func getNetworks(toComplete string) ([]string, cobra.ShellCompDirective) { suggestions := []string{} - networkListOptions := entities.NetworkListOptions{ - Filter: "name=" + toComplete, - } + networkListOptions := entities.NetworkListOptions{} networks, err := registry.ContainerEngine().NetworkList(registry.Context(), networkListOptions) if err != nil { @@ -198,8 +181,10 @@ func getNetworks(toComplete string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveError } - for _, network := range networks { - suggestions = append(suggestions, network.Name) + for _, n := range networks { + if strings.HasPrefix(n.Name, toComplete) { + suggestions = append(suggestions, n.Name) + } } return suggestions, cobra.ShellCompDirectiveNoFileComp } @@ -244,6 +229,36 @@ func validCurrentCmdLine(cmd *cobra.Command, args []string, toComplete string) b return true } +func prefixSlice(pre string, slice []string) []string { + for i := range slice { + slice[i] = pre + slice[i] + } + return slice +} + +func completeKeyValues(toComplete string, k keyValueCompletion) ([]string, cobra.ShellCompDirective) { + suggestions := make([]string, 0, len(k)) + directive := cobra.ShellCompDirectiveNoFileComp + for key, getComps := range k { + if strings.HasPrefix(toComplete, key) { + if getComps != nil { + suggestions, dir := getComps(toComplete[len(key):]) + return prefixSlice(key, suggestions), dir + } + return nil, cobra.ShellCompDirectiveNoFileComp + } + if strings.HasPrefix(key, toComplete) { + suggestions = append(suggestions, key) + latKey := key[len(key)-1:] + if latKey == "=" || latKey == ":" { + // make sure we don't add a space after ':' or '=' + directive = cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + } + } + } + return suggestions, directive +} + /* Autocomplete Functions for cobra ValidArgsFunction */ // AutocompleteContainers - Autocomplete all container names. @@ -251,7 +266,7 @@ func AutocompleteContainers(cmd *cobra.Command, args []string, toComplete string if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete) + return getContainers(toComplete, completeDefault) } // AutocompleteContainersCreated - Autocomplete only created container names. @@ -259,7 +274,7 @@ func AutocompleteContainersCreated(cmd *cobra.Command, args []string, toComplete if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete, "created") + return getContainers(toComplete, completeDefault, "created") } // AutocompleteContainersExited - Autocomplete only exited container names. @@ -267,7 +282,7 @@ func AutocompleteContainersExited(cmd *cobra.Command, args []string, toComplete if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete, "exited") + return getContainers(toComplete, completeDefault, "exited") } // AutocompleteContainersPaused - Autocomplete only paused container names. @@ -275,7 +290,7 @@ func AutocompleteContainersPaused(cmd *cobra.Command, args []string, toComplete if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete, "paused") + return getContainers(toComplete, completeDefault, "paused") } // AutocompleteContainersRunning - Autocomplete only running container names. @@ -283,7 +298,7 @@ func AutocompleteContainersRunning(cmd *cobra.Command, args []string, toComplete if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete, "running") + return getContainers(toComplete, completeDefault, "running") } // AutocompleteContainersStartable - Autocomplete only created and exited container names. @@ -291,7 +306,7 @@ func AutocompleteContainersStartable(cmd *cobra.Command, args []string, toComple if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getContainers(toComplete, "created", "exited") + return getContainers(toComplete, completeDefault, "created", "exited") } // AutocompletePods - Autocomplete all pod names. @@ -299,7 +314,7 @@ func AutocompletePods(cmd *cobra.Command, args []string, toComplete string) ([]s if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getPods(toComplete) + return getPods(toComplete, completeDefault) } // AutocompletePodsRunning - Autocomplete only running pod names. @@ -308,7 +323,7 @@ func AutocompletePodsRunning(cmd *cobra.Command, args []string, toComplete strin if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - return getPods(toComplete, "running", "degraded") + return getPods(toComplete, completeDefault, "running", "degraded") } // AutocompleteContainersAndPods - Autocomplete container names and pod names. @@ -316,8 +331,8 @@ func AutocompleteContainersAndPods(cmd *cobra.Command, args []string, toComplete if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - containers, _ := getContainers(toComplete) - pods, _ := getPods(toComplete) + containers, _ := getContainers(toComplete, completeDefault) + pods, _ := getPods(toComplete, completeDefault) return append(containers, pods...), cobra.ShellCompDirectiveNoFileComp } @@ -326,7 +341,7 @@ func AutocompleteContainersAndImages(cmd *cobra.Command, args []string, toComple if !validCurrentCmdLine(cmd, args, toComplete) { return nil, cobra.ShellCompDirectiveNoFileComp } - containers, _ := getContainers(toComplete) + containers, _ := getContainers(toComplete, completeDefault) images, _ := getImages(toComplete) return append(containers, images...), cobra.ShellCompDirectiveNoFileComp } @@ -381,7 +396,7 @@ func AutocompleteCpCommand(cmd *cobra.Command, args []string, toComplete string) return nil, cobra.ShellCompDirectiveNoFileComp } if len(args) < 2 { - containers, _ := getContainers(toComplete) + containers, _ := getContainers(toComplete, completeDefault) for _, container := range containers { // TODO: Add path completion for inside the container if possible if strings.HasPrefix(container, toComplete) { @@ -448,40 +463,20 @@ func AutocompleteCreateAttach(cmd *cobra.Command, args []string, toComplete stri // AutocompleteNamespace - Autocomplete namespace options. // -> host,container:[name],ns:[path],private func AutocompleteNamespace(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - namespacesOptions := []string{"host", "container:", "ns:", "private"} - - switch { - case strings.HasPrefix(toComplete, "container:"): - // Complete containers after colon - containers, _ := getContainers(toComplete[10:]) //trim "container:" - - // add "container:" in front of the suggestions - var suggestions []string - for _, container := range containers { - suggestions = append(suggestions, "container:"+container) - } - - return suggestions, cobra.ShellCompDirectiveNoFileComp - - case strings.HasPrefix(toComplete, "ns:"): - // Complete path after colon - return nil, cobra.ShellCompDirectiveDefault - - case strings.HasPrefix(toComplete, "c") || strings.HasPrefix(toComplete, "n"): - // don't insert space for container: and ns: - return []string{"container:", "ns:"}, cobra.ShellCompDirectiveNoSpace + kv := keyValueCompletion{ + "container:": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeDefault) }, + "ns:": func(s string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveDefault }, + "host": nil, + "private": nil, } - return namespacesOptions, cobra.ShellCompDirectiveNoFileComp + return completeKeyValues(toComplete, kv) } // AutocompleteUserNamespace - Autocomplete namespace options. // -> same as AutocompleteNamespace with "auto", "keep-id" added func AutocompleteUserNamespace(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { results, directive := AutocompleteNamespace(cmd, args, toComplete) - if directive == cobra.ShellCompDirectiveNoFileComp { - // add the auto and keep-id options - results = append(results, "auto", "keep-id") - } + results = append(results, "auto", "keep-id") return results, directive } @@ -535,29 +530,18 @@ func AutocompleteRestartOption(cmd *cobra.Command, args []string, toComplete str // AutocompleteSecurityOption - Autocomplete security options options. func AutocompleteSecurityOption(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - SecurityOptions := []string{"apparmor=", "no-new-privileges", "seccomp=", "label="} - switch { - case strings.HasPrefix(toComplete, "apparmor=u"): - // add space after unconfined - return []string{"apparmor=unconfined"}, cobra.ShellCompDirectiveNoFileComp - - case strings.HasPrefix(toComplete, "label=d"): - // add space after disable - return []string{"label=disable"}, cobra.ShellCompDirectiveNoFileComp - - case strings.HasPrefix(toComplete, "label="): - return []string{"label=user:", "label=role:", "label=type:", "label=level:", "label=filetype:", "label=disable"}, - cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace - - case strings.HasPrefix(toComplete, "seccomp="): - // complete files - return nil, cobra.ShellCompDirectiveDefault - - case strings.HasPrefix(toComplete, "n"): - // add space if no-new-privileges - return []string{"no-new-privileges"}, cobra.ShellCompDirectiveNoFileComp + kv := keyValueCompletion{ + "apparmor=": nil, + "no-new-privileges": nil, + "seccomp=": func(s string) ([]string, cobra.ShellCompDirective) { return nil, cobra.ShellCompDirectiveDefault }, + "label=": func(s string) ([]string, cobra.ShellCompDirective) { + if strings.HasPrefix(s, "d") { + return []string{"disable"}, cobra.ShellCompDirectiveNoFileComp + } + return []string{"user:", "role:", "type:", "level:", "filetype:", "disable"}, cobra.ShellCompDirectiveNoSpace | cobra.ShellCompDirectiveNoFileComp + }, } - return SecurityOptions, cobra.ShellCompDirectiveNoFileComp | cobra.ShellCompDirectiveNoSpace + return completeKeyValues(toComplete, kv) } // AutocompleteStopSignal - Autocomplete stop signal options. @@ -763,3 +747,48 @@ func AutocompleteSDNotify(cmd *cobra.Command, args []string, toComplete string) types := []string{"container", "conmon", "ignore"} return types, cobra.ShellCompDirectiveNoFileComp } + +var containerStatuses = []string{"created", "running", "paused", "stopped", "exited", "unknown"} + +// AutocompletePsFilters - Autocomplete ps filter options. +func AutocompletePsFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + kv := keyValueCompletion{ + "id=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeIDs) }, + "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeNames) }, + "status=": func(_ string) ([]string, cobra.ShellCompDirective) { + return containerStatuses, cobra.ShellCompDirectiveNoFileComp + }, + "ancestor": func(s string) ([]string, cobra.ShellCompDirective) { return getImages(s) }, + "before=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeDefault) }, + "since=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeDefault) }, + "volume=": func(s string) ([]string, cobra.ShellCompDirective) { return getVolumes(s) }, + "health=": func(_ string) ([]string, cobra.ShellCompDirective) { + return []string{define.HealthCheckHealthy, + define.HealthCheckUnhealthy}, cobra.ShellCompDirectiveNoFileComp + }, + "label=": nil, + "exited=": nil, + "until=": nil, + } + return completeKeyValues(toComplete, kv) +} + +// AutocompletePodPsFilters - Autocomplete pod ps filter options. +func AutocompletePodPsFilters(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + kv := keyValueCompletion{ + "id=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(s, completeIDs) }, + "name=": func(s string) ([]string, cobra.ShellCompDirective) { return getPods(s, completeNames) }, + "status=": func(_ string) ([]string, cobra.ShellCompDirective) { + return []string{"stopped", "running", + "paused", "exited", "dead", "created", "degraded"}, cobra.ShellCompDirectiveNoFileComp + }, + "ctr-ids=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeIDs) }, + "ctr-names=": func(s string) ([]string, cobra.ShellCompDirective) { return getContainers(s, completeNames) }, + "ctr-number=": nil, + "ctr-status=": func(_ string) ([]string, cobra.ShellCompDirective) { + return containerStatuses, cobra.ShellCompDirectiveNoFileComp + }, + "label=": nil, + } + return completeKeyValues(toComplete, kv) +} diff --git a/cmd/podman/containers/ps.go b/cmd/podman/containers/ps.go index 642feb5e0..a1a41ae08 100644 --- a/cmd/podman/containers/ps.go +++ b/cmd/podman/containers/ps.go @@ -64,8 +64,7 @@ func listFlagSet(cmd *cobra.Command) { filterFlagName := "filter" flags.StringSliceVarP(&filters, filterFlagName, "f", []string{}, "Filter output based on conditions given") - //TODO add custom filter function - _ = cmd.RegisterFlagCompletionFunc(filterFlagName, completion.AutocompleteNone) + _ = cmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePsFilters) formatFlagName := "format" flags.StringVar(&listOpts.Format, formatFlagName, "", "Pretty-print containers to JSON or using a Go template") diff --git a/cmd/podman/networks/connect.go b/cmd/podman/networks/connect.go new file mode 100644 index 000000000..a7636688c --- /dev/null +++ b/cmd/podman/networks/connect.go @@ -0,0 +1,47 @@ +package network + +import ( + "github.com/containers/common/pkg/completion" + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" +) + +var ( + networkConnectDescription = `Add container to a network` + networkConnectCommand = &cobra.Command{ + Use: "connect [options] NETWORK CONTAINER", + Short: "network connect", + Long: networkConnectDescription, + RunE: networkConnect, + Example: `podman network connect web secondary`, + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AutocompleteNetworks, + } +) + +var ( + networkConnectOptions entities.NetworkConnectOptions +) + +func networkConnectFlags(cmd *cobra.Command) { + flags := cmd.Flags() + aliasFlagName := "alias" + flags.StringSliceVar(&networkConnectOptions.Aliases, aliasFlagName, []string{}, "network scoped alias for container") + _ = cmd.RegisterFlagCompletionFunc(aliasFlagName, completion.AutocompleteNone) +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: networkConnectCommand, + Parent: networkCmd, + }) + networkConnectFlags(networkConnectCommand) +} + +func networkConnect(cmd *cobra.Command, args []string) error { + networkConnectOptions.Container = args[1] + return registry.ContainerEngine().NetworkConnect(registry.Context(), args[0], networkConnectOptions) +} diff --git a/cmd/podman/networks/disconnect.go b/cmd/podman/networks/disconnect.go new file mode 100644 index 000000000..598c23a1c --- /dev/null +++ b/cmd/podman/networks/disconnect.go @@ -0,0 +1,45 @@ +package network + +import ( + "github.com/containers/podman/v2/cmd/podman/common" + "github.com/containers/podman/v2/cmd/podman/registry" + "github.com/containers/podman/v2/pkg/domain/entities" + "github.com/spf13/cobra" + "github.com/spf13/pflag" +) + +var ( + networkDisconnectDescription = `Remove container from a network` + networkDisconnectCommand = &cobra.Command{ + Use: "disconnect [options] NETWORK CONTAINER", + Short: "network rm", + Long: networkDisconnectDescription, + RunE: networkDisconnect, + Example: `podman network disconnect web secondary`, + Args: cobra.ExactArgs(2), + ValidArgsFunction: common.AutocompleteNetworks, + } +) + +var ( + networkDisconnectOptions entities.NetworkDisconnectOptions +) + +func networkDisconnectFlags(flags *pflag.FlagSet) { + flags.BoolVarP(&networkDisconnectOptions.Force, "force", "f", false, "force removal of container from network") +} + +func init() { + registry.Commands = append(registry.Commands, registry.CliCommand{ + Mode: []entities.EngineMode{entities.ABIMode, entities.TunnelMode}, + Command: networkDisconnectCommand, + Parent: networkCmd, + }) + flags := networkDisconnectCommand.Flags() + networkDisconnectFlags(flags) +} + +func networkDisconnect(cmd *cobra.Command, args []string) error { + networkDisconnectOptions.Container = args[1] + return registry.ContainerEngine().NetworkDisconnect(registry.Context(), args[0], networkDisconnectOptions) +} diff --git a/cmd/podman/pods/ps.go b/cmd/podman/pods/ps.go index 51c2e92f0..99d324411 100644 --- a/cmd/podman/pods/ps.go +++ b/cmd/podman/pods/ps.go @@ -57,8 +57,7 @@ func init() { filterFlagName := "filter" flags.StringSliceVarP(&inputFilters, filterFlagName, "f", []string{}, "Filter output based on conditions given") - //TODO complete filters - _ = psCmd.RegisterFlagCompletionFunc(filterFlagName, completion.AutocompleteNone) + _ = psCmd.RegisterFlagCompletionFunc(filterFlagName, common.AutocompletePodPsFilters) formatFlagName := "format" flags.StringVar(&psInput.Format, formatFlagName, "", "Pretty-print pods to JSON or using a Go template") diff --git a/commands-demo.md b/commands-demo.md index 74879842f..cea7a63a3 100644 --- a/commands-demo.md +++ b/commands-demo.md @@ -47,6 +47,8 @@ | [podman-mount(1)](https://podman.readthedocs.io/en/latest/markdown/podman-mount.1.html) | Mount a working container's root filesystem | | [podman-network(1)](https://podman.readthedocs.io/en/latest/network.html) | Manage Podman CNI networks | | [podman-network-create(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-create.1.html) | Create a CNI network | +| [podman-network-connect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-connect.1.html) | Connect a container to a CNI network | +| [podman-network-disconnect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-dosconnect.1.html) | Disconnect a container from a CNI network | | [podman-network-inspect(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-inspect.1.html) | Displays the raw CNI network configuration for one or more networks | | [podman-network-ls(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-ls.1.html) | Display a summary of CNI networks | | [podman-network-rm(1)](https://podman.readthedocs.io/en/latest/markdown/podman-network-rm.1.html) | Remove one or more CNI networks | diff --git a/contrib/cirrus/cron-fail_addrs.csv b/contrib/cirrus/cron-fail_addrs.csv new file mode 100644 index 000000000..c25fc1226 --- /dev/null +++ b/contrib/cirrus/cron-fail_addrs.csv @@ -0,0 +1 @@ +rh.container.bot@gmail.com diff --git a/docs/source/markdown/podman-network-connect.1.md b/docs/source/markdown/podman-network-connect.1.md new file mode 100644 index 000000000..58b6e5c44 --- /dev/null +++ b/docs/source/markdown/podman-network-connect.1.md @@ -0,0 +1,34 @@ +% podman-network-connect(1) + +## NAME +podman\-network\-connect - Connect a container to a network + +## SYNOPSIS +**podman network connect** [*options*] network container + +## DESCRIPTION +Connects a container to a network. A container can be connected to a network by name or by ID. +Once connected, the container can communicate with other containers in the same network. + +## OPTIONS +#### **--alias** +Add network-scoped alias for the container. If the network is using the `dnsname` CNI plugin, these aliases +can be used for name resolution on the given network. Multiple *--alias* options may be specificed as input. + +## EXAMPLE + +Connect a container named *web* to a network named *test* +``` +podman network connect test web +``` + +Connect a container name *web* to a network named *test* with two aliases: web1 and web2 +``` +podman network connect --alias web1 --alias web2 test web +``` + +## SEE ALSO +podman(1), podman-network(1), podman-network-disconnect(1), podman-network-inspect(1) + +## HISTORY +November 2020, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network-disconnect.1.md b/docs/source/markdown/podman-network-disconnect.1.md new file mode 100644 index 000000000..95c7018a8 --- /dev/null +++ b/docs/source/markdown/podman-network-disconnect.1.md @@ -0,0 +1,29 @@ +% podman-network-disconnect(1) + +## NAME +podman\-network\-disconnect - Disconnect a container from a network + +## SYNOPSIS +**podman network disconnect** [*options*] network container + +## DESCRIPTION +Disconnects a container from a network. + +## OPTIONS +#### **--force**, **-f** + +Force the container to disconnect from a network + +## EXAMPLE +Disconnect a container named *web* from a network called *test*. + +``` +podman network disconnect test web +``` + + +## SEE ALSO +podman(1), podman-network(1), podman-network-connect(1) + +## HISTORY +November 2020, Originally compiled by Brent Baude <bbaude@redhat.com> diff --git a/docs/source/markdown/podman-network.1.md b/docs/source/markdown/podman-network.1.md index f05b2b78f..d21b200d9 100644 --- a/docs/source/markdown/podman-network.1.md +++ b/docs/source/markdown/podman-network.1.md @@ -13,7 +13,9 @@ The network command manages CNI networks for Podman. It is not supported for roo | Command | Man Page | Description | | ------- | --------------------------------------------------- | ---------------------------------------------------------------------------- | +| connect | [podman-network-connect(1)](podman-network-connect.1.md)| Connect a container to a network| | create | [podman-network-create(1)](podman-network-create.1.md)| Create a Podman CNI network| +| disconnect | [podman-network-disconnect(1)](podman-network-disconnect.1.md)| Disconnect a container from a network| | inspect | [podman-network-inspect(1)](podman-network-inspect.1.md)| Displays the raw CNI network configuration for one or more networks| | ls | [podman-network-ls(1)](podman-network-ls.1.md)| Display a summary of CNI networks | | rm | [podman-network-rm(1)](podman-network-rm.1.md)| Remove one or more CNI networks | diff --git a/docs/source/network.rst b/docs/source/network.rst index e7848c90e..0414c0880 100644 --- a/docs/source/network.rst +++ b/docs/source/network.rst @@ -1,8 +1,12 @@ Network ======= +:doc:`connect <markdown/podman-network-connect.1>` network connect + :doc:`create <markdown/podman-network-create.1>` network create +:doc:`disconnect <markdown/podman-network-disconnect.1>` network disconnect + :doc:`inspect <markdown/podman-network-inspect.1>` network inspect :doc:`ls <markdown/podman-network-ls.1>` network list diff --git a/libpod/container.go b/libpod/container.go index 9009a4ec8..4b9e6a5ba 100644 --- a/libpod/container.go +++ b/libpod/container.go @@ -206,6 +206,10 @@ type ContainerState struct { // and not delegated to the OCI runtime. ExtensionStageHooks map[string][]spec.Hook `json:"extensionStageHooks,omitempty"` + // NetInterfaceDescriptions describe the relationship between a CNI + // network and an interface names + NetInterfaceDescriptions ContainerNetworkDescriptions `json:"networkDescriptions,omitempty"` + // containerPlatformState holds platform-specific container state. containerPlatformState } @@ -244,6 +248,10 @@ type ContainerImageVolume struct { ReadWrite bool `json:"rw"` } +// ContainerNetworkDescriptions describes the relationship between the CNI +// network and the ethN where N is an integer +type ContainerNetworkDescriptions map[string]int + // Config accessors // Unlocked @@ -1102,3 +1110,19 @@ func (c *Container) networksByNameIndex() (map[string]int, error) { } return networkNamesByIndex, nil } + +// add puts the new given CNI network name into the tracking map +// and assigns it a new integer based on the map length +func (d ContainerNetworkDescriptions) add(networkName string) { + d[networkName] = len(d) +} + +// getInterfaceByName returns a formatted interface name for a given +// network along with a bool as to whether the network existed +func (d ContainerNetworkDescriptions) getInterfaceByName(networkName string) (string, bool) { + val, exists := d[networkName] + if !exists { + return "", exists + } + return fmt.Sprintf("eth%d", val), exists +} diff --git a/libpod/define/errors.go b/libpod/define/errors.go index 471827b7c..b96d36429 100644 --- a/libpod/define/errors.go +++ b/libpod/define/errors.go @@ -178,4 +178,7 @@ var ( // ErrStoreNotInitialized indicates that the container storage was never // initialized. ErrStoreNotInitialized = errors.New("the container storage was never initialized") + + // ErrNoNetwork indicates that a container has no net namespace, like network=none + ErrNoNetwork = errors.New("container has no network namespace") ) diff --git a/libpod/events.go b/libpod/events.go index 95317eb01..e199a3846 100644 --- a/libpod/events.go +++ b/libpod/events.go @@ -50,6 +50,18 @@ func (c *Container) newContainerExitedEvent(exitCode int32) { } } +// netNetworkEvent creates a new event based on a network connect/disconnect +func (c *Container) newNetworkEvent(status events.Status, netName string) { + e := events.NewEvent(status) + e.ID = c.ID() + e.Name = c.Name() + e.Type = events.Network + e.Network = netName + if err := c.runtime.eventer.Write(e); err != nil { + logrus.Errorf("unable to write pod event: %q", err) + } +} + // newPodEvent creates a new event for a libpod pod func (p *Pod) newPodEvent(status events.Status) { e := events.NewEvent(status) diff --git a/libpod/events/config.go b/libpod/events/config.go index 2ec3111fe..af09a65ae 100644 --- a/libpod/events/config.go +++ b/libpod/events/config.go @@ -30,6 +30,8 @@ type Event struct { Image string `json:",omitempty"` // Name where applicable Name string `json:",omitempty"` + // Network is the network name in a network event + Network string `json:"network,omitempty"` // Status describes the event that occurred Status Status // Time the event occurred @@ -101,6 +103,8 @@ const ( Container Type = "container" // Image - event is related to images Image Type = "image" + // Network - event is related to networks + Network Type = "network" // Pod - event is related to pods Pod Type = "pod" // System - event is related to Podman whole and not to any specific @@ -141,6 +145,10 @@ const ( LoadFromArchive Status = "loadfromarchive" // Mount ... Mount Status = "mount" + // NetworkConnect + NetworkConnect Status = "connect" + // NetworkDisconnect + NetworkDisconnect Status = "disconnect" // Pause ... Pause Status = "pause" // Prune ... diff --git a/libpod/events/events.go b/libpod/events/events.go index 42939d64c..4e7267af3 100644 --- a/libpod/events/events.go +++ b/libpod/events/events.go @@ -77,6 +77,8 @@ func (e *Event) ToHumanReadable() string { } } humanFormat += ")" + case Network: + humanFormat = fmt.Sprintf("%s %s %s %s (container=%s, name=%s)", e.Time, e.Type, e.Status, e.ID, e.ID, e.Network) case Image: humanFormat = fmt.Sprintf("%s %s %s %s %s", e.Time, e.Type, e.Status, e.ID, e.Name) case System: @@ -115,6 +117,8 @@ func StringToType(name string) (Type, error) { return Container, nil case Image.String(): return Image, nil + case Network.String(): + return Network, nil case Pod.String(): return Pod, nil case System.String(): @@ -162,6 +166,10 @@ func StringToStatus(name string) (Status, error) { return LoadFromArchive, nil case Mount.String(): return Mount, nil + case NetworkConnect.String(): + return NetworkConnect, nil + case NetworkDisconnect.String(): + return NetworkDisconnect, nil case Pause.String(): return Pause, nil case Prune.String(): diff --git a/libpod/events/journal_linux.go b/libpod/events/journal_linux.go index 5e3be8009..9a514e302 100644 --- a/libpod/events/journal_linux.go +++ b/libpod/events/journal_linux.go @@ -56,6 +56,9 @@ func (e EventJournalD) Write(ee Event) error { } m["PODMAN_LABELS"] = string(b) } + case Network: + m["PODMAN_ID"] = ee.ID + m["PODMAN_NETWORK_NAME"] = ee.Network case Volume: m["PODMAN_NAME"] = ee.Name } @@ -197,6 +200,9 @@ func newEventFromJournalEntry(entry *sdjournal.JournalEntry) (*Event, error) { / newEvent.Details = Details{Attributes: labels} } } + case Network: + newEvent.ID = entry.Fields["PODMAN_ID"] + newEvent.Network = entry.Fields["PODMAN_NETWORK_NAME"] case Image: newEvent.ID = entry.Fields["PODMAN_ID"] } diff --git a/libpod/events/logfile.go b/libpod/events/logfile.go index b70102450..57e38b815 100644 --- a/libpod/events/logfile.go +++ b/libpod/events/logfile.go @@ -76,7 +76,7 @@ func (e EventLogFile) Read(ctx context.Context, options ReadOptions) error { return err } switch event.Type { - case Image, Volume, Pod, System, Container: + case Image, Volume, Pod, System, Container, Network: // no-op default: return errors.Errorf("event type %s is not valid in %s", event.Type.String(), e.options.LogFilePath) diff --git a/libpod/networking_linux.go b/libpod/networking_linux.go index 3882e095a..8dce7c9fe 100644 --- a/libpod/networking_linux.go +++ b/libpod/networking_linux.go @@ -21,6 +21,7 @@ import ( cnitypes "github.com/containernetworking/cni/pkg/types/current" "github.com/containernetworking/plugins/pkg/ns" "github.com/containers/podman/v2/libpod/define" + "github.com/containers/podman/v2/libpod/events" "github.com/containers/podman/v2/libpod/network" "github.com/containers/podman/v2/pkg/errorhandling" "github.com/containers/podman/v2/pkg/netns" @@ -34,16 +35,16 @@ import ( ) // Get an OCICNI network config -func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr) ocicni.PodNetwork { +func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, ports []ocicni.PortMapping, staticIP net.IP, staticMAC net.HardwareAddr, netDescriptions ContainerNetworkDescriptions) ocicni.PodNetwork { var networkKey string if len(networks) > 0 { - // This is inconsistent for >1 network, but it's probably the + // This is inconsistent for >1 ctrNetwork, but it's probably the // best we can do. networkKey = networks[0] } else { networkKey = r.netPlugin.GetDefaultNetworkName() } - network := ocicni.PodNetwork{ + ctrNetwork := ocicni.PodNetwork{ Name: name, Namespace: name, // TODO is there something else we should put here? We don't know about Kube namespaces ID: id, @@ -55,9 +56,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port // If we have extra networks, add them if len(networks) > 0 { - network.Networks = make([]ocicni.NetAttachment, len(networks)) + ctrNetwork.Networks = make([]ocicni.NetAttachment, len(networks)) for i, netName := range networks { - network.Networks[i].Name = netName + ctrNetwork.Networks[i].Name = netName + if eth, exists := netDescriptions.getInterfaceByName(netName); exists { + ctrNetwork.Networks[i].Ifname = eth + } } } @@ -66,8 +70,8 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port // it's just the default. if len(networks) == 0 { // If len(networks) == 0 this is guaranteed to be the - // default network. - network.Networks = []ocicni.NetAttachment{{Name: networkKey}} + // default ctrNetwork. + ctrNetwork.Networks = []ocicni.NetAttachment{{Name: networkKey}} } var rt ocicni.RuntimeConfig = ocicni.RuntimeConfig{PortMappings: ports} if staticIP != nil { @@ -76,12 +80,12 @@ func (r *Runtime) getPodNetwork(id, name, nsPath string, networks []string, port if staticMAC != nil { rt.MAC = staticMAC.String() } - network.RuntimeConfig = map[string]ocicni.RuntimeConfig{ + ctrNetwork.RuntimeConfig = map[string]ocicni.RuntimeConfig{ networkKey: rt, } } - return network + return ctrNetwork } // Create and configure a new network namespace for a container @@ -110,7 +114,12 @@ func (r *Runtime) configureNetNS(ctr *Container, ctrNS ns.NetNS) ([]*cnitypes.Re if err != nil { return nil, err } - podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC) + + // Update container map of interface descriptions + if err := ctr.setupNetworkDescriptions(networks); err != nil { + return nil, err + } + podNetwork := r.getPodNetwork(ctr.ID(), podName, ctrNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ctr.state.NetInterfaceDescriptions) aliases, err := ctr.runtime.state.GetAllNetworkAliases(ctr) if err != nil { return nil, err @@ -760,7 +769,7 @@ func (r *Runtime) teardownNetNS(ctr *Container) error { requestedMAC = ctr.config.StaticMAC } - podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC) + podNetwork := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), networks, ctr.config.PortMappings, requestedIP, requestedMAC, ContainerNetworkDescriptions{}) if err := r.netPlugin.TearDownPod(podNetwork); err != nil { return errors.Wrapf(err, "error tearing down CNI namespace configuration for container %s", ctr.ID()) @@ -934,6 +943,29 @@ func (c *Container) getContainerNetworkInfo() (*define.InspectNetworkSettings, e return settings, nil } +// setupNetworkDescriptions adds networks and eth values to the container's +// network descriptions +func (c *Container) setupNetworkDescriptions(networks []string) error { + // if the map is nil and we have networks + if c.state.NetInterfaceDescriptions == nil && len(networks) > 0 { + c.state.NetInterfaceDescriptions = make(ContainerNetworkDescriptions) + } + origLen := len(c.state.NetInterfaceDescriptions) + for _, n := range networks { + // if the network is not in the map, add it + if _, exists := c.state.NetInterfaceDescriptions[n]; !exists { + c.state.NetInterfaceDescriptions.add(n) + } + } + // if the map changed, we need to save the container state + if origLen != len(c.state.NetInterfaceDescriptions) { + if err := c.save(); err != nil { + return err + } + } + return nil +} + // resultToBasicNetworkConfig produces an InspectBasicNetworkConfig from a CNI // result func resultToBasicNetworkConfig(result *cnitypes.Result) (define.InspectBasicNetworkConfig, error) { @@ -984,19 +1016,14 @@ func (w *logrusDebugWriter) Write(p []byte) (int, error) { return len(p), nil } -// DisconnectContainerFromNetwork removes a container from its CNI network -func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error { - ctr, err := r.LookupContainer(nameOrID) +// NetworkDisconnect removes a container from the network +func (c *Container) NetworkDisconnect(nameOrID, netName string, force bool) error { + networks, err := c.networksByNameIndex() if err != nil { return err } - networks, err := ctr.networksByNameIndex() - if err != nil { - return err - } - - exists, err := network.Exists(r.config, netName) + exists, err := network.Exists(c.runtime.config, netName) if err != nil { return err } @@ -1009,48 +1036,48 @@ func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force return errors.Errorf("container %s is not connected to network %s", nameOrID, netName) } - ctr.lock.Lock() - defer ctr.lock.Unlock() - if err := ctr.syncContainer(); err != nil { + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { return err } - podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil) - if err := r.netPlugin.TearDownPod(podConfig); err != nil { + if c.state.State != define.ContainerStateRunning { + return errors.Wrapf(define.ErrCtrStateInvalid, "cannot disconnect container %s from networks as it is not running", nameOrID) + } + if c.state.NetNS == nil { + return errors.Wrapf(define.ErrNoNetwork, "unable to disconnect %s from %s", nameOrID, netName) + } + podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) + if err := c.runtime.netPlugin.TearDownPod(podConfig); err != nil { return err } - if err := r.state.NetworkDisconnect(ctr, netName); err != nil { + if err := c.runtime.state.NetworkDisconnect(c, netName); err != nil { return err } // update network status - networkStatus := ctr.state.NetworkStatus - // if len is one and we confirmed earlier that the container is in - // fact connected to the network, then just return an empty slice - if len(networkStatus) == 1 { - ctr.state.NetworkStatus = make([]*cnitypes.Result, 0) - } else { - // clip out the index of the network - networkStatus[len(networkStatus)-1], networkStatus[index] = networkStatus[index], networkStatus[len(networkStatus)-1] - // shorten the slice by one - ctr.state.NetworkStatus = networkStatus[:len(networkStatus)-1] + networkStatus := c.state.NetworkStatus + // clip out the index of the network + tmpNetworkStatus := make([]*cnitypes.Result, len(networkStatus)-1) + for k, v := range networkStatus { + if index != k { + tmpNetworkStatus = append(tmpNetworkStatus, v) + } } - return nil + c.state.NetworkStatus = tmpNetworkStatus + c.newNetworkEvent(events.NetworkDisconnect, netName) + return c.save() } -// ConnectContainerToNetwork connects a container to a CNI network -func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error { - ctr, err := r.LookupContainer(nameOrID) +// ConnnectNetwork connects a container to a given network +func (c *Container) NetworkConnect(nameOrID, netName string, aliases []string) error { + networks, err := c.networksByNameIndex() if err != nil { return err } - networks, err := ctr.networksByNameIndex() - if err != nil { - return err - } - - exists, err := network.Exists(r.config, netName) + exists, err := network.Exists(c.runtime.config, netName) if err != nil { return err } @@ -1058,25 +1085,34 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] return errors.Wrap(define.ErrNoSuchNetwork, netName) } - _, nameExists := networks[netName] - if !nameExists && len(networks) > 0 { - return errors.Errorf("container %s is not connected to network %s", nameOrID, netName) + c.lock.Lock() + defer c.lock.Unlock() + if err := c.syncContainer(); err != nil { + return err } - ctr.lock.Lock() - defer ctr.lock.Unlock() - if err := ctr.syncContainer(); err != nil { + if c.state.State != define.ContainerStateRunning { + return errors.Wrapf(define.ErrCtrStateInvalid, "cannot connect container %s to networks as it is not running", nameOrID) + } + if c.state.NetNS == nil { + return errors.Wrapf(define.ErrNoNetwork, "unable to connect %s to %s", nameOrID, netName) + } + if err := c.runtime.state.NetworkConnect(c, netName, aliases); err != nil { return err } - if err := r.state.NetworkConnect(ctr, netName, aliases); err != nil { + ctrNetworks, err := c.networks() + if err != nil { return err } - - podConfig := r.getPodNetwork(ctr.ID(), ctr.Name(), ctr.state.NetNS.Path(), []string{netName}, ctr.config.PortMappings, nil, nil) + // Update network descriptions + if err := c.setupNetworkDescriptions(ctrNetworks); err != nil { + return err + } + podConfig := c.runtime.getPodNetwork(c.ID(), c.Name(), c.state.NetNS.Path(), []string{netName}, c.config.PortMappings, nil, nil, c.state.NetInterfaceDescriptions) podConfig.Aliases = make(map[string][]string, 1) podConfig.Aliases[netName] = aliases - results, err := r.netPlugin.SetUpPod(podConfig) + results, err := c.runtime.netPlugin.SetUpPod(podConfig) if err != nil { return err } @@ -1094,11 +1130,11 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] } // update network status - networkStatus := ctr.state.NetworkStatus + networkStatus := c.state.NetworkStatus // if len is one and we confirmed earlier that the container is in // fact connected to the network, then just return an empty slice if len(networkStatus) == 0 { - ctr.state.NetworkStatus = append(ctr.state.NetworkStatus, networkResults...) + c.state.NetworkStatus = append(c.state.NetworkStatus, networkResults...) } else { // build a list of network names so we can sort and // get the new name's index @@ -1117,5 +1153,30 @@ func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases [] copy(networkStatus[index+1:], networkStatus[index:]) networkStatus[index] = networkResults[0] } - return nil + c.newNetworkEvent(events.NetworkConnect, netName) + return c.save() +} + +// DisconnectContainerFromNetwork removes a container from its CNI network +func (r *Runtime) DisconnectContainerFromNetwork(nameOrID, netName string, force bool) error { + if rootless.IsRootless() { + return errors.New("network connect is not enabled for rootless containers") + } + ctr, err := r.LookupContainer(nameOrID) + if err != nil { + return err + } + return ctr.NetworkDisconnect(nameOrID, netName, force) +} + +// ConnectContainerToNetwork connects a container to a CNI network +func (r *Runtime) ConnectContainerToNetwork(nameOrID, netName string, aliases []string) error { + if rootless.IsRootless() { + return errors.New("network disconnect is not enabled for rootless containers") + } + ctr, err := r.LookupContainer(nameOrID) + if err != nil { + return err + } + return ctr.NetworkConnect(nameOrID, netName, aliases) } diff --git a/pkg/api/handlers/libpod/networks.go b/pkg/api/handlers/libpod/networks.go index 78e525f1f..f1578f829 100644 --- a/pkg/api/handlers/libpod/networks.go +++ b/pkg/api/handlers/libpod/networks.go @@ -131,3 +131,29 @@ func InspectNetwork(w http.ResponseWriter, r *http.Request) { } utils.WriteResponse(w, http.StatusOK, reports) } + +// Connect adds a container to a network +func Connect(w http.ResponseWriter, r *http.Request) { + runtime := r.Context().Value("runtime").(*libpod.Runtime) + + var netConnect entities.NetworkConnectOptions + if err := json.NewDecoder(r.Body).Decode(&netConnect); err != nil { + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, errors.Wrap(err, "Decode()")) + return + } + name := utils.GetName(r) + err := runtime.ConnectContainerToNetwork(netConnect.Container, name, netConnect.Aliases) + if err != nil { + if errors.Cause(err) == define.ErrNoSuchCtr { + utils.ContainerNotFound(w, netConnect.Container, err) + return + } + if errors.Cause(err) == define.ErrNoSuchNetwork { + utils.Error(w, "network not found", http.StatusNotFound, err) + return + } + utils.Error(w, "Something went wrong.", http.StatusInternalServerError, err) + return + } + utils.WriteResponse(w, http.StatusOK, "OK") +} diff --git a/pkg/api/server/register_networks.go b/pkg/api/server/register_networks.go index 6222006e5..ea169cbdf 100644 --- a/pkg/api/server/register_networks.go +++ b/pkg/api/server/register_networks.go @@ -253,5 +253,59 @@ func (s *APIServer) registerNetworkHandlers(r *mux.Router) error { // 500: // $ref: "#/responses/InternalError" r.HandleFunc(VersionedPath("/libpod/networks/create"), s.APIHandler(libpod.CreateNetwork)).Methods(http.MethodPost) + // swagger:operation POST /libpod/networks/{name}/connect libpod libpodConnectNetwork + // --- + // tags: + // - networks + // summary: Connect container to network + // description: Connect a container to a network. + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name of the network + // - in: body + // name: create + // description: attributes for connecting a container to a network + // schema: + // $ref: "#/definitions/NetworkConnectRequest" + // responses: + // 200: + // description: OK + // 404: + // $ref: "#/responses/NoSuchNetwork" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/networks/{name}/connect"), s.APIHandler(libpod.Connect)).Methods(http.MethodPost) + // swagger:operation POST /libpod/networks/{name}/disconnect libpod libpodDisconnectNetwork + // --- + // tags: + // - networks + // summary: Disconnect container from network + // description: Disconnect a container from a network. + // produces: + // - application/json + // parameters: + // - in: path + // name: name + // type: string + // required: true + // description: the name of the network + // - in: body + // name: create + // description: attributes for disconnecting a container from a network + // schema: + // $ref: "#/definitions/NetworkDisconnectRequest" + // responses: + // 200: + // description: OK + // 404: + // $ref: "#/responses/NoSuchNetwork" + // 500: + // $ref: "#/responses/InternalError" + r.HandleFunc(VersionedPath("/libpod/networks/{name}/disconnect"), s.APIHandler(compat.Disconnect)).Methods(http.MethodPost) return nil } diff --git a/pkg/bindings/network/network.go b/pkg/bindings/network/network.go index 151d15d3e..1d4be8a4c 100644 --- a/pkg/bindings/network/network.go +++ b/pkg/bindings/network/network.go @@ -88,3 +88,41 @@ func List(ctx context.Context, options entities.NetworkListOptions) ([]*entities } return netList, response.Process(&netList) } + +// Disconnect removes a container from a given network +func Disconnect(ctx context.Context, networkName string, options entities.NetworkDisconnectOptions) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + params := url.Values{} + body, err := jsoniter.MarshalToString(options) + if err != nil { + return err + } + stringReader := strings.NewReader(body) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/networks/%s/disconnect", params, nil, networkName) + if err != nil { + return err + } + return response.Process(nil) +} + +// Connect adds a container to a network +func Connect(ctx context.Context, networkName string, options entities.NetworkConnectOptions) error { + conn, err := bindings.GetClient(ctx) + if err != nil { + return err + } + params := url.Values{} + body, err := jsoniter.MarshalToString(options) + if err != nil { + return err + } + stringReader := strings.NewReader(body) + response, err := conn.DoRequest(stringReader, http.MethodPost, "/networks/%s/connect", params, nil, networkName) + if err != nil { + return err + } + return response.Process(nil) +} diff --git a/pkg/domain/entities/engine_container.go b/pkg/domain/entities/engine_container.go index 8ab72dbd8..b051d3eec 100644 --- a/pkg/domain/entities/engine_container.go +++ b/pkg/domain/entities/engine_container.go @@ -50,7 +50,9 @@ type ContainerEngine interface { SystemPrune(ctx context.Context, options SystemPruneOptions) (*SystemPruneReport, error) HealthCheckRun(ctx context.Context, nameOrID string, options HealthCheckOptions) (*define.HealthCheckResults, error) Info(ctx context.Context) (*define.Info, error) + NetworkConnect(ctx context.Context, networkname string, options NetworkConnectOptions) error NetworkCreate(ctx context.Context, name string, options NetworkCreateOptions) (*NetworkCreateReport, error) + NetworkDisconnect(ctx context.Context, networkname string, options NetworkDisconnectOptions) error NetworkInspect(ctx context.Context, namesOrIds []string, options InspectOptions) ([]NetworkInspectReport, []error, error) NetworkList(ctx context.Context, options NetworkListOptions) ([]*NetworkListReport, error) NetworkRm(ctx context.Context, namesOrIds []string, options NetworkRmOptions) ([]*NetworkRmReport, error) diff --git a/pkg/domain/entities/network.go b/pkg/domain/entities/network.go index 3cc970531..86c2e1bcd 100644 --- a/pkg/domain/entities/network.go +++ b/pkg/domain/entities/network.go @@ -49,3 +49,17 @@ type NetworkCreateOptions struct { type NetworkCreateReport struct { Filename string } + +// NetworkDisconnectOptions describes options for disconnecting +// containers from networks +type NetworkDisconnectOptions struct { + Container string + Force bool +} + +// NetworkConnectOptions describes options for connecting +// a container to a network +type NetworkConnectOptions struct { + Aliases []string + Container string +} diff --git a/pkg/domain/infra/abi/network.go b/pkg/domain/infra/abi/network.go index b7f90a034..c52584565 100644 --- a/pkg/domain/infra/abi/network.go +++ b/pkg/domain/infra/abi/network.go @@ -138,3 +138,12 @@ func ifPassesFilterTest(netconf *libcni.NetworkConfigList, filter []string) bool } return result } + +// NetworkDisconnect removes a container from a given network +func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname string, options entities.NetworkDisconnectOptions) error { + return ic.Libpod.DisconnectContainerFromNetwork(options.Container, networkname, options.Force) +} + +func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error { + return ic.Libpod.ConnectContainerToNetwork(options.Container, networkname, options.Aliases) +} diff --git a/pkg/domain/infra/tunnel/network.go b/pkg/domain/infra/tunnel/network.go index 15527e02c..10ae03045 100644 --- a/pkg/domain/infra/tunnel/network.go +++ b/pkg/domain/infra/tunnel/network.go @@ -55,3 +55,13 @@ func (ic *ContainerEngine) NetworkRm(ctx context.Context, namesOrIds []string, o func (ic *ContainerEngine) NetworkCreate(ctx context.Context, name string, options entities.NetworkCreateOptions) (*entities.NetworkCreateReport, error) { return network.Create(ic.ClientCxt, options, &name) } + +// NetworkDisconnect removes a container from a given network +func (ic *ContainerEngine) NetworkDisconnect(ctx context.Context, networkname string, options entities.NetworkDisconnectOptions) error { + return network.Disconnect(ic.ClientCxt, networkname, options) +} + +// NetworkConnect removes a container from a given network +func (ic *ContainerEngine) NetworkConnect(ctx context.Context, networkname string, options entities.NetworkConnectOptions) error { + return network.Connect(ic.ClientCxt, networkname, options) +} diff --git a/test/e2e/network_test.go b/test/e2e/network_test.go index 9f2fb4459..139a90ac7 100644 --- a/test/e2e/network_test.go +++ b/test/e2e/network_test.go @@ -346,4 +346,157 @@ var _ = Describe("Podman network", func() { c3.WaitWithDefaultTimeout() Expect(c3.ExitCode()).To(BeZero()) }) + + It("bad network name in disconnect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + dis := podmanTest.Podman([]string{"network", "disconnect", "foobar", "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("bad container name in network disconnect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName, "foobar"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("podman network disconnect with invalid container state should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + }) + + It("podman network disconnect", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "disconnect", netName, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).ToNot(BeZero()) + }) + + It("bad network name in connect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + dis := podmanTest.Podman([]string{"network", "connect", "foobar", "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("bad container name in network connect should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + dis := podmanTest.Podman([]string{"network", "connect", netName, "foobar"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + + }) + + It("podman connect on a container that already is connected to the network should error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + con := podmanTest.Podman([]string{"network", "connect", netName, "test"}) + con.WaitWithDefaultTimeout() + Expect(con.ExitCode()).ToNot(BeZero()) + }) + + It("podman network connect with invalid container state should result in error", func() { + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"create", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + dis := podmanTest.Podman([]string{"network", "connect", netName, "test"}) + dis.WaitWithDefaultTimeout() + Expect(dis.ExitCode()).ToNot(BeZero()) + }) + + It("podman network connect", func() { + SkipIfRemote("This requires a pending PR to be merged before it will work") + SkipIfRootless("network connect and disconnect are only rootfull") + netName := "aliasTest" + stringid.GenerateNonCryptoID() + session := podmanTest.Podman([]string{"network", "create", netName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(netName) + + ctr := podmanTest.Podman([]string{"run", "-dt", "--name", "test", "--network", netName, ALPINE, "top"}) + ctr.WaitWithDefaultTimeout() + Expect(ctr.ExitCode()).To(BeZero()) + + exec := podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth0"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + + // Create a second network + newNetName := "aliasTest" + stringid.GenerateNonCryptoID() + session = podmanTest.Podman([]string{"network", "create", newNetName}) + session.WaitWithDefaultTimeout() + Expect(session.ExitCode()).To(BeZero()) + defer podmanTest.removeCNINetwork(newNetName) + + connect := podmanTest.Podman([]string{"network", "connect", newNetName, "test"}) + connect.WaitWithDefaultTimeout() + Expect(connect.ExitCode()).To(BeZero()) + + exec = podmanTest.Podman([]string{"exec", "-it", "test", "ip", "addr", "show", "eth1"}) + exec.WaitWithDefaultTimeout() + Expect(exec.ExitCode()).To(BeZero()) + }) }) |