Edits history of script submission #24088 for ' Upsert pool (f5_bigip)'

  • bunnative
    One script reply has been approved by the moderators
    Ap­pro­ved
    //native
    
    async function connect(auth: RT.F5Bigip): Promise<{
      base: string
      headers: { [key: string]: string }
      release: () => Promise<void>
    }> {
      const base = auth.base_url.replace(/\/+$/, "")
      if (auth.auth_method === "basic") {
        return {
          base,
          headers: {
            Authorization: `Basic ${btoa(`${auth.username}:${auth.password}`)}`,
          },
          release: async () => {},
        }
      }
      const login = await fetch(`${base}/mgmt/shared/authn/login`, {
        method: "POST",
        headers: { "Content-Type": "application/json", Accept: "application/json" },
        body: JSON.stringify({
          username: auth.username,
          password: auth.password,
          loginProviderName: auth.login_provider_name || "tmos",
        }),
      })
      if (!login.ok) {
        throw new Error(`${login.status} ${await login.text()}`)
      }
      const token: string = (await login.json()).token.token
      return {
        base,
        headers: { "X-F5-Auth-Token": token },
        release: async () => {
          await fetch(
            `${base}/mgmt/shared/authz/tokens/${encodeURIComponent(token)}`,
            {
              method: "DELETE",
              headers: { "X-F5-Auth-Token": token },
            }
          )
        },
      }
    }
    
    // "/Common/web" -> "~Common~web" (iControl REST object reference); "%" (route domains), "?" and "#" are escaped
    function ref(fullPath: string) {
      return fullPath.replace(/\//g, "~").replace(/[%?#]/g, encodeURIComponent)
    }
    
    // "/Common/http  and /Common/tcp " -> "/Common/http and /Common/tcp"
    function normalizeMonitor(monitor: string | undefined) {
      return (monitor ?? "").trim().replace(/\s+/g, " ")
    }
    
    /**
     * Upsert pool
     * Declaratively converge an LTM pool: read the current pool and members, compute the difference with the intended monitor, load balancing mode, description and member list, and apply only that difference. With dry_run (the default) nothing is changed and the diff is returned.
     */
    export async function main(
      auth: RT.F5Bigip,
      name: string,
      partition: string | undefined,
      monitor: string | undefined,
      load_balancing_mode: string | undefined,
      description: string | undefined,
      members: { name: string; address?: string }[] | undefined,
      dry_run: boolean = true
    ) {
      const poolPartition = partition || "Common"
      const poolPath = `/${poolPartition}/${name}`
      const poolUrl = (f5: { base: string }) =>
        `${f5.base}/mgmt/tm/ltm/pool/${ref(poolPath)}`
    
      // Intended state. Empty inputs leave that attribute unmanaged.
      const wantAttrs: { [key: string]: string } = {}
      if (monitor) wantAttrs.monitor = normalizeMonitor(monitor)
      if (load_balancing_mode) wantAttrs.loadBalancingMode = load_balancing_mode
      if (description) wantAttrs.description = description
      const manageMembers = members != null && members.length > 0
      const wantMembers = (members ?? []).map((m) => {
        const fullPath = m.name.startsWith("/")
          ? m.name
          : `/${poolPartition}/${m.name}`
        const parts = fullPath.split("/")
        return {
          fullPath,
          body: {
            name: parts[parts.length - 1],
            partition: parts[1],
            ...(m.address ? { address: m.address } : {}),
          },
        }
      })
    
      const f5 = await connect(auth)
      try {
        const json = {
          ...f5.headers,
          "Content-Type": "application/json",
          Accept: "application/json",
        }
        const call = async (url: string, method: string, body?: unknown) => {
          const response = await fetch(url, {
            method,
            headers:
              body === undefined
                ? { ...f5.headers, Accept: "application/json" }
                : json,
            body: body === undefined ? undefined : JSON.stringify(body),
          })
          if (!response.ok) {
            throw new Error(
              `${method} ${url}: ${response.status} ${await response.text()}`
            )
          }
          return response
        }
    
        // Current state
        const poolResponse = await fetch(poolUrl(f5), {
          headers: { ...f5.headers, Accept: "application/json" },
        })
        if (!poolResponse.ok && poolResponse.status !== 404) {
          throw new Error(`${poolResponse.status} ${await poolResponse.text()}`)
        }
        const exists = poolResponse.ok
        const current = exists ? await poolResponse.json() : null
        const currentMembers: string[] = []
        if (exists) {
          const membersResponse = await call(`${poolUrl(f5)}/members`, "GET")
          for (const m of (await membersResponse.json()).items ?? []) {
            currentMembers.push(m.fullPath ?? `/${m.partition}/${m.name}`)
          }
        }
    
        // Difference
        const attributes: { [key: string]: { from: string | null; to: string } } =
          {}
        for (const [key, to] of Object.entries(wantAttrs)) {
          const raw = current?.[key] ?? null
          const from =
            key === "monitor" && raw != null ? normalizeMonitor(raw) : raw
          if (from !== to) attributes[key] = { from, to }
        }
        const membersToAdd = manageMembers
          ? wantMembers.filter((m) => !currentMembers.includes(m.fullPath))
          : []
        const membersToRemove = manageMembers
          ? currentMembers.filter((p) => !wantMembers.some((m) => m.fullPath === p))
          : []
        const diff = {
          pool: poolPath,
          action: !exists
            ? "create"
            : Object.keys(attributes).length ||
                membersToAdd.length ||
                membersToRemove.length
              ? "update"
              : "none",
          attributes,
          members_to_add: membersToAdd.map((m) => m.fullPath),
          members_to_remove: membersToRemove,
        }
        const changed = diff.action !== "none"
    
        // Only an explicit false applies changes (an API caller's null stays a dry run)
        const dryRun = dry_run !== false
        if (dryRun || !changed) {
          return { changed, dry_run: dryRun, applied: false, diff }
        }
    
        // Apply only the difference
        if (!exists) {
          await call(`${f5.base}/mgmt/tm/ltm/pool`, "POST", {
            name,
            partition: poolPartition,
            ...wantAttrs,
          })
        } else if (Object.keys(attributes).length) {
          const patch: { [key: string]: string } = {}
          for (const [key, change] of Object.entries(attributes))
            patch[key] = change.to
          await call(poolUrl(f5), "PATCH", patch)
        }
        for (const m of membersToAdd) {
          await call(`${poolUrl(f5)}/members`, "POST", m.body)
        }
        for (const path of membersToRemove) {
          await call(`${poolUrl(f5)}/members/${ref(path)}`, "DELETE")
        }
    
        return { changed, dry_run: false, applied: true, diff }
      } finally {
        await f5.release()
      }
    }
    

    Submitted by hugo989 7 hours ago